【高阶数据结构】二叉树搜索树 {概念;实现:核心结构,增删查,默认成员函数;应用:K模型和KV模型;性能分析;相关练习}

二叉搜索树

一、二叉搜索树的概念

二叉搜索树又称二叉排序树,它可以是一棵空树,若果不为空则满足以下性质:

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树
  4. 二叉搜索树中不允许出现相同的键值

提示:二叉搜索树的附加功能是排序和去重。对于一个二叉搜索树,按照左子树-根节点-右子树的顺序进行中序遍历,得到的序列就是有序的。


二、二叉树搜索树的实现

2.1 核心结构

cpp 复制代码
template <class K>
struct BSTreeNode{ //搜索二叉树的节点
  typedef BSTreeNode<K> Node;
  Node *_left;
  Node *_right;
  K _key;

  BSTreeNode(const K &key = K())
    :_left(nullptr), //必须将指针初始化为nullptr,防止出现野指针问题。
    _right(nullptr),
    _key(key)
  {}
}; 

template <class K>
class BSTree{ //搜索二叉树
  typedef BSTreeNode<K> Node;
  Node *_root = nullptr;
    
public:
  BSTree() = default; //C++11的用法:使用了default关键字,表示使用编译器自动生成的默认构造函数
    
  void InOrder(){  //中序遍历多套一层是因为递归调用需要传递根节点指针,而根节点指针_root是private权限,在类外不能访问。
    _InOrder(_root);  
    cout << endl;
  }
    
  //......
};
template <class K>
  void BSTree<K>::_InOrder(Node *root){ //中序遍历
    if(root == nullptr) return;
    _InOrder(root->_left);
    cout << root->_key << " ";
    _InOrder(root->_right);
  }

2.2 查找

  1. 从根开始比较,如果key比根大则到右树去找;如果key比根小则到左树去找;
  2. 最多查找高度次。如果走到空节点还未找到,则说明这个键值不存在。
cpp 复制代码
//迭代法查找:
template <class K>
   bool BSTree<K>::Find(const K &key){
    //如果是空树,直接返回false;
    if(_root == nullptr) return false;
       
    Node *cur = _root;
    while(cur != nullptr)
    {
      if(key > cur->_key) //如果键值大,就到右树去找
      {
        cur = cur->_right;
      }
      else if(key < cur->_key) //如果键值小,就到左树去找
      {
        cur = cur->_left;
      }
      else{
        //找到返回true
        return true;
      }
    }
    //如果走到空节点还未找到,则说明这个键值不存在,返回false
    return false;
  }

//递归法查找:
template <class K>
  bool BSTree<K>::_rFind(Node *root, const K &key){
    if(root == nullptr) return false; //如果是空树或者走到空节点还未找到,返回false
    if(key > root->_key) return _rFind(root->_right, key); //如果键值大,就到右树去找
    else if(key < root->_key) return _rFind(root->_left, key); //如果键值小,就到左树去找
    else return true;//找到返回true
  }

2.3 插入

插入的具体过程如下:

  1. 树为空,则直接新增节点,赋值给root指针
  2. 树不为空,按二叉搜索树性质查找插入位置,插入新节点:
  3. 如果找到相同的键值,则不进行插入。
  4. 直到找到合适的空位置,才能进行插入操作。
cpp 复制代码
//迭代法插入:
template <class K>
bool BSTree<K>::Insert(const K &key){
    //树为空,直接进行插入:
    if(_root == nullptr)
    {
      _root = new Node(key);
      return true;
    }
    
    //树不为空,按二叉搜索树性质查找插入位置,插入新节点:
    Node *cur = _root;
    Node *parent = _root;
    while(cur != nullptr)
    {
      if(key > cur->_key) //如果键值大,就到右树去找
      {
        parent = cur; //移动cur指针前记录父节点指针parent,便于下一步插入操作的连接。
        cur = cur->_right;
      }
      else if(key < cur->_key) //如果键值小,就到左树去找
      {
        parent = cur;
        cur = cur->_left;
      }
      else{
        return false; //如果找到相同的键值,则插入失败。
      }
    }
    
    //直到找到合适的空位置,才能进行插入操作:
    if(key > parent->_key) //判断该位置是父节点的左节点还是右节点
    {
      parent->_right = new Node(key);
    }
    else{
      parent->_left = new Node(key);
    }
    return true;
}

