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

前言

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

//使用c语言定义二叉树的节点
typedef int BinaryTreeValType;
typedef struct BinaryTreeNode
{
    BinaryTreeValType val;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
}BinaryTreeNode;

在多数的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;
};

到这里,我们基本已经搭建出了一个骨架。接下来我们就来实现这棵二叉树的增删查功能。好奇的同学可能会问,为什么没有改。因为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;
    }
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;
    }

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;
    }

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


3.<k,v>模型的二叉搜索树的增删查改

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

3.0 如何理解<K,V>

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

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

template<typename T1,typename T2>
struct pair
{ 
  pair(T1 t1,T2 t2)
    :first(t1)
    ,second(t2)
    {}
  T1 first;
  T2 second;
};

这里我们先不使用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;
};
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;
	  }

和前面不一样的地方是,我们这里返回的是节点的指针。
因为节点的指针可以用来修改对应的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;
	  }

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

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;
}

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


结语

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


  • 14
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值