DS进阶:二叉搜索树

简介: DS进阶:二叉搜索树

                                                创作不易,感谢三连!

一、二叉搜索树的概念

思考:

为什么二叉搜索树也叫做二叉查找树和二叉排序树呢??

1、 本身树形结构用来存储数据相比顺序表和链表来说并不占有优势,他的最大优势就在于查找优势当我们有一些数据经常需要进行搜索和查找时,只要按照上图的性质存放在树形结构里,那么我们可以通过这个规律在查找的时候将原本需要O(N)的时间复杂度变成O(logN)。

2、我们会发现只要按照上图的性质去存放数据,当我们对树形结构进行前序遍历的时候,恰好是升序的

二、二叉搜索树的实现

为了方便我们进行观察,我们在下图这个基础上去分析该具体如何实现!

2.1 基本的二叉树结构

template<class K>
struct BSTreeNode
{
  BSTreeNode<K>* _left;//左孩子
  BSTreeNode<K>* _right;//右孩子
  K _key;//存储的节点
  BSTreeNode(const K& key=K()) //创建一个新的节点
    :_left(nullptr)
    , _right(nullptr)
    , _key(key)
  {}
};
template<class K>
class BSTree
{
  typedef BSTreeNode<K> Node;
public:
private:
  Node* _root = nullptr;
};

2.2 二叉搜索树的插入(循环版)

插入的具体过程如下:

a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点(如下图)

//实现节点的插入
bool Insert(const K& key)
{
  //如果没有节点,就让其成为新的树
  if (_root == nullptr)
  {
    _root = new Node(key);
    return true;
  }
  //如果有节点  就用cur去遍历,如果小,就去左子树找,如果大,就去右子树找
  Node* parent = nullptr;
  Node* cur = _root;
  while (cur)
  {
    if (key < cur->_key)
    {
      parent = cur;
      cur = cur->_left;
    }
    else if (key > cur->_key)
    {
      parent = cur;
      cur = cur->_right;
    }
    else
    {
      return false;
    }
  }
  //循环结束,说明已经找到
  //进行连接
  //如果key较小,成为左子树,较大,则成为右子树
    if (key < parent->_key) parent->_left = new Node(key);
  else    parent->_right = new Node(key);
  return true;
}

2.3 二叉搜索树的查找(循环版)

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。

b、最多查找高度次,走到到空,还没找到,这个值不存在。

bool Find(const K& key)
{
  Node* cur = _root;
  while (cur)
  {
    if (key < cur->_key)  cur = cur->_left;
    else if (key > cur->_key) cur = cur->_right;
    else  return true;
  }
  return false;
}

2.4 二叉搜索树的删除(重难点)

    首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情

况:

a. 要删除的结点无孩子结点

b. 要删除的结点只有左孩子结点

c. 要删除的结点只有右孩子结点

d. 要删除的结点有左、右孩子结点

   看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程
如下:

情况b:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除(托孤)

情况c:删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点--直接删除(托孤)

情况d:在它的右子树中寻找最小节点(或者在左子树找最大节点),用它的值填补到被删除节点

中,再来处理该结点的删除问题--替换法删除(请保姆)

这样对吗??我们来分析极端情况

//删除节点
  bool Erase(const K& key)
  {
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
      if (key < cur->_key)
      {
        parent = cur;
        cur = cur->_left;
      }
      else if (key > cur->_key)
      {
        parent = cur;
        cur = cur->_right;
      }
      else //找到了,开始删除
      {
        //如果左为空,托孤
        if (cur->_left == nullptr)
        {
          if (cur == _root)
          {
            _root = cur->_right;
          }
          else
          {
            //如果cur和父亲的左子树一样
            if (parent->_left == cur)  parent->_left = cur->_right;
            else parent->_right = cur->_right;
          }
          delete cur;
        }
      
        //如果右为空,托孤
        else if (cur->_right == nullptr)
        {
          if (cur == _root)
          {
            _root = cur->_left;
          }
          else
          {
            if (parent->_left == cur) parent->_left = cur->_left;
            else parent->_right = cur->_left;
          }
          delete cur;
        }
        //左右都为空,请保姆
        else  //可以找右子树的最小节点(最左)替代,也可以找左子树的最大节点(最右)替代。
        { 
          Node* pminRight =cur;
          Node* minRight = cur->_right;
          while (minRight->_left)
          {
            pminRight = minRight;
            minRight = minRight->_left;
          }
          //此时已经找到了最左节点
          cur->_key = minRight->_key;//更新过去。然后删除节点
          //极端情况1:右子树没有自己的左子树
          if (pminRight->_left == minRight) pminRight->_left = minRight->_right;
          else   pminRight->_right = minRight->_right;
          delete minRight;
        }
        return true;
      }
    }
    return false;
  }