//递归法插入:
/*传引用的好处:root是父节点内_left,_right指针的引用(对于空树就是_root的引用),修改root就是修改父节点的_left,_right指针。所以我们不需要再记录父节点的指针,也不需要再判断该位置是父节点的左节点还是右节点。*/
template <class K>
  bool BSTree<K>::_rInsert(Node* &root, const K &key){
    //如果树为空或者找到了合适的空位置,进行插入操作:
    if(root == nullptr)
    {
      root = new Node(key);
      return true;
    }
      
    if(key > root->_key)  //如果键值大,就到右树去插入
        return _rInsert(root->_right, key);
    else if(key < root->_key)  //如果键值小,就到左树去插入
        return _rInsert(root->_left, key);
    else  //如果找到相同的键值,则插入失败。
        return false;
  }

2.4 删除

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

  1. 要删除的结点是叶节点
  2. 要删除的结点只有左结点
  3. 要删除的结点只有右结点
  4. 要删除的结点有左、右结点

看起来待删除节点有4中情况,实际情况1可以与情况2或者3合并起来(使父节点指向nullptr),因此真正的删除过程如下:

  1. 使父结点指向被删除节点的左结点;然后删除该结点;--直接删除
  2. 使父结点指向被删除节点的右结点;然后删除该结点;--直接删除
  3. 找到被删除节点左树的最大值(最右)或右树的最小值(最左);将其key值与被删除节点的key值交换,保证搜索树的结构;最后删除该节点(最大或最小节点)--替换法删除
cpp 复制代码
//迭代法删除:
template <class K>
  bool BSTree<K>::Erase(const K &key){
    Node *cur = _root;
    Node *parent = _root; 
    while(cur != nullptr)
    {
      if(key > cur->_key)
      {
        parent = cur; //移动cur指针前记录父节点指针parent,便于下一步删除操作的连接。
        cur = cur->_right;
      }
      else if(key < cur->_key)
      {
        parent = cur;
        cur = cur->_left;
      }
      else{
        //情况1,2,3 直接删除
        if(cur->_left == nullptr || cur->_right == nullptr)
        {
          DelNode(parent, cur);
        }
        else{
          //情况4 替换法删除
          Node *lmaxp = cur; //如果cur->left就是左树的最大值,lmaxp应该指向cur
          Node *lmax = cur->_left; //lmax找左树的最大值,即左树的最右节点。
          while(lmax->_right != nullptr) 
          {
            lmaxp = lmax; //移动lmax指针前记录父节点指针lmaxp,便于下一步删除操作的连接。
            lmax = lmax->_right;
          } 
          swap(cur->_key, lmax->_key); //将其key值与被删除节点的key值交换,保证搜索树的结构
        
          //最后删除该节点。
          //注意:lmax指向最右节点,但该节点可能有左树;即使是叶节点,删除后也要接nullptr;
          //因此要调用DelNode删除前进行连接。替换法删除实际是将问题转化为情况1或2或3
          DelNode(lmaxp, lmax); 
        }
        return true; //完成删除马上返回
      }
    }
     //如果是空树,或者找不到要删除的元素,删除失败。
     return false; 
  }

//直接删除节点
template <class K>
  void BSTree<K>::DelNode(Node *parent, Node *cur){
    if(cur->_left == nullptr) //要删除的结点只有右结点(或者是叶节点);使其父结点指向其右结点(叶节点指向空);
    {
      if(cur == _root) //如果要删除的是根节点,需要特殊处理。
      {
        _root = cur->_right;
      }
      else if(parent->_left == cur) //判断要删除的结点是父节点的左节点还是右节点
      {
        parent->_left = cur->_right;
      }
      else{
        parent->_right = cur->_right;
      }
    }
    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;
  }

