二叉搜索树--二叉搜索树概念,操作(查找插入删除 拷贝构造 赋值 析构),二叉搜索树的应用 k模型,kv模型,二叉搜索树的性能分析(留了一个问题,不论数据的插入的顺序如何,都会让数据成为二叉搜索树))

本文介绍了二叉搜索树的概念,包括其查找、插入和删除操作,并探讨了二叉搜索树在K模型和KV模型中的应用。此外,还分析了二叉搜索树的性能,提出一个问题:如何确保不论数据插入顺序,都能形成二叉搜索树。文章提到了旋转和深度一致性的解决方案,可能关联到红黑树的知识。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么会有二叉树进阶

1、二叉树在数据讲过了,但是又把二叉树提出来,是因为给二叉树又加了新的规则
2、map.set特性需要先铺垫二叉搜索树,二叉搜索树也是树形结构
3、对二叉搜索树有了了解,可以更好的了解map set的特性
4、二叉树部分面试题较难,前边讲的解题方法不容易理解,所以学习map set 二叉搜索树对解题有帮助

二叉搜索树

在这里插入图片描述

1、当然,在实际中也可以是左边大右边小,也是二叉搜索树。
2、如果是一个完全二叉树或者满二叉树,符合二叉搜索树的性质,那么它的查找复杂度就是一个log(n)

二叉搜索树操作

1、二叉搜索树的查找

在这里插入图片描述

//查找
   Node* find(const T& val)
   {
     int count = 0;//可以记录找到这个数总共查找了几次
     if(_root == nullptr)
     {
        return _root;
     }
     //根不为空,从根开始找,由于需要向下移动,所以定义一个cur
     Node* cur = _root;
     while(cur)
     {
        count++:
        if(cur->_data == val)
        {
           cout<<"count:"<<count<<endl;
           return cur;
        }
        else if(cur->_data > val)
        {
           cur = cur->left;
        }
        else
        {  
            cur = cur->right;
        }
     }
     //没有找到
      cout<<"count:"<<count<<endl;
      return nullptr;
     }
     
    

2、二叉搜索树的插入

在这里插入图片描述
1、二叉搜索树的插入只能插入到叶子结点
2、插入分为 找位置,然后插入
3、并且二叉搜索树不会放重复的数据
4、中序遍历是一个有序序列
5、二叉树插入的时候应该插入的是val值,然后用val构造结点,然后进行插入

//插入就是成功与否
bool Insert(const T& val)
{
   //根为空
   if(_root == nullptr)
   {
      _root = new Node(val);
      return true;
   }
   //不为空,则进行查找插入,
   //有查找,则需要cur
   Node* cur = _root;
   //因为要进行插入,所以要记录一下该点插入的上一个结点,也就是父节点parent
   Node* parent = nullptr;
   while(cur)
   {
      parent = cur;
      if(cur->_data == val)
      {return false;}
      else if(cur->_data > val)
      {
        cur = cur->_left;
      }
      else
      {
        cur = cur->_right;
      }
   }
   //将val值定义成Node,创建新节点,因为cur为空了,所以可以用来创建新节点
   cur = new Node(val);
   if(parent ->_data > val)
   {
      parent->_left = cur;
   }
   else
      parent->_right = cur;
   return true;
}

1、二叉树的插入其实就是在构造一颗二叉搜索树

3、二叉搜索树的删除

在这里插入图片描述
上图将abcd四种情况 结合三种情况,可以写条件是
1、左为空–处理右孩子情况
2、右为空–处理左孩子情况
3、左右都不为空
将左右孩子都为空放在1 或者2都可以处理 看徐婧航的代码 比较好
在这里插入图片描述
在这里插入图片描述
上图是当删除结点的时候,该结点有左右孩子的情况。删除左子树的最右结点的四种情况。

1、因为插入和删除 都是要先进行查找,在进行插入和删除的操作的
2、插入 查找的是一个合适的叶子结点位置,记录parent结点,根据val与父节点大小对比进行插入
3、删除 查找的是当前要删除的结点,记录parent结点,然后处理被删除结点 父亲与孩子之间的关系。
4、因为插入删除之间的查找 需要parent的更新,为了后续的操作,所以不能复用查找的代码
5、插入删除更新parent什么时候更新,要根据实际情况而定,parent插入可以在while下之后更新,但是删除必须在判断了之后传递的时候更新parent,防止parnet与cur指向相同位置,插入是可以让cur和parent指向相同的位置
6、删除的时候,一定要注意如果删除的是根节点,是没有parent的,需要_root直接接收被删除结点的左右孩子