2.5 二叉搜索树的前序遍历

我们会发现如果我们这样写的话,必须要传_root进去,但是_root是私有成员,这个时候有两个方法,第一个就是写一个getroot函数,第二个方法就是再套一层

//防止外部用不了
  void _InOrder(Node* root)
  {
    if (root == nullptr)
return;
    _InOrder(root->_left);
    cout << root->_key << " ";
    _InOrder(root->_right);
  }
//前序遍历,可以排序
  void InOrder()
  {
    _InOrder(_root);
    cout << endl;
  }

2.6 二叉搜索树的查找(递归版)

bool _FindR(Node* root, const K& key)
{
  if (root == nullptr)
    return false;
  if (root->_key == key)
    return true;
  //小,到左边找,大到右边找
  if (key < root->_key) return _FindR(root->_left, key);
  else return _FindR(root->_right, key);
}
bool FindR(const K& key)
{
  return _FindR(_root, key);
}

2.7 二叉搜索树的插入(递归版)

    在非递归版本中,我们需要记录父节点,然后将目标节点和父节点进行连接,那递归要如何实现连接呢??

bool _InsertR(Node*& root, const K& key)
{ 
  if (root == nullptr)
  {
    root = new Node(key);
    return true;
  }
    //小到左边找,大到右边找,相等就是没必要插入
  if (key < root->_key) return _InsertR(root->_left,key)
  else if (key > root->_key) return _InsertR(root->_right,key)
  else return false;
}
bool InsertR(const K& key)
{
  return _InsertR(_root, key);
}

2.8  二叉搜索树的删除(递归版)

bool _EraseR(Node*&root,const K&key)
  {
    if (root == nullptr)
      return false;
    //小就到左子树去删,大就到右子树去删
    if (key < root->_key) return _EraseR(root->_left, key);
    else if (key > root->_key) return _EraseR(root->_right, key);
    else//此时找到了该点,开始删除
    {
      Node* del = root;//先记住要删除的结点
        //三种情况,删除结点只有一个左孩子/只有一个右孩子/有两个孩子
      if (root->_left == nullptr)  root = root->_right; //root是上一层递归root的右指针别名
      else if(root->_right == nullptr)  root = root->_left;
      else //最复杂的情况,有两个孩子
      {
        //找左孩子的最大结点来替换
        Node* maxleft = root->_left;
        while (maxleft->_right) maxleft = maxleft->_right;//不要直接走到空。此时的maxleft就是要替换的结点
        swap(maxleft->_key, root->_key);
        return _EraseR(root->_left, key);//转化成到左子树里去删key
      }
      return true;
    }
  }
  bool EraseR(const K& key)
  {
    return _EraseR(_root, key);
  }

2.9 二叉搜索树的构造和拷贝构造

BSTree() = default; // 制定强制生成默认构造(用缺省值去初始化)
//写拷贝构造必须要写默认构造
//拷贝构造
Node* Copy(Node* root)
{
  if (root == nullptr) return nullptr;
  Node* newroot = new Node(root->_key);
  newroot->_left = Copy(root->_left);
  newroot->_right = Copy(root->_right);
  return newroot;
}
BSTree(const BSTree<K>& t)
{
  _root = Copy(t._root);
}

2.10 二叉搜索树赋值(现代写法)

//赋值(现代写法)
  BSTree<K>& operator=(BSTree<K> t)
  {
    swap(_root, t._root);
    return *this;
  }

2.11 二叉搜索树的销毁

走一个后续遍历

//销毁
void Destroy(Node*& root)//引用时为了方便顺便给root置空
{
  if (root == nullptr)
    return;
  //后续遍历,删左 删右 再删中间
  Destroy(root->_left);
  Destroy(root->_left);
  delete root;
  root = nullptr;
}
~BSTree()
{
  Destroy(_root);
}

2.12 二叉搜索树实现的全部代码

要注意要将内部的封装递归的函数给他隐藏起来