//递归法删除:
/*传引用的好处:root是父节点内_left,_right指针的引用,修改root就是修改父节点的_left,_right指针。所以我们不需要再记录父节点的指针,也不需要再判断该位置是父节点的左节点还是右节点。*/
template <class K>
  bool BSTree<K>::_rErase(Node* &root, const K &key){
    //如果是空树,或者找不到要删除的元素,删除失败。
    if(root == nullptr) return false;
      
    if(key > root->_key) 
        _rErase(root->_right, key);
    else if(key < root->_key) 
        _rErase(root->_left, key);
    else{
      Node *del = root; //记录要删除的节点
      //情况1,2,3 直接删除
      if(root->_left == nullptr)
      {
        root = root->_right;
      }
      else if(root->_right == nullptr)
      {
        root = root->_left; 
      }
      else{
        //情况4 替换法删除 
        Node *rmin = root->_right; //rmin找右树的最小值,即右树的最左节点。
        while(rmin->_left!=nullptr)
        {
          rmin = rmin->_left;
        }
        swap(rmin->_key, root->_key); //将其key值与被删除节点的key值交换,保证搜索树的结构
        
        //最后删除该节点。
        //注意:rmin指向最左节点,但该节点可能有右树;即使是叶节点,删除后也要接nullptr;
        //因此要递归调用_rErase删除前进行连接。替换法删除实际是将问题转化为情况1或2或3
        //交换后,已经不能从根节点开始找key了(key的位置已经不符合搜索树结构);
        //应该从右子树的根节点开始找(key的位置在右子树中仍符合搜索树结构);
        return _rErase(root->_right, key);
      }
      //释放节点
      delete del;
      return true;
    }
  }

2.5 默认成员函数

cpp 复制代码
template <class K>
class BSTree{
  typedef BSTreeNode<K> Node;
  Node *_root = nullptr;
public:
  BSTree() = default; //使用编译器自动生成的默认构造函数

  //多套一层是因为递归调用需要传递根节点指针,而根节点指针_root是private权限,在类外不能访问。析构同理。
  BSTree(const BSTree<K> &bst){
    _root = _Copy(bst._root);
  }

  BSTree<K>& operator=(BSTree<K> bst){ //复用拷贝构造
    swap(bst._root, _root);
    return *this;
  }
  
  ~BSTree(){
    _Destroy(_root);
  }
  
  //......
};

拷贝构造

cpp 复制代码
  template <class K>
  Node* BSTree<K>::_Copy(Node *root){
    if(root == nullptr) return nullptr;
    Node *copyroot = new Node(root->_key); //前序遍历拷贝,保证两棵树的结构顺序完全相同
    copyroot->_left = _Copy(root->_left);
    copyroot->_right = _Copy(root->_right);
    return copyroot;
  }

析构

cpp 复制代码
  template <class K>
  void BSTree<K>::_Destroy(Node *root){
    if(root == nullptr) return;
    _Destroy(root->_left); //析构必须采用后序遍历
    _Destroy(root->_right);
    delete root;
  } 

三、二叉搜索树的应用