//要进行删除 首先是先找到该结点,由于要进行删除操作,所以需要将parent记录下来
bool erase(const T& val)//找到值相同 直接delete结点就可以了
{
   if(_root == nullptr)
   {
     return false;
   }
   //先查找,定义一个cur,还进行删除操作,在定义一个parent
   Node* parent = nullptr;
   Node* cur = _root;
   //因为上边的查找的代码没有parent的更新,所以查找的代码不能够进行复用
 //需要重新写查找代码
   while(cur)
   {
      parent = cur;//parnet更新不能写在这,因为每次一这样,parent与cur指向相同的位置,则无法删除操作
      if(cur->_data == val)
      {
        break;
      }
      else if(cur->_data > val)
      {
         parent = cur;
         cur = cur->_left;
      }
      else
      {
         parent = cur;
         cur = cur->_right;
      }
   }
   //没有找到结点
   if(cur == nullptr)
   {
     return false;
   }
   //1、非叶子结点
        //a、左右孩子都有
        //b、有左孩子
        //c、有右孩子
   if(cur ->_left && cur->_right)//a、左右孩子都有
   {
    //该结点的左子树的最大值也就是向右找  或者  找该节点右子树的最小值也就是向左找 
    //我的方法是将cur位置保存了一下,然后用cur去找左子树的最右结点,用cur替换tmp,删除cur就可以了
    //老师的方法是重新定义一个mostRight结点,让cur待在删除的结点位置,然后mostRight去找,最后替换mostRight 和cur,删除mostRight 
     /* Node* tmp = cur;
      parent = nullptr;
      cur = cur->_left;
      while(cur->_right)
      {
         parent = cur;
         cur = cur ->_right;
      }
      tmp->_data = cur->_data;
      parent->_right = cur->_left;
      delete cur;*/ //我的想法  
      Node* mostRight = cur->_left;
      parent = cur;
      while(mostRight->_right)
      {
         parent = mostRight;
         mostRight = mostRight->_right;
      }
      cur->_data = mostRight->_data;
      //删除最右结点,如果mostRight在parent的左边,或者mostRight在parent的右边,都是有可能的,因为是去左子树找最右结点的。
      if(parent->_left == mostRight)
      {
        parent->_left = mostRight->_left;
      }
      else
        parent->_right = mostRight->_left;
      delete mostRight;
   }
   else if(cur->_left)//b、有左孩子
   {
      if(cur != _root)
      {
         if(parent->_left == cur)
         parent->_left = cur->_left;
         else
         parent ->_right = cur->_left;
      }
      else  
      {
         _root = cur->_left;
      }
      delete cur;
   }
   else if(cur->_right)//c、有右孩子
   {
      if(cur != _root)
      {
         if(parent->_left == cur)
         parent->_left = cur->_right;
         else
         parent ->_right = cur->_right;
      }
      else
      {
        _root = cur->_right;
      }
      delete cur;
   }
   2、叶子结点
   else
   {  
      if(cur != _root)
      {
        if(parent->_left == cur)
        parent->_left = nullptr;
        else
        parent ->_right = nullptr;
      }
      //根结点
      else
        _root = nullptr;
      delete cur;
   }
return true;
}

二叉搜索树的实现

//二叉搜索树首先要有一个结点,结点有数值 左右指针,所以reeee7e体
//由于结点里边的数据可以多种类型,所以泛型
template<class T>
struct BSTNode
{
  T _data;
  BSTNode<T>* _left;
  BSTNode<T>* _right;
  //构造一个结点
  BSTNode(const T& val = T())
  :_data(val)
  ,_left(nullptr)
  ,_right(nullptr)
  {}
}

//二叉搜索树 我们需要构建一棵树,然后对其进行操作
//实现左子树小于根,右子树大于根
template<class T>
class BSTree
{
public:
   //给树的结点取一个别名
   typedef BSTNode<T> Node;
   