template<class K>
struct BSTreeNode
{
  BSTreeNode<K>* _left;//左孩子
  BSTreeNode<K>* _right;//右孩子
  K _key;//存储的节点
  BSTreeNode(const K& key) //创建一个新的节点
    :_left(nullptr)
    , _right(nullptr)
    , _key(key)
  {}
};
//若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
//若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
template<class K>
class BSTree
{
  typedef BSTreeNode<K> Node;
public:
  BSTree() = default; // 制定强制生成默认构造
  //拷贝构造
  BSTree(const BSTree<K>& t)
  {
    _root = Copy(t._root);
  }
  //赋值(现代写法)
  BSTree<K>& operator=(BSTree<K> t)
  {
    swap(_root, t._root);
    return *this;
  }
  //销毁
  void Destroy(Node*& root)//引用时为了方便顺便给root置空
  {
    if (root == nullptr)
      return;
    //后续遍历,删左 删右 再删中间
    Destroy(root->_left);
    Destroy(root->_left);
    delete root;
    root = nullptr;
  }
  ~BSTree()
  {
    Destroy(_root);
  }
  //实现节点的插入(循环版本)
  bool Insert(const K& key)
  {
    //如果没有节点,就让其成为新的树
    if (_root == nullptr)
    {
      _root = new Node(key);
      return true;
    }
    //如果有节点  就用cur去遍历,如果小,就去左子树找,如果大,就去右子树找
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
      if (key < cur->_key)
      {
        parent = cur;
        cur = cur->_left;
      }
      else if (key > cur->_key)
      {
        parent = cur;
        cur = cur->_right;
      }
      else
      {
        return false;
      }
    }
    //循环结束,说明已经找到
    //进行连接
    //如果key较小,成为左子树,较大,则成为右子树
    if (key < parent->_key) parent->_left = new Node(key);
    else parent->_right = new Node(key);
    return true;
  }
  bool InsertR(const K& key)
  {
    return _InsertR(_root, key);
  }
  //删除节点(循环版本)
  bool Erase(const K& key)
  {
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
      if (key < cur->_key)
      {
        parent = cur;
        cur = cur->_left;
      }
      else if (key > cur->_key)
      {
        parent = cur;
        cur = cur->_right;
      }
      else //找到了,开始删除
      {
        //如果左为空,托孤
        if (cur->_left == nullptr)
        {
          if (cur == _root)
          {
            _root = cur->_right;
          }
          else
          {
            //如果cur和父亲的左子树一样
            if (parent->_left == cur)  parent->_left = cur->_right;
            else parent->_right = cur->_right;
          }
          delete cur;
        }
      
        //如果右为空,托孤
        else if (cur->_right == nullptr)
        {
          if (cur == _root)
          {
            _root = cur->_left;
          }
          else
          {
            if (parent->_left == cur) parent->_left = cur->_left;
            else parent->_right = cur->_left;
          }
          delete cur;
        }
        //左右都为空,请保姆
        else  //可以找右子树的最小节点(最左)替代,也可以找左子树的最大节点(最右)替代。
        { 
          Node* pminRight =cur;
          Node* minRight = cur->_right;
          while (minRight->_left)
          {
            pminRight = minRight;
            minRight = minRight->_left;
          }
          //此时已经找到了最左节点
          cur->_key = minRight->_key;//更新过去。然后删除节点
          //极端情况1:右子树没有自己的左子树
          if (pminRight->_left == minRight) pminRight->_left = minRight->_right;
          else   pminRight->_right = minRight->_right;
          delete minRight;
        }
        return true;
      }
    }
    return false;
  }
  bool EraseR(const K& key)
  {
    return _EraseR(_root, key);
  }
  //查找(循环版本)
  bool Find(const K& key)
  {
    Node* cur = _root;
    while (cur)
    {
      if (key < cur->_key)  cur = cur->_left;
      else if (key > cur->_key) cur = cur->_right;
      else  return true;
    }
    return false;
  }
  //查找(递归版本)
  bool FindR(const K& key)
  {
    return _FindR(_root, key);
  }
   //前序遍历,可以排序
  void InOrder()
  {
    _InOrder(_root);
    cout << endl;
  }
