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


    前言

    在正式介绍今天的二叉搜索树之前,我们要先复习先前阶段的有关于二叉树的知识。首先我们要知道。首先,首先,我们通常定义的二叉树都是链式结构。

    //使用c语言定义二叉树的节点
    typedef int BinaryTreeValType;
    typedef struct BinaryTreeNode
    {
        BinaryTreeValType val;
        struct BinaryTreeNode* left;
        struct BinaryTreeNode* right;
    }BinaryTreeNode;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在多数的oj题中,二叉树也大多是以上面这样的链式结构来定义的。回忆起二叉树的链式结构定义对于理解的正文内容是很有必要的!


    正文

      前言回顾完成,接下来就是我们正文的内容了。今天我们要来讲的是二叉搜索树。二叉搜索树是stl官方库里面的set和map容器使用的数据结构。不过这两个容器使用的二叉搜索树是相对比较复杂的红黑树。不过本质上还是二叉搜索树。 因为set和map容器常常是面试中会被考察到的知识点。所以我们就正式进入二叉搜索树的学习,简单了解二叉搜索树。

    二叉搜索树

    1.概念

    二叉搜索树,顾名思义就是一棵用来搜索的二叉树,而我们通常认为空树也是一棵二叉搜索树 而如果一棵非空的二叉搜索树满足那它必须要满足如下的几个条件

    1.对于任意一个节点,左子树都比它小
    2.对于任意一个节点,右子树都比它大

    满足这样结构的树我们就可以称之为二叉搜索树。而在理想状态下,二叉搜索树用来搜索的效率可以达到O(logn)是特别好的情况。 而在极端情况下,形成单边树的查找效率就变成了O(n),所以针对二叉搜索树,大佬们又进一步进行了优化—>avl树和红黑树。 不过这些都不是我们今天的重点,我们今天的重点是二叉搜索树。由于C语言的语法的限制,二叉搜索树我们旋转使用c++语言来描述。


    2. k模型的二叉搜索树的增删查

    首先,我们今天实现的二叉搜索树的两种模型是基于stl官方库中的两个容器:
    set和map 而K模型的二叉搜索树对应的就是set容器
    首先我们可以快速搭建一个结构出来:

    //在二叉搜索树里面,模板参数喜欢命名成K,K有--->key关键字的意思
    template<typename K>
    struct BinaryTreeNode
    {
        BinaryTreeNode<K>* _left;
        BinaryTreeNode<K>* _right;
         K _key;//实际应该使用const k,但是由于删除的需要,所以这里暂且是K
        BinaryTreeNode(const K& key)
            :_left(nullptr)
            ,_right(nullptr)
            ,_key(key)
        {}
    };
    //有了节点,快速搭建一个二叉树的结构
    template<typename K>
    class BSTree
    {
     	typedef BinaryTreeNode<K> Node;
    public:
       //提供默认构造
        BSTree()
          :_root(nullptr)
          {}
    private:
       Node* _root;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    到这里,我们基本已经搭建出了一个骨架。接下来我们就来实现这棵二叉树的增删查功能。好奇的同学可能会问,为什么没有改。因为k模型的二叉搜索树本身是按照key来进行查找的,如果随意修改key的话就会乱套!

    2.1 查找节点

    首先查找节点是非常容易的:直接上代码,不多解释:

    //查找函数
        bool Find(const K& key)
        {
            Node* cur = _root;
            while (cur)
            {  
                //当前的key小于待查找的key,去当前节点的右子树查找
                if (cur->_key < key)
                {
                    cur = cur->_right;
                }
                //当前的key大于待查找的key,去当前节点的左子树查找
                else if (cur->_key > key)
                {
                    cur = cur->_left;
                }
                else
                {
                    return true;
                }
            }
            return false;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    2.2. 增加节点

    增加节点和查找节点的思想类似,也是比较简单。不多做解释,都在代码里。

    //插入
        bool insert(const K& key)
        {  
            //空树特殊处理
            if (!_root)
            {
                _root = new Node(key);
                return true;
            }
           //查找对应的插入位置
            Node* cur = _root;
            Node* parent = nullptr;
            while (cur)
            {
                parent = cur;
                //走右子树
                if (cur->_key < key)
                {
                    cur = cur->_right;
                }
                //走左子树
                else if (cur->_key > key)
                {
                    cur = cur->_left;
                }
                //已经存在,防止重复插入
                else
                {
                    return false;
                }
            }
            //不存在可以插入
            Node* newnode = new Node(key);
            //插入到左子树
            if (parent->_key > key)
            {
                parent->_left = newnode;
                return true;
            }
            else
                parent->_right = newnode;
            return true;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    2.3.删除节点(较难)

    接下来,相对比较麻烦的就是二叉搜索树的删除了。因为对应的情况比较复杂。
    首先给定一棵二叉搜索树如下图:
    在这里插入图片描述

    情况1:待删除的节点没有左孩子。比如图中的节点8

    在这里插入图片描述

    情况2:待删除的节点没有右孩子:
    由于本张图片不好演示,所以寻找了1张新的搜索二叉树来演示

    在这里插入图片描述
    而上面两种情况有一种特例:如果根节点恰好就是只有一个孩子的节点,此时parent为空,所以对于根节点需要特殊处理。
    在这里插入图片描述

    情况3:待删除的节点拥有左右孩子

    在这里插入图片描述
    而对于删除叶子节点的情况,可以归类到只有左孩子和只有右孩子中的任意一种处理。所以完成的删除代码如下:

     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
                {
                    //情况1:左子树是空
                    if (cur->_left == nullptr)
                    {
                        //如果是根要特殊处理
                        if (cur == _root)
                        {
                            _root = cur->_right;
                        }
                        //这里处理了parent还是nullptr,空指针解引用!
    
                        else
                        {
                            if (cur == parent->_left)
                            {
                                parent->_left = cur->_right;
                            }
                            else
                            {
                                parent->_right = cur->_right;
                            }
                            delete cur;
                        }
                    }
                    //右子树是空树
                    else if (cur->_right == nullptr)
                    {   
                        assert(parent);
                        if (cur == _root)
                        {
                            _root = cur->_left;
                        }
                        if (cur == parent->_left)
                        {
                            parent->_left = cur->_left;
                        }
                        else
                        {
                            parent->_right = cur->_right;
                        }
                        delete cur;
                    }
                    //两个节点
                    else
                    {
                        Node* minParent = cur;
                        Node* minRight = cur->_right;
                        while (minRight->_left)
                        {
                            minParent = minRight;
                            minRight = minRight->_left;
                        }
                        //覆盖
                        cur->_key = minRight->_key;
                        if (minParent->_left == minRight)
                        {
                            minParent->_left = minRight->_right;
                        }
                        else
                        {
                            minParent->_right = minRight->_right;
                        }
                        delete minRight;
                    }
                    return true;
                }
            }
            //删除节点不在这里面
            return false;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    这就是k模型的二叉搜索树的增删查。接下来我们来看一看
    模型的二叉搜索树。


    3.模型的二叉搜索树的增删查改

    前面我们介绍了一个k模型的二叉搜索树,stl的set容器也是使用k模型的二叉搜索树。而标准库中还提供了一个map的容器,它所使用的是模型的二叉搜索树。我们接下来就看一看这个k,v模型是何方神圣。

    3.0 如何理解

    K模型:所谓的k模型就是key模型。对应的业务场景就是
    一个小区里面的所有住户的名字存入到二叉搜索树里。对应用户的姓名就是搜索的需要的结果。
    模型:这个模型就是一个关键字key对应的一个值val,对应的业务场景是小区业主的门牌号和对应的业主具有一 一对应关系。 通过对应的门牌号可以找到对应业主的名字

    而在c++里面,使用pair来构建键值对。

    template<typename T1,typename T2>
    struct pair
    { 
      pair(T1 t1,T2 t2)
        :first(t1)
        ,second(t2)
        {}
      T1 first;
      T2 second;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里我们先不使用pair,我们先把key和val先分开。
    和前面一样,我们先快速打一个框架

    template<class K,class V>
      struct BinaryTreeNode
      {
    	  BinaryTreeNode<K, V>* _left;
    	  BinaryTreeNode<K, V>* _right;
    	  K _key;
    	  V _val;
    	  BinaryTreeNode(const K& key,const V& val)
    		  : _left(nullptr)
    		  , _right(nullptr)
    		  , _key(key)
    		  , _val(val)
    	     {}
      };
    //二叉树的核心结构
    template<typename K,typename V>
    class BSTree
    { 
      typedef  BinaryTreeNode<K, V> Node;
     public:
       BSTree()
         :_root(nullptr)
       {}
     private:
       Node* _root;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    3.1 使用递归完成二叉搜索树的查

    接下来,我们先上手实现最简单的查找,使用递归查找的思想也比较简单。

    当前的key>查找的key,往左子树寻找,当前的key<查找的key,往右子树寻找
    所以最后写出的代码就是这样

    //递归版本查找--->类内用于递归的子函数
    	  Node* _FindR(Node* root,const K& key)
    	  {
    		  //空树找不到
    		  if (!root)
    		  {
    			  return nullptr;
    		  }
    		  //大于就找左子树
    		  else if (root->_key > key)
    		  {
    			  return _FindR(root->_left, key);
    		  }
    		  else if (root->_key < key)
    		  {
    			  return _FindR(root->_right, key);
    		  }
    		  else
    		  {
    			  return root;
    		  }
    		  //return true;
    	  }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    和前面不一样的地方是,我们这里返回的是节点的指针。
    因为节点的指针可以用来修改对应的val值,但是key同样是不可以被修改的!所以我们选择返回节点的指针而不是布尔值。

    3.2 使用递归完成二叉搜索树的增

    前面的查找是相对比较好理解,并且代码也不难。那么接下来我们来看一看插入节点。
    在这里插入图片描述
    但是这样就会有一个问题:newnode是一个局部变量,我们无法把这个newnode的局部变量连接到整个二叉树的正确位置。换言之,当递归到合适位置的时候,我们不能把newnode和它的父节点链接到一起!
    而解决的方案就是在使用引用传参!

    bool _InsertR(Node*& root, const K& key, const V& val)
    	  {
    		  if (!root)
    		  {
    			  root = new Node(key, val);
    			  return true;
    		  }
    		  else if (root->_key > key)
    		  {
    			  return _InsertR(root->_left, key, val);
    		  }
    		  else if (root->_key < key)
    		  {
    			  return _InsertR(root->_right, key, val);
    		  }
    		  return false;
    	  }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    接下来,结合代码和递归展开图来好好体会一下,引用带来的魅力。
    在这里插入图片描述
    在这里引用起到了画龙点睛的作用!

    3.3使用递归完成二叉搜索树的删(难)

    接下来,我们就开始使用递归进行删除二叉树的节点,和非递归的方式一样也是分成三种情况

    bool _EraseR(Node*& root,const K& key)
    	  {
    		  if (!root)
    		  {
    			  return false;
    		  }
    		  else if (root->_key > key)
    		  {
    			  return _EraseR(root->_left, key);
    		  }
    		  else if (root->_key < key)
    		  {
    			  return _EraseR(root->_right, key);
    		  }
    		  //找到了要删除
    		  else
    		  {  
    			  Node* del = root;
    			  //情况1:只有右子树
    			  if (root->_left == nullptr)
    			  {
    				  root = root->_right;
    			  }
    			  //情况2:只有左子树
    			  else if (root->_right == nullptr)
    			  {
    				  root = root->_left;
    			  }
    			  //情况3:左右都有,替代
    			  else
    			  {
    				  Node* minRight = root->_right;
    				  while (minRight->_left)
    				  {
    					  minRight = minRight->_left;
    				  }
    				  //交换
    				  swap(root->_key, minRight->_key);
    				  swap(root->_val, minRight->_val);
    				  return _EraseR(root->_right, key);
    			  }
    			  delete del;
    			  return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    实际上,递归起到的作用仅仅只是寻找。而真正精髓的地方就是删除两个节点的处理
    在这里插入图片描述


    结语

    二叉搜索树是一个十分重要的结构,但是我们今天只是实现的二叉搜索树还存在一个很致命的问题: 极端情况下,二叉搜索树会退化成单边树,此时二叉树就退化成了单链表或者是接近单链表! 而为了能够让树接近平衡,大佬们又发明了avl树和红黑树。这两棵树相对结构复杂,但都是建立在今天的基础上。如有错误之处,还望大佬之处。希望大家共同进步!


  • 相关阅读:
    归一化原来这么重要!深入浅出详解Transformer中的Normalization
    力扣(LeetCode)175. 组合两个表(2022.06.24)
    【Opencv】OpenCV使用CMake和MinGW的编译安装出错解决
    Goby 漏洞发布|用友 U8 Cloud ActionHandlerServlet 远程代码执行漏洞
    MySql常见复合查询(重点)
    Streaming Systems
    openjudge 1.13.4 垂直直方图
    VScode 调试python程序,debug状态闪断问题的解决方法
    嵌入式实时操作系统的设计与开发 (邮箱)
    K-means聚类是一种非常流行的聚类算法
  • 原文地址:https://blog.csdn.net/qq_56628506/article/details/126441250