   //1、查找在上边
   //2、插入在上边
   //3、删除在上边
   //为了验证一下这个二叉搜索树的中序遍历是一个递增序列,写一个中序遍历
//4、中序遍历就是一个动作,没有返回值
void inorder()//这里是因为中序遍历的时候要访问_root,但是_root是私有的,所以需要外一个接口,内也应该有一个接口。
{
  _inorder(_root);
  cout<<endl;
}
void _inorder(Node* root)//遍历应该是结点 不是BSTree
{
  if(root == nullptr)
  {
    return;
  }
  _inorder(root->_left);
  cout<<root->_data<<" ";
  _inorder(root->_right);
}
//8、构造函数
BSTree()
:_root(nullptr)
{}
//5、析构
//用递归进行析构二叉搜索树,先析构左子树再是右子树,然后根节点,由于析构从外界传值,又要访问_root。所以定义一个内接口
void destory(Node* root)
{
   destory(root->_left);
   destory(root->_right);
   delete root;
}
~BSTree()//析构函数不传参,但是要递归析构,可以考虑重写一个destory函数,进行递归析构
{
  destory(_root);  
  _root = nullptr;
}
//6、二叉树的拷贝构造---拷贝构造肯定要创建结点
Node* copyTree(Node* root)
{
    if(root)
    {
          Node* node = new Node(root->_data);
          node->left = copyTree(root->_left);
          node->_right = copyTree(root->_right);
          return node;
    }
    return nullptr;
}
BSTree(const BSTree<T>& bst)//
{
//因为在copyTree已经创建好树并且连接起来了,所以呢 最后只返回_root结点就可以了
     _root = copyTree(bst._root);
}
//7 二叉树的赋值运算
BSTree<T>& operator=(const BSTree<T>& bst)
{
      if(this != &bst)
      {
           destory(_root);
           _root = copyTree(bst._root);
      } 
      return *this;
}
//现代写法
BSTree<T>& operator=(const BSTree<T> bst)//这里要进行交换,如果传&的话,会让原来的bst树发生变化的,所以赋值传值。
{
    swap(_root,bst._root);
    return *this;
}

private:
  //给树定义一个_root的结点
  //再给一个缺省值,这样的话在构造_root的时候就不用调用结点的构造函数。
  Node* _root  = nullptr;
}


//插入随机数,然后中序遍历,在查找那个数字 
 void test
     {
       srand(time(nullptr));//让数更加随机
       int num;
       cout<<"i请输入一个num"<<endl;
       cin>> num;
       BSTree<int> b;
       for(int i = 0;i < num;i++)
       {
          b.insert(rand());
       }
       b.inorder();//将随机数打印出来
       cout<<"请输入一个查找的数字"<<endl;
       cin >> num;
       b.find(num);
     }

void test
{
  BSTree<int> b;
  b.insert(10);
  b.insert(5);
  b.insert(15);
  b.insert(3);
  b.insert(0);
  b.insert(2);
  b.insert(13);
  b.insert(17);
  b.inorder();
  b.inorder();
  b.erase(3);
  b.inorder();
  b.erase(10);
  b.inorder();
}

void test()
{
  BSTree<int> b;
  b.insert(10);
  b.insert(5);
  b.insert(15);
  b.insert(3);
  b.insert(0);
  b.insert(2);
  b.insert(13);
  b.insert(17);
  b.inorder();
  BSTree<int> copy(b);
  copy.inorder();
  BSTree<int> b2;
  b2 = b;
  b2.inorder();
}

1、二叉树的拷贝构造不好理解,代码也不太好想,看图
在这里插入图片描述
2、二叉搜索树的拷贝构造和析构一样,都是递归拷贝构造 递归析构的,所以都能重新写个函数实现递归的要求。
3、析构函数与构造函数必须成对出现。所以在上边方程写了析构函数,也就必须再写一个构造函数。

二叉搜索树的应用

1、K模型

在这里插入图片描述
1、二叉搜索树就是一个K的模型

2、KV模型

在这里插入图片描述
2、对于KV模型,是需要改变二叉搜索的内部一些东西的