protected://递归用到的子函数放在这里封起来
  Node* Copy(Node* root)
  {
    if (root == nullptr) return nullptr;
    Node* newroot = new Node(root->_key);
    newroot->_left = Copy(root->_left);
    newroot->_right = Copy(root->_right);
    return newroot;
  }
  bool _InsertR(Node*& root, const K& key)
  {
    if (root == nullptr)
    {
      root = new Node(key);
      return true;
    }
    //小到左边找,大到右边找
    if (key < root->_key) return _InsertR(root->_left, key);
    else if (key > root->_key) return _InsertR(root->_right, key);
    else return false;
  }
  bool _EraseR(Node*& root, const K& key)
  {
    if (root == nullptr)
      return false;
    //小就到左子树去删,大就到右子树去删
    if (key < root->_key) return _EraseR(root->_left, key);
    else if (key > root->_key) return _EraseR(root->_right, key);
    else//此时找到了该点,开始删除
    {
      Node* del = root;//先记住要删除的结点
      //三种情况,删除结点只有一个左孩子/只有一个右孩子/有两个孩子
      if (root->_left == nullptr)  root = root->_right; //root是上一层递归root的右指针别名
      else if (root->_right == nullptr)  root = root->_left;
      else //最复杂的情况,有两个孩子
      {
        //找左孩子的最大结点来替换
        Node* maxleft = root->_left;
        while (maxleft->_right) maxleft = maxleft->_right;//不要直接走到空。此时的maxleft就是要替换的结点
        swap(maxleft->_key, root->_key);
        return _EraseR(root->_left, key);//转化成到左子树里去删key
      }
      return true;
    }
  }
  bool _FindR(Node* root, const K& key)
  {
    if (root == nullptr)
      return false;
    if (root->_key == key)
      return true;
    //小,到左边找,大到右边找
    if (key < root->_key) return _FindR(root->_left, key);
    else return _FindR(root->_right, key);
  }
  void _InOrder(Node* root)
  {
    if (root == nullptr)
      return;
    _InOrder(root->_left);
    cout << root->_key << " ";
    _InOrder(root->_right);
  }
private:
  Node* _root = nullptr;
};

三,二叉搜索树的应用

3.1 k模型

K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

举例:

(1)门禁系统

      最常见的就是我们平时的刷脸进校园,我们的信息被存储在树形结构中,刷脸的时候如果是学校的人就可以进去,如果是其他学校的人,系统找不到对应的数据,就不让进去。

(2)车库系统

     小区的车库会对出入的车辆进行管控,存储车牌信息,识别是否是小区业主的车辆,外人的车不能进入小区的停车场。再比如超市的地下停车场,超市的员工或者老板可以直接进入,但是如果是顾客的话,出车场时会显示你的停用时间,然后根据停用时间去计算你应该付多少停车费,当接收你已支付的信息时,才会让你出去。

(3)给一个单词word,判断单词的拼写是否争取

     以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

上述我们的实现就是实现的K模型

3.2 kv模型

    每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见

 

(1)中英互译词典

     比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;

(2)电话号码查快递信息

(3)统计高频单词

3.3 k模型改造成kv模型

只展示和K模型不一样的函数

