【数据结构】二叉搜索树的原理及其实现

简介: 【数据结构】二叉搜索树的原理及其实现

认识二叉搜索树

二叉搜索树又称二叉排序树或者二叉查找树,本质上是一颗二叉树。对于二叉搜索树的任一节点,具有以下一些性质:

  • 如果左子树非空,那么左子树上的值一定都小于根节点的值
  • 如果右子树非空,那么右子树上的值一定都大于根节点的值
  • 它的左右子树都是二叉搜索树

例如下图:

根据二叉搜索树的性质,左节点的值 < 根节点的值 < 右节点的值

我们很容易想到,对二叉搜索树进行中序遍历就可以得到一个节点值递增的序列。因此我们也将二叉搜索树称为二叉排序树。

二叉搜索树的应用其实非常广泛,像我们经常用的map,set等容器,底层其实就是一个二叉搜索树。

二叉搜索树的原理及实现

查找

  1. 从根节点开始,类似于二分,比要是查找的节点值大于当前节点值,那就向当前节点的右孩子节点继续找。否则就往左孩子方向找。直到找到为止,或者走到空为止。
  2. 查找的时间复杂度为树的高度,设二叉搜索树的节点数为N,那么平均查找时间就是O(logN)。当然,如果是极端情况下,即退化成一个单链表,查找的时间复杂度就是O(N)

建树

  • 如果当前树为空,那就把当前节点设置为树的根节点。
  • 和查找过程类似,大于当前节点就往右走,小于就往左走,等于就退出(防止节点冗余)。直到直到一个空位置插上,往后的插入操作也是一样
  • 建树的时间复杂度与查找的时间复杂度有关,一般情况下建树的时间复杂度是NlogN。极端情况就是N^2,这种时候,二叉树就退化成了一个单链表。

颗二叉搜索树的性能。

无论是插入操作还是查找操作的时间复杂度都取决于查找的时间效率,也就意味着,查找的效率极大程度上体现一颗二叉搜索树的性能。

等于有n个节点的二叉搜索树

  • 最优情况:二叉搜索树是完全二叉树或者是接近完全二叉树(左右子树的节点数量相差不大),其平均比较时间是logN。
  • 最坏情况:退化成类似一个单链表,平均比较次数为N^2。

根据以上分析得出结论,树的高度越低意味着整棵树越像完全二叉树,查找的效率也就越高。越高意味着越像单支树,查找的效率也就越低。为了保持二叉搜索树的性能,也就衍生出了AVL树和红黑树。

二叉搜索树的key模型实现

代码:


namespace k {

  template<class T>
  class BSTNode {
  public:
    BSTNode()
      :_val(0)
    {
      left = nullptr;
      right = nullptr;
    }

    BSTNode(T val)
      :_val(val)
    {
      left = nullptr;
      right = nullptr;
    }
    T _val;
    BSTNode<T>* left;
    BSTNode<T>* right;
  };


  template<class T>
  class BSTree {

    typedef BSTNode<T> Node;
    typedef Node* pNode;
  public:
    BSTree()
      :_root(nullptr)
    {

    }

    //删除
    bool Erase(const T& val) {
      if (_root == nullptr)return false;
      if (!Find(val))return false;
      pNode cur = _root;
      pNode pa = _root;
      while (cur) {
        if (cur->_val < val) {
          pa = cur;
          cur = cur->right;
        }
        else if (cur->_val > val) {
          pa = cur;
          cur = cur->left;
        }
        else break;
      }

      //被删除的节点孩子有空
      if (cur->left == nullptr) {
        if (cur == _root) {//如果被删除的节点是头节点
          pNode t = _root;
          _root = cur->right;
          delete t;
        }
        else if (pa->_val < cur->_val) {
          pa->right = cur->right;
          delete cur;
        }
        else {
          pa->left = cur->right;
          delete cur;
        }

      }
      else if (cur->right == nullptr) {
        if (cur == _root) {//如果被删除的节点是头节点
          pNode t = _root;
          _root = cur->left;
          delete t;
        }
        else if (pa->_val < cur->_val) {
          pa->right = cur->left;
          delete cur;
        }
        else {
          pa->left = cur->left;
          delete cur;
        }

      }//cur的两个孩子节点都不为空
      else {
        pNode ffa = cur;
        pNode minright = cur->right;
        while (minright->left) {
          ffa = minright;
          minright = minright->left;
        }

        ffa->left = minright->right;
        std::swap(minright->_val, cur->_val);
        delete minright;
      }


    }


    //查找
    bool Find(const T& val) {
      assert(_root != nullptr);
      pNode cur = _root;
      while (cur) {
        if (cur->_val < val) {
          cur = cur->right;
        }
        else if (cur->_val > val) {
          cur = cur->left;
        }
        else return true;

      }
      return false;
    }

    //插入
    bool Insert(const T& val) {
      if (_root == nullptr) {
        _root = new Node(val);
        return true;
      }

      pNode cur = _root;
      pNode pa = nullptr;
      while (cur) {
        if (cur->_val < val) {
          pa = cur;
          cur = cur->right;
        }
        else if (cur->_val > val) {
          pa = cur;
          cur = cur->left;
        }
        else return false;//已经存在无需插入

      }
      if (pa->_val > val) {
        pa->left = new Node(val);
      }
      else {
        pa->right = new Node(val);
      }
      return true;
    }
    //遍历
    void InOrder() {
      _InOrder(_root);
      std::cout << std::endl;
    }
  private:
    void _InOrder(pNode root) {
      if (root == nullptr)return;
      _InOrder(root->left);
      std::cout << root->_val << " ";
      _InOrder(root->right);
    }
    pNode _root;
  };
}