template<class K,class V>
struct BSTNode
{
//数据的位置
  k _key;
  //数据
  v _data;
  BSTNode<K,V>* _left;
  BSTNode<K,V>* _right;
  //构造一个结点
  BSTNode(const K& key,const V& val)
  :_key(key)
  ,_data(val)
  ,_left(nullptr)
  ,_right(nullptr)
  {}
}

//二叉搜索树 我们需要构建一棵树,然后对其进行操作
//实现左子树小于根,右子树大于根
template<class K,class V>
class BSTree
{
public:
   //给树的结点取一个别名
   typedef BSTNode<K,V> Node;
   
   //1、查找
    Node* find(const K& key)
   {
     int count = 0;//可以记录找到这个数总共查找了几次
     if(_root == nullptr)
     {
        return _root;
     }
     //根不为空,从根开始找,由于需要向下移动,所以定义一个cur
     Node* cur = _root;
     while(cur)
     {
        count++:
        if(cur->_key == key)
        {
           cout<<"count:"<<count<<endl;
           return cur;
        }
        else if(cur->_key > key)
        {
           cur = cur->left;
        }
        else
        {  
            cur = cur->right;
        }
     }
     //没有找到
      cout<<"count:"<<count<<endl;
      return nullptr;
     }
   //2、插入
   bool Insert(const K& key,const V& data)
{
   //根为空
   if(_root == nullptr)
   {
      _root = new Node(key,data);
      return true;
   }
   //不为空,则进行查找插入,
   //有查找,则需要cur
   Node* cur = _root;
   //因为要进行插入,所以要记录一下该点插入的上一个结点,也就是父节点parent
   Node* parent = nullptr;
   while(cur)
   {
      parent = cur;
      if(cur->_key == key)
      {return false;}
      else if(cur->_key > key)
      {
        cur = cur->_left;
      }
      else
      {
        cur = cur->_right;
      }
   }
   //将val值定义成Node,创建新节点,因为cur为空了,所以可以用来创建新节点
   cur = new Node(key,data);
   if(parent ->_key > key)
   {
      parent->_left = cur;
   }
   else
      parent->_right = cur;
   return true;
}