namespace key_value
{
  template<class K,class V>
  struct BSTreeNode
  {
    BSTreeNode<K,V>* _left;//左孩子
    BSTreeNode<K,V>* _right;//右孩子
    K _key;//存储的节点
    V _value;//关键信息
    BSTreeNode(const K& key = K(),const V & value = V()) //创建一个新的节点
      :_left(nullptr)
      , _right(nullptr)
      , _key(key)
      , _value(value)
    {}
  };
  //若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  //若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  template<class K,class V>
  class BSTree
  {
    typedef BSTreeNode<K,V> Node;
  public:
    BSTree() = default; // 制定强制生成默认构造
    //拷贝构造
    BSTree(const BSTree<K>& t)
    {
      _root = Copy(t._root);
    }
    //赋值(现代写法)
    BSTree<K,V>& operator=(BSTree<K> t)
    {
      swap(_root, t._root);
      return *this;
    }
    //实现节点的插入(循环版本)
    bool Insert(const K& key,const V&value)
    {
      //如果没有节点,就让其成为新的树
      if (_root == nullptr)
      {
        _root = new Node(key,value);
        return true;
      }
      //如果有节点  就用cur去遍历,如果小,就去左子树找,如果大,就去右子树找
      Node* parent = nullptr;
      Node* cur = _root;
      while (cur)
      {
        if (key < cur->_key)
        {
          parent = cur;
          cur = cur->_left;
        }
        else if (key > cur->_key)
        {
          parent = cur;
          cur = cur->_right;
        }
        else
        {
          return false;
        }
      }
      //循环结束,说明已经找到
      //进行连接
      //如果key较小,成为左子树,较大,则成为右子树
      if (key < parent->_key) parent->_left = new Node(key,value);
      else parent->_right = new Node(key,value);
      return true;
    }
    //查找(循环版本)
    Node* Find(const K& key)
    {
      Node* cur = _root;
      while (cur)
      {
        if (key < cur->_key)  cur = cur->_left;
        else if (key > cur->_key) cur = cur->_right;
        else  return cur;
      }
      return nullptr;
    }
    //前序遍历,可以排序
    void InOrder()
    {
      _InOrder(_root);
    }
  protected://递归用到的子函数放在这里封起来
    Node* Copy(Node* root)
    {
      if (root == nullptr) return nullptr;
      Node* newroot = new Node(root->_key,root->_value);
      newroot->_left = Copy(root->_left);
      newroot->_right = Copy(root->_right);
      return newroot;
    }
    void _InOrder(Node* root)
    {
      if (root == nullptr)
        return;
      _InOrder(root->_left);
      cout << root->_key << ":" << root->_value << endl;
      _InOrder(root->_right);
    }
  private:
    Node* _root = nullptr;
  };
}

3.4 kv模型实现中英字典

void test4()
{
  key_value::BSTree<string,string> dict;
  dict.Insert("string", "字符串");
  dict.Insert("left", "左");
  dict.Insert("right", "右");
  dict.Insert("hello", "你好");
  dict.Insert("teacher", "老师");
  dict.Insert("student", "学生");
  string str;
  while (cin >> str)
  {
    key_value::BSTreeNode<string, string>* ret = dict.Find(str);
    if (ret) cout << ":" << ret->_value << endl;
    else  cout << "无此单词" << endl;
  }
  dict.InOrder();
}

ctrl+c是强制结束进程  ctrl+z是正常结束

3.5 kv模型实现统计高频单词

//统计高频单词
void test5()
{
  key_value::BSTree<string,int> countTree;
  string arr[] = { "hello","student","teacher","hello","goodbye","hello","height","mouth","day","day" };  
  for (auto str : arr)
  {
    key_value::BSTreeNode<string, int>* ret = countTree.Find(str);
    if (ret == nullptr)  countTree.Insert(str, 1);
    else  ++ret->_value;
  }
  countTree.InOrder();
}

四,二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

   对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

   但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:O(log2 N)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:O(N/2)

     如果退化成单支树,二叉搜索树的性能就失去了。所以我们还需要对二叉搜索树进行改进,就有了红黑树和AVL树。后续会介绍。

 

 

相关文章
|
7月前
|
存储 算法
【数据结构与算法】二叉树基础OJ--下(巩固提高)
【数据结构与算法】二叉树基础OJ--下(巩固提高)
|
7月前
|
存储 编译器
DS:单链表的实现
DS:单链表的实现
|
4月前
|
C++
A : DS哈希查找–链地址法
这篇文章介绍了如何使用链地址法解决哈希查找中的冲突问题,并通过C++代码示例演示了使用表头插入法构建哈希表并进行数据查找的过程。
|
7月前
|
存储 C++
【C++】二叉树进阶 -- 详解
【C++】二叉树进阶 -- 详解
|
7月前
|
存储 C++
C++进阶--AVL树
C++进阶--AVL树
|
7月前
|
C++
C++进阶--搜索二叉树
C++进阶--搜索二叉树
|
7月前
|
存储 算法
【数据结构与算法】二叉树基础OJ -- 上 (巩固提高)-2
【数据结构与算法】二叉树基础OJ -- 上 (巩固提高)-2
|
7月前
|
算法
【数据结构与算法】二叉树基础OJ -- 上 (巩固提高)-1
【数据结构与算法】二叉树基础OJ -- 上 (巩固提高)-1
|
7月前
|
存储 算法 C语言
DS:二叉树的顺序结构及堆的实现
DS:二叉树的顺序结构及堆的实现
【DS】实现二叉树的基本操作
【DS】实现二叉树的基本操作
132 0
【DS】实现二叉树的基本操作