3.1 K模型

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

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:(示例代码:wordchecker.cc

  1. 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  2. 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
cpp 复制代码
//wordchecker.cc
#include "BSTree_K.hpp"
#include <iostream>
#include <string>
using namespace std;

int main(){
  string tmp[] = {"search", "word", "vector", "string", "dictionary", "list", "binary"};
  BSTree<string> lib;
  for(string &e : tmp)
  {
    lib.Insert(e);
  }
  lib.InOrder();
  string input;
  while(cin >> input)
  {
    bool ret = lib.Find(input);
    if(ret)
    {
      cout << "拼写正确!" << endl;
    }
    else{
      cout << "拼写错误!" << endl;
    }
  }
  return 0;
}

3.2 KV模型

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。

KV模型中key为关键码,二叉搜索树构建过程中的比较仍以key值为准。value只是一个对应值,附加值。

该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;(示例代码:dictionary.cc
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。(示例代码:wordcounter.cc)

将二叉搜索树改造成KV模型:

cpp 复制代码
//将二叉搜索树改造成KV模型
//只有少量改动,不变的内容省略不写。
#include <iostream>
using namespace std;

template <class K, class V>
struct BSTreeNode{
  typedef BSTreeNode<K, V> Node;
  Node *_left;
  Node *_right;
  K _key;
  V _val; //加了一个value

  BSTreeNode(const K &key = K(), const V &val = V())
    :_left(nullptr),
    _right(nullptr),
    _key(key),
    _val(val)
  {}
}; 

template <class K, class V>
class BSTree{
  typedef BSTreeNode<K, V> Node;
  Node *_root = nullptr;
    
public:
  bool Insert(const K &key, const V &val);
  
  Node* Find(const K &key);

  //......
  
private:
  Node* _Copy(Node *root);
     
  //......
};

template <class K, class V>
bool BSTree<K,V>::Insert(const K &key, const V &val){
    if(_root == nullptr)
    {
      _root = new Node(key, val); //插入时要初始化value
      return true;
    }
    //......
    if(key > parent->_key)
    {
      parent->_right = new Node(key, val);
    }
    else{
      parent->_left = new Node(key, val);
    }
    return true;
}
  
template <class K, class V>
    BSTreeNode<K,V>* BSTree<K,V>::Find(const K &key){
    if(_root == nullptr) return nullptr;
    Node *cur = _root;
    while(cur != nullptr)
    {
      //......
      else{
        return cur; //找到返回节点指针,便于查看和修改value
      }
    }
    return nullptr;
  }
  
template <class K, class V>
  BSTreeNode<K,V>* BSTree<K,V>::_Copy(Node *root){
    if(root == nullptr) return nullptr;
    Node *copyroot = new Node(root->_key, root->_val);  //拷贝也要复制value
    //......
  }

template <class K, class V>
  void BSTree<K,V>::_InOrder(Node *root){
    if(root == nullptr) return;
    _InOrder(root->_left);
    cout << root->_key << "-->" << root->_val << endl;
    _InOrder(root->_right);
  }

KV模型的应用示例:

cpp 复制代码
//dictionary.cc
#include "BSTree_KV.hpp"
#include <iostream>
#include <string>
using namespace std;

int main(){
  BSTree<string, string> dct;
  dct.Insert("buffer", "缓冲器");
  dct.Insert("error", "错误");
  dct.Insert("derive", "继承自,来自");
  dct.Insert("soldier", "士兵");
  dct.Insert("column", "列");
  dct.Insert("row", "行");

  dct.InOrder();

  string input;
  while(cin >> input){
    BSTreeNode<string,string> *ret = dct.Find(input);
    if(ret != nullptr)
    {
      cout << ret->_val << endl;
    }
    else{
      cout << "词库中无此单词" << endl;
    }
  }
}

//wordcounter.cc
int main(){
  string tmp[] = {"orange", "orange","orange","strawberry", "pear","apple", "apple","apple","apple","peach","peach"};
  BSTree<string, int> counter;
  for(string &e : tmp)
  {
    BSTreeNode<string, int> *ret = counter.Find(e);
    if(ret)
    {
      ++ret->_val;
    }
    else{
      counter.Insert(e, 1);
    }
  }
  counter.InOrder();
  return 0;
}

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

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

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找次数是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。时间复杂度:O(h),h是树的深度。

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

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

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

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


五、相关练习

这些题目更适合使用C++完成,难度也更大一些

  1. 二叉树创建字符串。OJ链接
  2. 二叉树的分层遍历1。OJ链接
  3. 二叉树的分层遍历2。OJ链接
  4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。OJ链接
  5. 二叉树搜索树转换成排序双向链表。OJ链接
  6. 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ链接
  7. 根据一棵树的中序遍历与后序遍历构造二叉树。OJ链接
  8. 二叉树的前序遍历,非递归迭代实现 。OJ链接
  9. 二叉树中序遍历 ,非递归迭代实现。OJ链接
  10. 二叉树的后序遍历 ,非递归迭代实现。OJ链接
相关推荐
林开落L8 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
Prejudices21 分钟前
C++如何调用Python脚本
开发语言·c++·python
单音GG24 分钟前
推荐一个基于协程的C++(lua)游戏服务器
服务器·c++·游戏·lua
qing_04060340 分钟前
C++——多态
开发语言·c++·多态
孙同学_40 分钟前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
charlie1145141911 小时前
Qt Event事件系统小探2
c++·qt·拖放·事件系统
iiiiiankor1 小时前
C/C++内存管理 | new的机制 | 重载自己的operator new
java·c语言·c++
小辛学西嘎嘎1 小时前
C/C++精品项目之图床共享云存储(3):网络缓冲区类和main
c语言·开发语言·c++
韭菜盖饭1 小时前
LeetCode每日一题3261---统计满足 K 约束的子字符串数量 II
数据结构·算法·leetcode
♡喜欢做梦2 小时前
【数据结构】ArrayList与LinkedList详解!!!——Java
java·开发语言·数据结构·链表