   //3、删除
   bool erase(const K& key)//找到值相同 直接delete结点就可以了
{
   if(_root == nullptr)
   {
     return false;
   }
   //先查找,定义一个cur,还进行删除操作,在定义一个parent
   Node* parent = nullptr;
   Node* cur = _root;
   //因为上边的查找的代码没有parent的更新,所以查找的代码不能够进行复用
 //需要重新写查找代码
   while(cur)
   {
      parent = cur;//parnet更新不能写在这,因为每次一这样,parent与cur指向相同的位置,则无法删除操作
      if(cur->_key == key)
      {
        break;
      }
      else if(cur->_key > key)
      {
         parent = cur;
         cur = cur->_left;
      }
      else
      {
         parent = cur;
         cur = cur->_right;
      }
   }
   //没有找到结点
   if(cur == nullptr)
   {
     return false;
   }
   //1、非叶子结点
        //a、左右孩子都有
        //b、有左孩子
        //c、有右孩子
   if(cur ->_left && cur->_right)//a、左右孩子都有
   {
    //该结点的左子树的最大值也就是向右找  或者  找该节点右子树的最小值也就是向左找 
    //我的方法是将cur位置保存了一下,然后用cur去找左子树的最右结点,用cur替换tmp,删除cur就可以了
    //老师的方法是重新定义一个mostRight结点,让cur待在删除的结点位置,然后mostRight去找,最后替换mostRight 和cur,删除mostRight 
     /* Node* tmp = cur;
      parent = nullptr;
      cur = cur->_left;
      while(cur->_right)
      {
         parent = cur;
         cur = cur ->_right;
      }
      tmp->_data = cur->_data;
      parent->_right = cur->_left;
      delete cur;*/ //我的想法  
      Node* mostRight = cur->_left;
      parent = cur;
      while(mostRight->_right)
      {
         parent = mostRight;
         mostRight = mostRight->_right;
      }
      cur->_key = mostRight->_key;***~~//这块替换结点应该也要,老师还没有发现~~ ***
      cur->_data = mostRight->_data;
      //删除最右结点,如果mostRight在parent的左边,或者mostRight在parent的右边,都是有可能的,因为是去左子树找最右结点的。
      if(parent->_left == mostRight)
      {
        parent->_left = mostRight->_left;
      }
      else
        parent->_right = mostRight->_left;
      delete mostRight;
   }
   else if(cur->_left)//b、有左孩子
   {
      if(cur != _root)
      {
         if(parent->_left == cur)
         parent->_left = cur->_left;
         else
         parent ->_right = cur->_left;
      }
      else  
      {
         _root = cur->_left;
      }
      delete cur;
   }
   else if(cur->_right)//c、有右孩子
   {
      if(cur != _root)
      {
         if(parent->_left == cur)
         parent->_left = cur->_right;
         else
         parent ->_right = cur->_right;
      }
      else
      {
        _root = cur->_right;
      }
      delete cur;
   }
   2、叶子结点
   else
   {  
      if(cur != _root)
      {
        if(parent->_left == cur)
        parent->_left = nullptr;
        else
        parent ->_right = nullptr;
      }
      //根结点
      else
        _root = nullptr;
      delete cur;
   }
return true;
}
   //为了验证一下这个二叉搜索树的中序遍历是一个递增序列,写一个中序遍历
//4、中序遍历就是一个动作,没有返回值
void inorder()//这里是因为中序遍历的时候要访问_root,但是_root是私有的,所以需要外一个接口,内也应该有一个接口。
{
  _inorder(_root);
  cout<<endl;
}
void _inorder(Node* root)//遍历应该是结点 不是BSTree
{
  if(root == nullptr)
  {
    return;
  }
  _inorder(root->_left);
  cout<<root->_key<<" "<<root->_data<<" ";
  _inorder(root->_right);
}
//8、构造函数
BSTree()
:_root(nullptr)
{}
//5、析构
//用递归进行析构二叉搜索树,先析构左子树再是右子树,然后根节点,由于析构从外界传值,又要访问_root。所以定义一个内接口
void destory(Node* root)
{
   destory(root->_left);
   destory(root->_right);
   delete root;
}
~BSTree()//析构函数不传参,但是要递归析构,可以考虑重写一个destory函数,进行递归析构
{
  destory(_root);  
  _root = nullptr;
}
//6、二叉树的拷贝构造---拷贝构造肯定要创建结点
Node* copyTree(Node* root)
{
    if(root)
    {
          Node* node = new Node(root->_key,root->_data);
          node->left = copyTree(root->_left);
          node->_right = copyTree(root->_right);
          return node;
    }
    return nullptr;
}
BSTree(const BSTree<K,V>& bst)//
{
//因为在copyTree已经创建好树并且连接起来了,所以呢 最后只返回_root结点就可以了
     _root = copyTree(bst._root);
}
//7 二叉树的赋值运算
BSTree<K,V>& operator=(const BSTree<K,V>& bst)
{
      if(this != &bst)
      {
           destory(_root);
           _root = copyTree(bst._root);
      } 
      return *this;
}
//现代写法
BSTree<T>& operator=(const BSTree<T> bst)//这里要进行交换,如果传&的话,会让原来的bst树发生变化的,所以赋值传值。
{
    swap(_root,bst._root);
    return *this;
}


private:
  //给树定义一个_root的结点
  //再给一个缺省值,这样的话在构造_root的时候就不用调用结点的构造函数。
  Node* _root  = nullptr;
}

void test()
{
  BSTree<int,int> b;
  b.insert(10,10);
  b.insert(5,5);
  b.insert(15,15);
  b.insert(3,3);
  b.insert(0.0);
  b.insert(2.2);
  b.insert(13.13);
  b.insert(17,13);//key不能相同,只能data可以相同
   b.insert(17,17);//这个插入不进去,key不能相同
  b.inorder();
  
  BSTree<intint> copy(b);
  copy.inorder();
  BSTree<int,int> b2;
  b2 = b;
  b2.inorder();
}

二叉搜索树的性能分析

在这里插入图片描述
如何将单支树 进行改进 **不论按照什么次序进行插入,都可以是二叉搜索树呢?**下节课看
使用了旋转 深度一致相同的方式?应该是在红黑树那一块有解决方法。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值