二叉搜索树(C++实现)

文章目录

  • [1. 二叉搜索树的概念](#1. 二叉搜索树的概念)
  • [2. 二叉搜索树的性能分析](#2. 二叉搜索树的性能分析)
  • [3. 二叉搜索树的插入](#3. 二叉搜索树的插入)
  • 4.二叉搜索树的常见功能
    • [4.1 遍历](#4.1 遍历)
      • [4.1.1 非递归实现](#4.1.1 非递归实现)
      • [4.1.2 递归实现](#4.1.2 递归实现)
    • [4.2 二叉搜索树的查找](#4.2 二叉搜索树的查找)
    • [4.3 二叉搜索树的删除](#4.3 二叉搜索树的删除)
  • [5. 二叉搜索树key和key/value使用场景](#5. 二叉搜索树key和key/value使用场景)
    • [5.1 key搜索场景](#5.1 key搜索场景)
    • [5.2 key/value搜索场景:](#5.2 key/value搜索场景:)
    • [5.3 key/value二叉搜索树代码实现](#5.3 key/value二叉搜索树代码实现)

1. 二叉搜索树的概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
  • 它的左右子树也分别为二叉搜索树
  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续我们学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值
cpp 复制代码
//节点
template<class K>
struct BSTNode
{
    K _key;
    BSTNode<K>* _left;
    BSTNode<K>* _right;

    BSTNode(const K& key)
        :_key(key)
        , _left(nullptr)
        , _right(nullptr)
    {}
};
//树的结构
template<class k>
class BSTree
{
    typedef BSTNode<k> Node;
private:
    Node* root=nullptr;    
};

2. 二叉搜索树的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为: log ⁡ 2 N \log_2 N log2N

最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为: N N N

所以综合而言二叉搜索树增删查改时间复杂度为: O ( N ) O(N) O(N)

那么这样的效率显然是无法满足我们需求的,我们后期需要二叉搜索树的变形,平衡二叉搜索树AVL树红黑树,才能适用于我们在内存中存储和搜索数据。

另外需要说明的是,二分查找也可以实现 O ( log ⁡ 2 N ) O(\log_2 N) O(log2N)级别的查找效率,但是二分查找有两大缺陷:

  1. 需要存储在支持下标随机访问的结构中,并且有序。
  2. 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插入和删除数据一般需要挪动数据。

这里也就体现出了平衡二叉搜索树的价值。

3. 二叉搜索树的插入

插入的具体过程如下:

  1. 树为空,则直接新增结点,赋值给root指针
  2. 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  3. 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑一致性,插入相等的值不要一会往右走,一会往左走)

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

我们这里实现不支持重复数据的版本:

cpp 复制代码
    bool Insert(const K& key)
    {
        //根节点单独考虑
        if(_root==nullptr)
        {
            _root=new Node(key);
            return true;
        }
        
        Node* parent = nullptr;//记录cur的父节点
        Node* cur = _root;//临时节点 用于遍历的
        while (cur)
        {
            //当节点数据大的时候,转移到左子树,parent用于记录节点
            if (cur->_key > key)
            {
                parent=cur;
                cur=cur->_left;
            }
            //当节点数据小的时候,转移到右子树,parent用于记录节点
            else if (cur->_key < key)
            {
                parent=cur;
                cur=cur->_right;
            }
            else
            {
                return false //当数据相同,插入失败
            }
        }
        
      //创建新节点
        cur=new BSTNode(key);
      //用新节点和parent节点数据比较,判断是在parent节点左边还是右边
        if (cur->_key>parent->_key)
        {
            parent->_right=cur;
        }
        else
        {
            parent->_left=cur;
        }
     
      return true;
    }

4.二叉搜索树的常见功能

4.1 遍历

4.1.1 非递归实现

遍历我们选择中序遍历,因为可以正好让树的数据以升序的形式遍历。

但是这里我们传统的写法会有一些问题,因为我们的_rootprivate成员,无法访问,所以就没法传根节点 。对此,我们可以通过函数套用函数的形式实现(因为_root在类里面可以调用。)

cpp 复制代码
    void InOrder()
    {
        _InOrder(_root);
    }
    //中序遍历
    void _InOrder(Node* root)
    {
        if (root==nullptr)
        {
            return;
        }
        
        _InOrder(root->_left);
        cout<<root->_key<<" ";
        _InOrder(root->_right);
    }

4.1.2 递归实现

cpp 复制代码
private:
bool _Insert(Node*& root, const k& x)//Node*& root:使用指针引用,确保递归过程中对root的修改(如新建结点赋值)能同步到上层父结点的孩子指针;
        {
            if (root == nullptr)
            {
                root = new Node(x);
                return true;
            }

            if (root->_key < x)
                return _Insert(root->_right, x);
            else if (root->_key > x)
                return _Insert(root->_left, x);
            else
                return false;
        }
public:
	bool InsertR(const K& key)
		{
			return _Insert(_root, key);
		}
  • _Insert(私有递归函数):核心插入逻辑,接收当前子树根节点指针引用 + 待插入键值,递归查找插入位置;
  • InsertR(公有接口):对外暴露的插入接口,调用私有递归函数,传入整棵树的根节点_root。

4.2 二叉搜索树的查找

  1. 从根开始比较,查找xx比根的值大 则往右边走查找,x比根值小则往左边走查找。
  2. 最多查找高度次,走到到空,还没找到,这个值不存在。
  3. 如果不支持插入相等的值,找到x即可返回
  4. 如果支持插入相等的值,意味着有多个x存在,一般要求查找中序的第一个x。如下图,查找3,要找到1的右孩子的那个3返回。
cpp 复制代码
//查找
    bool Find(const k& key)
    {
        Node* cur=_root;
        while (cur)
        {
            if (key>cur->_key)
            {
                cur=cur->_right;
            }
            else if (key<cur->_key)
            {
                cur=cur->_left;
            }
            else
            {
                return true;
            }
        }
        return false;
    }

4.3 二叉搜索树的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

  1. 要删除结点N左右孩子均为空
  2. 要删除的结点N左孩子位空,右孩子结点不为空
  3. 要删除的结点N右孩子位空,左孩子结点不为空
  4. 要删除的结点N左右孩子结点均不为空

对应以上四种情况的解决方案:

  1. 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是一样的)

示例 :删除结点1(左右孩子均空)

复制代码
原树结构(简化):
      8
     / \
    3   10
   / \    \
  1   6    14
     / \   /
    4   7 13

处理流程:
1. 找到结点1的父结点(3);
2. 将父结点3的左孩子指针置空;
3. 删除结点1。

删除后结构:
      8
     / \
    3   10
     \    \
      6    14
     / \   /
    4   7 13
  1. 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点

示例 :删除结点10(左空、右孩子是14)

复制代码
原树结构(简化):
      8
     / \
    3   10
     \    \
      6    14
     / \   /
    4   7 13

处理流程:
 1. 找到结点10的父结点(8);
 2. 将父结点8的右孩子指针指向10的右孩子(14);
 3. 删除结点10。

删除后结构:
      8
     / \
    3   14
     \   /
      6 13
     / \
    4   7
  1. 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点

示例 :删除结点6(右空、左孩子是4)

复制代码
原树结构(简化):
      8
     / \
    3   10
   / \    \
  1   6    14
     /     /
    4     13

处理流程:
1. 找到结点6的父结点(3);
2. 将父结点3的右孩子指针指向6的左孩子(4);
3. 删除结点6。

删除后结构:
      8
     / \
    3   10
   / \    \
  1   4    14
           /
          13
  1. 无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意一个,放到N的位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。

示例 :删除根结点8(左右孩子均非空,以交换左树最大值为例)

复制代码
        8  (待删除结点N)
       / \
      3   10
     / \    \
    1   5    14
       / \   /
      4   7 13
         /
        6 
3 的右孩子是 5 → 5 的右孩子是 7 → 7 的右孩子为空,因此 R=7(左子树最大值结点)
仅交换值,结点的父子关系不变,交换后树结构变为:
        7  (原8的位置,值替换为7)
       / \
      3   10
     / \    \
    1   5    14
       / \   /
      4   8 13  (原7的位置,值替换为8)
         /
        6 
此时 R 结点(值 8)的特征:左孩子为 6、右孩子为空(符合 "右空、左非空" 的情况 3):
1. 找到 R 的父结点:5;
2. 将父结点 5 的右孩子指针指向 R 的左孩子 6;
3. 删除 R 结点(值 8)。
        7
       / \
      3   10
     / \    \
    1   5    14
       / \   /
      4   6 13

按照我们的逻辑给出的代码就是(有误):

cpp 复制代码
    bool Erase(const k& key)
    {
        Node* parent = nullptr;//记录cur父节点的
        Node* cur = _root;//临时节点 用于遍历的 寻找key值位置
        while (cur)
        {
            //当节点数据大的时候,转移到左子树,parent用于记录节点
            if (cur->_key > key)
            {
                parent=cur;
                cur=cur->_left;
            }
            //当节点数据小的时候,转移到右子树,parent用于记录节点
            else if (cur->_key < key)
            {
                parent=cur;
                cur=cur->_right;
            }
            else
            {
              //寻找成功开始删除
              //左为空
                if (cur->_left==nullptr)
                {   //排出
                    //看看cur在父亲节点的哪一边
                    if (parent->_left==cur)
                    {
                        parent->_left=cur->_right;
                    }
                    else
                    {
                        parent->_right=cur->_right;
                    }
                    delete cur;
                }
            //右为空
               else if (cur->_right==nullptr)
                  {   //排出
                    //看看cur在父亲节点的哪一边
                    if (parent->_left==cur)
                    {
                        parent->_left=cur->_left;
                    }
                    else
                    {
                        parent->_right=cur->_left;
                    }
                      delete cur;
                }
                else //左右都不为空,找子树中合适的节点代替 这里以替换左树最大值为例子
                {
                    Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
                    Node* maxleft=cur->_left;
                    while(maxleft->_right)
                    {
                        maxleftparent=maxleft;
                        maxleft=maxleft->_right;
                    }
                    //删除节点值于左树最大值 交换
                    swap(maxleft->_key, cur->_key);
                    //
                    maxleftparent->_right=maxleft->_left;
                    delete maxleft;
                }
            }
            return true;
        }
        return false;
    }

删除操作的易错点

但是这个代码有几个致命bug:

bug1:

cpp 复制代码
                    Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
                    Node* maxleft=cur->_left;
                    while(maxleft->_right)
                    {
                        maxleftparent=maxleft;
                        maxleft=maxleft->_right;
                    }
                    //删除节点值于左树最大值 交换
                    swap(maxleft->_key, cur->_key);
                    //
                    maxleftparent->_right=maxleft->_left;
                    delete maxleft;

用这个树 删除8 用交换右树最小值的方法 会出现以下情况:(这次以交换右边最小值为例子)

当我们 8和10交换后 我们minrightparent->_left=minright->_right 就出事了

所以我们要判断 minrightminrightparent的左树还是右树(右树就说明minrightparentcur

修改后代码:

我们换成以交换左树最大修改后代码:

cpp 复制代码
                    Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
                    Node* maxleft=cur->_left;
                    while(maxleft->_right)
                    {
                        maxleftparent=maxleft;
                        maxleft=maxleft->_right;
                    }
                    //删除节点值于左树最大值 交换
                    swap(maxleft->_key, cur->_key);
                    //判断maxleft节点是不是cur
                    if(maxleftparent->_right==maxleft)
                    maxleftparent->_right=maxleft->_left;
                    else
                        maxleftparent->_left=maxleft->_left;
                    delete maxleft;

bug2:

cpp 复制代码
                if (cur->_left==nullptr)
                {   //排出
                    //看看cur在父亲节点的哪一边
                    if (parent->_left==cur)
                    {
                        parent->_left=cur->_right;
                    }
                    else
                    {
                        parent->_right=cur->_right;
                    }
                    delete cur;
                }
            //右为空
               else if (cur->_right==nullptr)
                  {   //排出
                    //看看cur在父亲节点的哪一边
                    if (parent->_left==cur)
                    {
                        parent->_left=cur->_left;
                    }
                    else
                    {
                        parent->_right=cur->_left;
                    }
                      delete cur;
                }

很好我们解决了一个 其实还有一个容易忽略 当我们删除根节点时候

我们惊奇的发现 特喵根节点父亲节点为空啊 所以也得单独考虑

修改后:

cpp 复制代码
//寻找成功开始删除
              //左为空
                //root单独判断
                if (cur->_left==nullptr)
                {   //排出
                    //root单独判断
                    if(cur!=_root)
                    {
                        //看看cur在父亲节点的哪一边
                        if (parent->_left==cur)
                        {
                            parent->_left=cur->_right;
                        }
                        else
                        {
                            parent->_right=cur->_right;
                        }
                    }
                    else
                    {
                        //更改_root指向
                        _root=cur->_right;
                    }
                    delete cur;
                }
            //右为空
               else if (cur->_right==nullptr)
                  {   //排出
                      //root单独判断
                      if(cur!=_root)
                      {
                          //看看cur在父亲节点的哪一边
                          if (parent->_left==cur)
                          {
                              parent->_left=cur->_left;
                          }
                          else
                          {
                              parent->_right=cur->_left;
                          }
                      }
                      else
                      {
                          //更改_root指向
                          _root=cur->_left;
                      }
                      delete cur;
                }

删除操作正确代码

cpp 复制代码
    bool Erase(const k& key)
    {
        Node* parent = nullptr;//记录cur父节点的
        Node* cur = _root;//临时节点 用于遍历的 寻找key值位置
        while (cur)
        {
            //当节点数据大的时候,转移到左子树,parent用于记录节点
            if (cur->_key > key)
            {
                parent=cur;
                cur=cur->_left;
            }
            //当节点数据小的时候,转移到右子树,parent用于记录节点
            else if (cur->_key < key)
            {
                parent=cur;
                cur=cur->_right;
            }
            else
            {
              //寻找成功开始删除
              //左为空
                //root单独判断
                if (cur->_left==nullptr)
                {   //排出
                    //root单独判断
                    if(cur!=_root)
                    {
                        //看看cur在父亲节点的哪一边
                        if (parent->_left==cur)
                        {
                            parent->_left=cur->_right;
                        }
                        else
                        {
                            parent->_right=cur->_right;
                        }
                    }
                    else
                    {
                        //更改_root指向
                        _root=cur->_right;
                    }
                    delete cur;
                }
            //右为空
               else if (cur->_right==nullptr)
                  {   //排出
                      //root单独判断
                      if(cur!=_root)
                      {
                          //看看cur在父亲节点的哪一边
                          if (parent->_left==cur)
                          {
                              parent->_left=cur->_left;
                          }
                          else
                          {
                              parent->_right=cur->_left;
                          }
                      }
                      else
                      {
                          //更改_root指向
                          _root=cur->_left;
                      }
                      delete cur;
                }
                else //左右都不为空,找子树中合适的节点代替 这里以替换左树最大值为例子
                {
                    Node* maxleftparent=cur;//记录最大节点父亲节点 因为左子树最大节点 有一个左子树 需要删除
                    Node* maxleft=cur->_left;
                    while(maxleft->_right)
                    {
                        maxleftparent=maxleft;
                        maxleft=maxleft->_right;
                    }
                    //删除节点值于左树最大值 交换
                    swap(maxleft->_key, cur->_key);
                    //判断maxleft节点是不是cur
                    if(maxleftparent->_right==maxleft)
                    maxleftparent->_right=maxleft->_left;
                    else
                       maxleftparent->_left=maxleft->_left;
                    delete maxleft;
                }
            }
            return true;
        }
        return false;
    }

5. 二叉搜索树key和key/value使用场景

5.1 key搜索场景

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。

场景1: 小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。

场景2: 检查一篇英文文章单词拼写是否正确,将词库中所有单词放入二叉搜索树,读取文章中的单词,查找是否在二叉搜索树中,不在则波浪线标红提示。

5.2 key/value搜索场景:

每一个关键码key,都有与之对应的值valuevalue可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的valuekey/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树性质了,可以修改value

场景1: 简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。

场景2: 商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场,删除入场时间和车牌。

场景3: 统计一篇文章中单词出现的次数,读取一个单词,查找单词是否存在,不存在这个说明第一次出现,(单词加入树),单词存在,则++单词对应的次数。

5.3 key/value二叉搜索树代码实现

其实整体更改并不是很大

cpp 复制代码
namespace key_value
{

    template<class K, class V>
    struct BSTNode
    {
        K _key;
        V _value;

        // pair<K, V> _kv;

        BSTNode<K, V>* _left;
        BSTNode<K, V>* _right;

        BSTNode(const K& key, const V& value)
            :_key(key)
            ,_value(value)
            , _left(nullptr)
            , _right(nullptr)
        {}
    };

    // key
    template<class K, class V>
    class BSTree
    {
        typedef BSTNode<K, V> Node;
    public:
        bool Insert(const K& key, const V& value)
        {
            if (_root == nullptr)
            {
                _root = new Node(key, value);
                return true;
            }

            Node* parent = nullptr;
            Node* cur = _root;
            while (cur)
            {
                if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else
                {
                    return false;
                }
            }

            cur = new Node(key, value);
            if (parent->_key < key)
            {
                parent->_right = cur;
            }
            else
            {
                parent->_left = cur;
            }

            return true;
        }

        Node* Find(const K& key)//不能只查找在不在了 要返回查找节点对应的value
        {
            Node* 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 Erase(const K& key)
        {
            Node* parent = nullptr;
            Node* cur = _root;
            while (cur)
            {
                if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else
                {
                    // 准备删除
                    if (cur->_left == nullptr)
                    {
                        if (cur != _root)
                        {
                            // 左为空,父亲指向我的右
                            if (cur == parent->_left)
                            {
                                parent->_left = cur->_right;
                            }
                            else
                            {
                                parent->_right = cur->_right;
                            }
                        }
                        else
                        {
                            _root = cur->_right;
                        }

                        delete cur;
                    }
                    else if (cur->_right == nullptr)
                    {
                        if (cur != _root)
                        {
                            // 右为空,父亲指向我的左
                            if (cur == parent->_left)
                            {
                                parent->_left = cur->_left;
                            }
                            else
                            {
                                parent->_right = cur->_left;
                            }
                        }
                        else
                        {
                            _root = cur->_left;
                        }

                        delete cur;
                    }
                    else
                    {
                        // 左右都不为空,找子树中适合的节点替代我
                        Node* minRightParent = cur;
                        Node* minRight = cur->_right;
                        while (minRight->_left)
                        {
                            minRightParent = minRight;
                            minRight = minRight->_left;
                        }

                        swap(cur->_key, minRight->_key);

                        if (minRight == minRightParent->_left)
                            minRightParent->_left = minRight->_right;
                        else
                            minRightParent->_right = minRight->_right;

                        delete minRight;
                    }

                    return true;
                }
            }

            return false;
        }

        void InOrder()
        {
            _InOrder(_root);
            cout << endl;
        }

    private:
        void _InOrder(Node* root)
        {
            if (root == nullptr)
                return;

            _InOrder(root->_left);
            cout << root->_key << ":" << root->_value << endl;//遍历的时候 key 和 value都输出
            _InOrder(root->_right);
        }
    private:
        Node* _root = nullptr;
    };
}

例子1:实现一个小字典

效果如下:
例子2:查找水果出现次数

代码和效果展示:

相关推荐
仰泳的熊猫1 小时前
1061 Dating
数据结构·c++·算法·pat考试
CoderYanger1 小时前
A.每日一题——1523. 在区间范围内统计奇数数目
java·数据结构·算法·leetcode·职场和发展
程序员-周李斌1 小时前
ArrayBlockingQueue 源码解析
java·开发语言·后端·哈希算法·散列表
surtr12 小时前
Round 1019(div2) CD
数据结构·c++·算法·贪心算法·stl
Tim_102 小时前
【C++入门】02、C++程序初识
开发语言·c++
lkbhua莱克瓦242 小时前
项目知识——Next.js App Router体系
开发语言·javascript·项目知识
冰西瓜6002 小时前
分治(二)算法设计与分析 国科大
数据结构·算法
Cricyta Sevina2 小时前
Java 语言多线程核心概念全解析
java·开发语言