二叉搜索树的key-value模型实现

代码:


namespace k_val {

  template<class K,class V>
  class BSTNode {
  public:
    BSTNode()
      :_key(0)
      ,_val(0)
    {
      left = nullptr;
      right = nullptr;
    }

    BSTNode(const K& key,const V& val)
      :_key(key)
      ,_val(val)
    {
      left = nullptr;
      right = nullptr;
    }
    K _key;
    V _val;
    BSTNode<K,V>* left;
    BSTNode<K,V>* right;
  };


  template<class K,class V>
  class BSTree {

    typedef BSTNode<K,V> Node;
    typedef Node* pNode;
  public:
    BSTree()
      :_root(nullptr)
    {

    }

    //删除
    bool Erase(const K& key) {
      if (_root == nullptr)return false;
      if (!Find(key))return false;
      pNode cur = _root;
      pNode pa = _root;
      while (cur) {
        if (cur->_key < key) {
          pa = cur;
          cur = cur->right;
        }
        else if (cur->_key > key) {
          pa = cur;
          cur = cur->left;
        }
        else break;
      }

      //被删除的节点孩子有空
      if (cur->left == nullptr) {
        if (cur == _root) {//如果被删除的节点是头节点
          pNode t = _root;
          _root = cur->right;
          delete t;
        }
        else if (pa->_key < cur->_key) {
          pa->right = cur->right;
          delete cur;
        }
        else {
          pa->left = cur->right;
          delete cur;
        }

      }
      else if (cur->right == nullptr) {
        if (cur == _root) {//如果被删除的节点是头节点
          pNode t = _root;
          _root = cur->left;
          delete t;
        }
        else if (pa->_key < cur->_key) {
          pa->right = cur->left;
          delete cur;
        }
        else {
          pa->left = cur->left;
          delete cur;
        }

      }//cur的两个孩子节点都不为空
      else {
        pNode ffa = cur;
        pNode minright = cur->right;
        while (minright->left) {
          ffa = minright;
          minright = minright->left;
        }

        ffa->left = minright->right;
        std::swap(minright->_key, cur->_key);
        delete minright;
      }


    }


    //查找
    pNode Find(const K& key) {
      assert(_root != nullptr);
      pNode cur = _root;
      while (cur) {
        if (cur->_key < key) {
          cur = cur->right;
        }
        else if (cur->_key > key) {
          cur = cur->left;
        }
        else return cur;

      }
      return nullptr;
    }

    //插入
    bool Insert(const K& key,const V& val) {
      if (_root == nullptr) {
        _root = new Node(key,val);
        return true;
      }

      pNode cur = _root;
      pNode pa = nullptr;
      while (cur) {
        if (cur->_key < key) {
          pa = cur;
          cur = cur->right;
        }
        else if (cur->_key > key) {
          pa = cur;
          cur = cur->left;
        }
        else return false;//已经存在无需插入

      }
      if (pa->_key > key) {
        pa->left = new Node(key,val);
      }
      else {
        pa->right = new Node(key,val);
      }
      return true;
    }
    //遍历
    void InOrder() {
      _InOrder(_root);
      std::cout << std::endl;
    }
  private:
    void _InOrder(pNode root) {
      if (root == nullptr)return;
      _InOrder(root->left);
      std::cout << root->_val << " ";
      _InOrder(root->right);
    }
    pNode _root;
  };
}


相关文章
|
5月前
|
存储 NoSQL Redis
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
Redis系列学习文章分享---第十六篇(Redis原理1篇--Redis数据结构-动态字符串,insert,Dict,ZipList,QuickList,SkipList,RedisObject)
81 1
|
2月前
|
存储 Java Serverless
【数据结构】哈希表&二叉搜索树详解
本文详细介绍了二叉搜索树和哈希表这两种数据结构。二叉搜索树是一种特殊二叉树,具有左子树节点值小于根节点、右子树节点值大于根节点的特点,并且不允许键值重复。文章给出了插入、删除和搜索等方法的具体实现。哈希表则通过哈希函数将键名映射为数组下标,实现快速查找,其插入、删除和查找操作时间复杂度理想情况下为O(1)。文中还讨论了哈希函数的设计原则、哈希冲突的解决方法及哈希表的实现细节。
38 8
【数据结构】哈希表&二叉搜索树详解
|
1月前
|
消息中间件 存储 Java
数据结构之 - 深入探析队列数据结构: 助你理解其原理与应用
数据结构之 - 深入探析队列数据结构: 助你理解其原理与应用
28 4
|
5月前
|
存储 消息中间件 缓存
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
Redis系列学习文章分享---第十七篇(Redis原理篇--数据结构,网络模型)
95 0
|
1月前
|
搜索推荐 C++
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(一)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
2月前
|
设计模式 安全 Java
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
假如有T1、T2两个线程同时对某链表扩容,他们都标记头结点和第二个结点,此时T2阻塞,T1执行完扩容后链表结点顺序反过来,此时T2恢复运行再进行翻转就会产生环形链表,即B.next=A;采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。JDK8中,HashMap采用尾插法,扩容时链表节点位置不会翻转,解决了扩容死循环问题,但是性能差了一点,因为要遍历链表再查到尾部。例如15(即2^4-1)的二进制为1111,31的二进制为11111,63的二进制为111111,127的二进制为1111111。
HashMap底层原理:数据结构+put()流程+2的n次方+死循环+数据覆盖问题
|
1月前
|
搜索推荐 索引
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理(二)
【初阶数据结构】深度解析七大常见排序|掌握底层逻辑与原理
|
1月前
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(三)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
1月前
|
存储
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(二)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
1月前
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(一)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现