【高阶数据结构】二叉搜索树的插入、删除和查找(精美图解+完整代码)

🤡博客主页:醉竺

🥰本文专栏:《高阶数据结构》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨ 


目录

1. 二叉查找树的概念和性质

2. 二叉查找树的查找

2.1 递归代码实现

2.2 非递归代码实现

2.3 查找时间复杂度分析 

3. 二叉查找树的插入

3.1 递归代码实现 

3.2 非递归代码实现 

4. 二叉查找树的删除 (难点) 

(1) 被删除节点左子树为空

(2) 被删除节点右子树为空

(3) 被删除节点左右子树均不空

(4) 上述情况具体图解示例 

4.1 递归代码实现

4.2 非递归代码实现

5. 完整代码提供和运行结果 

5. 二叉查找树的其它操作(了解)

6. 二叉查找树的实际应用


1. 二叉搜索树的概念和性质

二叉查找树(BinarySearchTree,BST),又称为二叉查找树、二叉排序树,是一种对查找和排序都有用的特殊二叉树。存在的意义在于实现快速查找,同时,它也支持快速插入和删除。它是怎么做到这些的呢?

这些都依赖于二叉查找树的特殊结构。二叉搜索树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于该节点的值,而右子树中每个节点的值都大于该节点的值。 当然,左、右子树本身也是一棵二叉查找树。

下面几个树就是二叉搜索树,你一看应该就清楚了。

二叉搜索树的特性:左子树 < 根 < 右子树,即如果对二叉搜索树进行中序遍历,得到的结果就是一个有序的递增序列,也就是说内部存储的数据是已经排好序的,所以它也叫做二叉排序树(Binary Sort Tree)。
上图中的二叉搜索树按中序遍历序列,第一棵为“3,4,5,6,9,11”,第二棵为“8,11,12,17,19,23”,第三棵为“8,10 ,13,15,22”。   

二叉搜索树的性质可以总结如下。

二叉搜索树或是空树,或是满足如下性质的二叉树:

1)若其左子树非空,则左子树上所有节点的值均小于根节点的值。 

2)若其右子树非空,则右子树上所有节点的值均大于根节点的值。

3)其左右子树本身又各是一棵二叉查找树。 

下面,先看一看二叉搜索树的类模板定义代码,分为每个节点的定义,以及二叉搜索树的定义两个部分。  为后续相关操作做铺垫。

//树中每个节点的定义
template<class K> //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;private:Node* _root = nullptr;public:// 默认构造BSTree() = default;~BSTree(){Destroy(_root);}// 拷贝构造BSTree(const BSTree<K>& t){_root = Copy(t._root);}// t1 = t3BSTree<K>& operator=(BSTree<K> t){swap(_root, t._root);return *this;}void InOrder(){_InOrder(_root);cout << endl;}private: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;}void Destroy(Node*& root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}//二叉树中序遍历代码(排序),方便测试时显示节点数据void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}
};

2. 二叉搜索树的查找

因为二叉搜索树的中序遍历有序性,所以查找和二分查找类似,每次缩小查找范围,查找的效率较高。

图解查找步骤: 

例如,一棵二叉搜索树,如图 2-1 所示,查找关键字32。

 1)32 与二叉搜索树的树根 25 比较,32 > 25,则在右子树中查找,如图 2-2 所示。

2)32 与右子树的树根 69 比较,32<69,则在左子树中查找,如图 2-3 所示。

3)32 与左子树的树根 32 比较,相等,查找成功,返回该节点指针,如图 2-4 所示。 

2.1 递归代码实现

算法步骤:

1)若二叉搜索树为空,查找失败,返回空指针。

2)若二叉搜索树非空,将 key 与根节点的关键字 root->_key 比较:

•  若key == root->_key,查找成功,返回根节点指针;

•  若key > root->_key,则递归查找右子树。

•  若key < root->_key,则递归查找左子树; 

bool FindR(const K& key)
{return _FindR(_root, key);
}bool _FindR(Node* root, const K& key)
{if (root == nullptr){return false;}if (key > root->_key){return _FindR(root->_right, key);}else if (key < root->_key){return _FindR(root->_left, key);}else{return true;}
}

2.2 非递归代码实现

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

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

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

2.3 查找时间复杂度分析 

我们前面说过,二叉搜索树的意义在于实现快速查找。无论对二叉搜索树做何种操作,首先把进行操作的节点找到才是最重要的。因此,这里的时间复杂度分析主要针对的是节点的查找操作。

先说 查找长度。在查找操作中,需要对比的节点次数就是查找长度,它反映了查找操作的时间复杂度。

图7的左侧是一棵满二叉树,如果要查找50这个节点,则需要分别与60、40、50这三个节点做对比,这意味着50这个节点的查找长度为3。而图7右侧这棵失衡的二叉树(斜树),要查找50这个节点,则需要分别与90、80、70、60、50这5个节点做对比,这意味着50这个节点的查找长度为5。

我们再引申到 平均查找长度ASL(Average Search Length)。它可以用来衡量整个二叉查找树的查找效率。

  • 上图 左侧图,查找节点60,查找长度为1,如果查找40、80这两个节点,查找长度为2,如果查找30、50、70、90这四个节点,查找长度为3。又因为图中有7个节点,所以所有节点的平均查找长度ASL = (1*1 + 2*2 + 3*4)/ 7 = 2.42。

  • 上图 右侧图,同理,ASL = (1*1 + 2*1 + 3*1 +4*1 + 5*1 + 6*1 + 7*1)/ 7 = 4。

可以看到,虽然图中2棵二叉查找树存储的数据相同,但 左侧的查找效率显然更高

刚刚是查找节点成功时的平均查找长度,那么查找节点失败时的平均查找长度该如何计算呢?我们将图中的二叉树变为扩展二叉树。

可以看到,如果查找失败,则最终的查找位置会停留在带有#标记的扩展节点上。

  • 图8左侧图,带有#标记的扩展节点一共是8个,也就是说查找节点时需要对比的3次节点值的情形是8种。所以查找节点失败时的平均查找长度ASL = (3*8)/ 8 = 3。

  • 图8右侧图,带有#标记的扩展节点一共是8个,同理,查找节点时需要对比1次节点值的情形是1种,需要对比2次节点值的情形是1种,以此类推。所以查找节点失败时的平均查找长度ASL = (1*1+2*1+3*1+4*1+5*1+6*1+7*2)/8 = 4.375。

显然,即便是查找节点失败时的平均查找长度,图7左侧二叉查找树的查找效率也是更高的。

不难看出, 查找长度与树的高度是成正比的,也就是说,二叉查找树的查找效率主要取决于树的高度。在查找操作中,需要对比的节点次数一定不会超过该树的高度。  

  • 如果是一棵满二叉树或者完全二叉树,那么根据二叉树的性质五,该二叉树的高度为\left \lfloor log{2}^{n} \right \rfloor+1。换句话说,对于有n个节点的二叉树,它的最小高度是\left \lfloor log{2}^{n} \right \rfloor+1,这意味着查找操作最好情况时间复杂度为O(log{2}^{n})(n代表该二叉树的节点数量)。
  • 如果一棵二叉树的高度和节点数相同,也就是一棵斜树,其高度为n,这意味着查找操作最坏情况时间复杂度为O(n), 看起来已经是一个链表了。

那么为了提高查找效率,应该尽可能地让二叉查找树的高度变得最小(尽可能接近\left \lfloor log{2}^{n} \right \rfloor+1)。也就是说,在创建二叉查找树时,应该尽可能让该二叉查找树保持左右节点的平衡,从而引出平衡二叉树的概念。所谓平衡二叉树,就是该树上任意节点的左子树和右子树深度之差不超过1。后续文章会讲解平衡二叉树。

总之有:

二叉查找树的查找时间复杂度和树的形态有关,分为最好情况、最坏情况和平均情况分析。

•  最好:二叉查找树的形态和二分查找的判定树相似,时间复杂度为O(logn)

•  最坏:二叉排序查找树的形态为单支树,退化为顺序查找,时间复杂度为O(n)

•  平均: n个节点的二叉查找树有n!棵(有的形态相同),平均情况下,时间复杂度为O(logn)


3. 二叉搜索树的插入

因为二叉搜索树的中序遍历有序性,首先要查找待插入关键字的插入位置,当查找不成功时,将待插入关键字作为新的叶子节点插入到最后一个查找节点的左孩子或右孩子。

算法步骤:

1)若二叉搜索树为空,则直接新增节点,赋值给root指针作为根节点,数据域为key,左右子树均为空。

2)若二叉搜索树非空,按二叉搜索数性质查找插入位置,插入新节点。即将key与根节点的关键字root->_key比较:

•  若key > root->_key,则将key 插入右子树;

•  若key < root->_key,则将key 插入左子树。

图解步骤: 

例如,一棵二叉搜索树,如下图所示,插入关键字30。 

1)30与树根25比较,30>25,则在25的右子树中查找,如图3-1所示。

2)30与右子树的树根69比较,30<69,则在69的左子树中查找,如图3-2所示。 

3)30与左子树的树根32比较,30<32,则在32的左子树中查找,如图3-3所示。

4)32的左子树为空,则将30作为新的叶子节点,插入32的左子树,如图3-4所示。 

3.1 递归代码实现 

bool InsertR(const K& key)
{return _InsertR(_root, key);
}bool _InsertR(Node*& root, const K& key) // 注意第一个参数类型
{if (root == nullptr){root = new Node(key);return true;}if (key > root->_key)return _InsertR(root->_right, key);else if (key < root->_key)return _InsertR(root->_left, key);elsereturn false;
}

3.2 非递归代码实现 

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

4. 二叉搜索树的删除 (难点) 

二叉搜索树的删除操作相对要更复杂一些,针对所要删除的节点的子节点个数不同,有几种情况需要处理。

首先要在二叉查找树中找到待删除的节点,然后执行删除操作。假设指针p 指向待删除节点,指针f 指向 p 的双亲节点。根据待删除节点所在位置的不同,删除操作处理方法也不同,可分为3种情况:

(1) 被删除节点左子树为空

        如果被删除节点左子树为空,则令其右子树子承父业代替其位置即可。例如,在二叉查找树中删除P节点,如图4-1所示。

(2) 被删除节点右子树为空

        如果被删除节点右子树为空,则令其左子树子承父业代替其位置即可,如图4-2所示。

(3) 被删除节点左右子树均不空

        如果被删除节点的左子树和右子树均不空,则没办法再使用子承父业的方法了。根据二叉查找树的中序有序性,删除该节点时,可以用其直接前驱(或直接后继)的值替换到要删除的节点上,然后再删除其直接前驱(或直接后继)即可。

那么中序遍历序列中,一个节点的直接前驱(或直接后继)是哪个节点呢?  

        直接前驱:中序遍历中,节点 p 的直接前驱为其左子树的最右节点。即沿着 p 的左子树一直访问其右子树,直到没有右子树,就找到了最右节点,如图4-3(a) 所示。s 指向 p 的直接前驱,q 指向 s 的双亲。

        直接后继:中序遍历中,节点 p 的直接后继为其右子树的最左节点,如图4-3(b) 所示。s 指向p 的直接后继,q 指向s 的双亲。 

        以 p 的直接前驱 s 代替 p 为例,相当于令 s 节点的数据赋值给 p 节点,即 s 代替 p。然后删除 s 节点即可,因为 s 为最右节点,它没有右子树,删除后,左子树子承父业代替 s,如图4-4 所示。 

        例如,在二叉搜索树中删除 24。首先查找到 24 的位置 p,然后找到 p 的直接前驱 s(22)节点,令 22 赋值给 p 的数据域,删除 s 节点,删除过程如图4-5 所示。 

        删除节点之后是不是仍然满足二叉查找树的中序遍历有序性?
        需要注意的是,有一种特殊情况,即 p 的左孩子没有右子树,s 就是其左子树的最右节点(直接前驱),即 s 代替 p,然后删除 s 节点即可,因为 s 为最右节点没有右子树,删除后,左子树子承父业代替 s,如图4-6 所示。 

例如,在二叉搜索树中删除20,删除过程如图4-7 所示 


二叉查找树算法步骤:  

(4) 上述情况具体图解示例 

下面是上述情况具体图解例子:
情况(0),要删除的节点左右子树为空 :(情况0上述我并没有单独列出,因为此情况可以并入左子树为空或者右子树的情况内。因为无论用被删除节点的左子树还是右子树“子承父业”,都是空nullptr,因此可以并入上述(1)或者(2),不影响最后结果。)

情况(1)要删除的节点的左子树为空:

        在二叉搜索树中删除 32,首先查找到 32 所在的位置,判断其左子树为空,则令其右子树子承父业代替其位置,删除过程如图4-8 所示。 

情况(2)要删除的节点的右子树为空:

         在二叉搜索树中删除 69,首先查找到 69 所在的位置,判断其右子树为空,则令其左子树子承父业代替其位置,删除过程如图4-9 所示。

情况(3)要删除的节点的左右子树均不空:

        在二叉搜索树中删除 25,首先查找到 25 所在的位置,判断其左右子树均不空,则令其直接前驱(左子树最右节点 20)代替之,再删除其直接前驱 20 即可。删除 20 时,其左子树子承父业,删除过程如图4-10所示。 

4.1 递归代码实现

bool EraseR(const K& key)
{return _EraseR(_root, key);
}bool _EraseR(Node*& root, const K& key) //注意第一个参数类型
{if (root == nullptr)return false;if (key > root->_key){return _EraseR(root->_right, key);}else if (key < root->_key){return _EraseR(root->_left, key);}else // 找到了节点,执行删除操作:{// 即将被删除节点的左孩子为空 (或者即将被删除节点的左孩子和右孩子都为空)if (root->_left == nullptr){Node* del = root;root = root->_right;delete del;return true;}else if (root->_right == nullptr) //即将被删除节点的右孩子为空 (或者即将被删除节点的左孩子和右孩子都为空){Node* del = root;root = root->_left;delete del;return true;}else // 即将被删除节点的左右孩子都不为空{Node* subLeft = root->_right;while (subLeft->_left){subLeft = subLeft->_left; // 被删除节点的右子树的最左节点(最小节点)}swap(root->_key, subLeft->_key); // 转换成在子树去递归删除return _EraseR(root->_right, key);}}
}

4.2 非递归代码实现

bool Erase(const K& key)
{Node* parent = nullptr;Node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{// 准备删除if (cur->_left == nullptr){//左为空if (cur == _root){_root = cur->_right;}else{if (cur == parent->_left){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr){//右为空if (cur == _root){_root = cur->_left;}else{if (cur == parent->_left){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}delete cur;}else{//左右都不为空// 右树的最小节点(最左节点)Node* parent = cur;Node* subLeft = cur->_right;while (subLeft->_left){parent = subLeft;subLeft = subLeft->_left;}swap(cur->_key, subLeft->_key);if (subLeft == parent->_left)parent->_left = subLeft->_right;elseparent->_right = subLeft->_right;delete subLeft;}return true;}}return false;
}

5. 完整代码提供和运行结果 

这里提供完整的代码实现,可以复制粘贴到编译器上运行调试:

BinarySearchTree.h 

#pragma once
#include<iostream>
using namespace std;// 树中每个节点的定义
template<class K> // 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;private:Node* _root = nullptr;public:// 默认构造BSTree() = default;~BSTree(){Destroy(_root);}BSTree(const BSTree<K>& t){_root = Copy(t._root);}// t1 = t3BSTree<K>& operator=(BSTree<K> t){swap(_root, t._root);return *this;}private:void Destroy(Node*& root) // 加引用是为了最后能够让真正的实参根节点也能置空。不加引用也可以,但是"root = nullptr;"这句代码就无效了。{if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}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;}public:// 二叉查找树的查找(非递归实现)bool Find(const K& key){Node* cur = _root;while (cur){if (key > cur->_key){cur = cur->_right;}else if (key < cur->_key){cur = cur->_left;}else{return true;}}return false;}// 二叉查找树的插入(非递归实现)bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (key > cur->_key){cur = cur->_right;}else if (key < cur->_key){cur = cur->_left;}else{return false;}}cur = new Node(key);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}// 二叉查找树的删除(非递归实现)bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{// 准备删除if (cur->_left == nullptr){//左为空if (cur == _root){_root = cur->_right;}else{if (cur == parent->_left){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr){//右为空if (cur == _root){_root = cur->_left;}else{if (cur == parent->_left){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}delete cur;}else{//左右都不为空// 右树的最小节点(最左节点)Node* parent = cur;Node* subLeft = cur->_right;while (subLeft->_left){parent = subLeft;subLeft = subLeft->_left;}swap(cur->_key, subLeft->_key);if (subLeft == parent->_left)parent->_left = subLeft->_right;elseparent->_right = subLeft->_right;delete subLeft;}return true;}}return false;}public:// 二叉查找树的中序遍历(排序)递归实现,方便测试时显示节点数据void InOrder(){_InOrder(_root);cout << endl;}// 二叉查找树的查找(递归实现)bool FindR(const K& key){return _FindR(_root, key);}// 二叉查找树的插入(递归实现)bool InsertR(const K& key){return _InsertR(_root, key);}// 二叉查找树的删除(递归实现)bool EraseR(const K& key){return _EraseR(_root, key);}private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}bool _FindR(Node* root, const K& key){if (root == nullptr){return false;}if (key > root->_key){return _FindR(root->_right, key);}else if (key < root->_key){return _FindR(root->_left, key);}else{return true;}}bool _InsertR(Node*& root, const K& key){if (root == nullptr){root = new Node(key);return true;}if (key > root->_key)return _InsertR(root->_right, key);else if (key < root->_key)return _InsertR(root->_left, key);elsereturn false;}bool _EraseR(Node*& root, const K& key){if (root == nullptr)return false;if (key > root->_key){return _EraseR(root->_right, key);}else if (key < root->_key){return _EraseR(root->_left, key);}else{// 删除if (root->_left == nullptr){Node* del = root;root = root->_right;delete del;return true;}else if (root->_right == nullptr){Node* del = root;root = root->_left;delete del;return true;}else{Node* subLeft = root->_right;while (subLeft->_left){subLeft = subLeft->_left;}swap(root->_key, subLeft->_key);// 转换成在子树去递归删除return _EraseR(root->_right, key);}}}
};

Test_BinarySearchTree.h  

#include"BinarySearchTree_K.h"// 测试非递归的主要操作
void test()
{int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> bt;for (auto e : a){bt.Insert(e);}bt.InOrder();cout << bt.Find(6) << endl; // true(1)cout << bt.Find(666) << endl; // false(0)bt.Erase(14);bt.InOrder();bt.Erase(3);bt.InOrder();bt.Erase(8);bt.InOrder();
}// 测试递归的主要操作
void testR()
{int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> bt;for (auto e : a){bt.InsertR(e);}bt.InOrder();cout << bt.FindR(6) << endl; // true(1)cout << bt.FindR(666) << endl; // false(0)bt.EraseR(14);bt.InOrder();bt.EraseR(3);bt.InOrder();bt.EraseR(8);bt.InOrder();
}// 测试拷贝构造和赋值运算符重载
void testBase()
{int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> bt;for (auto e : a){bt.InsertR(e);}bt.InOrder();BSTree<int> cp1(bt);cp1.InOrder();BSTree<int> cp2;cp2 = bt;cp2.InOrder();
}int main()
{test();cout << "----------------------------" << endl;testR();cout << "----------------------------" << endl;testBase();return 0;
}

运行结果:


5. 二叉搜索树的其它操作(了解)

接下来,我再为你补充一些二叉搜索树的其他常用操作。

  • 查找值最大 最小的节点

//查找值最大节点
Node* SearchMaxValuePoint()
{return SearchMaxValuePoint(root);
}Node* SearchMaxValuePoint(Node* tNode)
{if (tNode == nullptr) //空树return nullptr;//从根节点开始往右侧找即可Node* tmpnode = tNode;while (tmpnode->_right != nullptr)tmpnode = tmpnode->_right;return tmpnode;
}//查找值最小节点
Node* SearchMinValuePoint()
{return SearchMinValuePoint(root);
}Node* SearchMinValuePoint(Node* tNode)
{if (tNode == nullptr) //空树return nullptr;//从根节点开始往左侧找即可Node* tmpnode = tNode;while (tmpnode->_left != nullptr)tmpnode = tmpnode->_left;return tmpnode;
}
  • 找出中序遍历序列中当前节点的前趋和后继节点

解决这个问题的方法有很多,书写的程序代码也各不相同。如果每个节点要有一个指向父节点的指针,那么解决起来可能更容易一些,如果没有指向父节点的指针,那么一般就要从根节点开始找起。

但不管怎样,一定要把握住两个原则。

  1. 当前节点的前趋节点一定是比当前节点值小的,也是再往前的一系列节点中最大的。

  2. 当前节点的后继节点一定是比当前节点值大的,也是再往后的一系列节点中节点值最小的。

//找按中序遍历的二叉查找树中当前节点的前趋节点
Node* GetPriorPoint_IO(Node* findnode)
{if (findnode == nullptr)return nullptr;Node* prevnode = nullptr;Node* currnode = root;  //当前节点,从根开始找while (currnode != nullptr){if (currnode->_key < findnode->_key) //当前节点小{//(1)从一系列比当前要找的值小的节点中找一个值最大的当前趋节点//当前节点值比要找的  节点值小,所以当前节点认为有可能是前趋if (prevnode == nullptr){//如果前趋节点还为空,那不防把当前节点认为就是前趋prevnode = currnode;}else //prevnode不为空{//既然是找前趋,那自然是找到比要找的值小的 一系列节点中 值最大的if (prevnode->_key < currnode->_key){prevnode = currnode; //前趋自然是找一堆 比当前值小的 值中 最大的一个。}}//(2)继续逼近要找的节点,一直到找到要找的节点,找到要找的节点后,要找的节点的左节点仍旧可能是前趋currnode = currnode->_right;  //当前节点小,所以往当前节点的右子树转}else if (currnode->_key > findnode->_key) //当前节点值比要找的值大,所以当前节点肯定不会是要找的值的前趋{//当前节点大,所以往当前节点的左子树转currnode = currnode->_left;}else //(currnode->_key == findnode->_key) ,这个else其实可以和上个else合并,但为了清晰,就不合并了{//当前节点值  就是要找的节点值,那么 前趋也可能在当前节点的左子树中,所以往左子树转继续找看有没有更合适的前趋currnode = currnode->_left;}} //end whilereturn prevnode;
}
//找按中序遍历的二叉查找树中当前节点的后继节点
Node* GetNextPoint_IO(Node* findnode)
{if (findnode == nullptr)return nullptr;Node* nextnode = nullptr;Node* currnode = root;  //当前节点,从根开始找while (currnode != nullptr){if (currnode->_key > findnode->_key) //当前节点大{//(1)从一系列比当前要找的值大的节点中找一个值最小的当后继节点//当前节点值比要找的  节点值大,所以当前节点认为有可能是后继if (nextnode == nullptr){//如果后继节点还为空,那不防把当前节点认为就是后继nextnode = currnode;}else //nextnode不为空{//既然是找后继,那自然是找到比要找的值大的 一系列节点中 值最小的if (nextnode->_key > currnode->_key){nextnode = currnode; //后继自然是找一堆 比当前值大的 值中 最小的一个。}}//(2)继续逼近要找的节点,一直到找到要找的节点,找到要找的节点后,要找的节点的右节点仍旧可能是后继currnode = currnode->_left;  //当前节点大,所以往当前节点的左子树转}else if (currnode->_key < findnode->_key) //当前节点值比要找的值小,所以当前节点肯定不会是要找的值的后继{//当前节点小,所以往当前节点的右子树转currnode = currnode->_right;}else //(currnode->_key == findnode->_key) {//当前节点值  就是要找的节点值,那么 后继也可能在当前节点的右子树中,所以往右子树转继续找看有没有更合适的后继currnode = currnode->_right;}} //end whilereturn nextnode;
}

6. 二叉搜索树的实际应用

1. K模型K模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到的值。 上述所有操作就是K模型。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

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

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

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

这里提供一下KV模型的代码仅供学习参考,跟上述K模型基本一样,只是模板多了一个参数V。

BianrySearchTree_KV.h 

#pragma once
#include<iostream>
using namespace std;// 树中每个节点的定义
template<class K, class V> // K V代表数据元素类型
struct BSTreeNode
{BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;K _key;V _value;BSTreeNode(const K& key, const V& value):_left(nullptr), _right(nullptr), _key(key), _value(value){}
};template<class K, class V>
class BSTree
{typedef BSTreeNode<K, V> Node;private:Node* _root = nullptr;public:// 默认构造BSTree() = default;~BSTree(){Destroy(_root);}BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}// t1 = t3BSTree<K, V>& operator=(BSTree<K, V> t){swap(_root, t._root);return *this;}private:void Destroy(Node*& root) // 加引用是为了最后能够让真正的实参根节点也能置空。不加引用也可以,但是"root = nullptr;"这句代码就无效了。{if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}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;}public:// 二叉查找树的中序遍历(排序)递归实现,方便测试时显示节点数据void InOrder(){_InOrder(_root);cout << endl;}// 二叉查找树的查找(递归实现)Node* FindR(const K& key){return _FindR(_root, key);}// 二叉查找树的插入(递归实现)bool InsertR(const K& key, const V& value){return _InsertR(_root, key, value);}// 二叉查找树的删除(递归实现)bool EraseR(const K& key){return _EraseR(_root, key);}private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}Node* _FindR(Node* root, const K& key){if (root == nullptr){return nullptr;}if (key > root->_key){return _FindR(root->_right, key);}else if (key < root->_key){return _FindR(root->_left, key);}else{return root;}}bool _InsertR(Node*& root, const K& key, const V& value){if (root == nullptr){root = new Node(key, value);return true;}if (key > root->_key)return _InsertR(root->_right, key, value);else if (key < root->_key)return _InsertR(root->_left, key, value);elsereturn false;}bool _EraseR(Node*& root, const K& key){if (root == nullptr)return false;if (key > root->_key){return _EraseR(root->_right, key);}else if (key < root->_key){return _EraseR(root->_left, key);}else{// 删除if (root->_left == nullptr){Node* del = root;root = root->_right;delete del;return true;}else if (root->_right == nullptr){Node* del = root;root = root->_left;delete del;return true;}else{Node* subLeft = root->_right;while (subLeft->_left){subLeft = subLeft->_left;}swap(root->_key, subLeft->_key);// 转换成在子树去递归删除return _EraseR(root->_right, key);}}}
};

Test_BianrySearchTree_KV.h  

#include"BinarySearchTree_KV.h"// 测试词典
void testDict()
{BSTree<string, string> dict;dict.InsertR("sort", "排序");dict.InsertR("left", "左边");dict.InsertR("right", "右边");dict.InsertR("insert", "插入");dict.InsertR("key", "关键词");string str;while (cin >> str){BSTreeNode<string, string>* ret = dict.FindR(str);if (ret){cout << ret->_value << endl;}else{cout << "没有此关建词!" << endl;}}
}// 测试统计次数
void testCount()
{string arr[] = { "苹果", "栗子", "苹果", "苹果", "栗子", "木瓜", "荔枝", "葡萄", "木瓜", "西瓜", "桃子", "橘子", "西瓜" };BSTree<string, int> countTree;for (auto& e : arr){BSTreeNode<string, int>* ret = countTree.FindR(e);if (ret == nullptr){countTree.InsertR(e, 1);}else{ret->_value++;}}countTree.InOrder();
}int main()
{// testDict();testCount();return 0;
}

运行结果: 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/427688.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mysql梳理6——order by排序

目录 6 order by排序 6.1 排序数据 6.2 单列排序 6.3 多行排列 6 order by排序 6.1 排序数据 使用ORDER BY字句排序 ASC&#xff08;ascend&#xff09;:升序DESC(descend):降序 ORDER BY子句在SELECT语句的结尾 6.2 单列排序 如果没有使用排序操作&#xff0c;默认…

一、桥式整流电路

桥式整流电路 1、二极管的单向导电性: 伏安特性曲线: 理想开关模型和恒压降模型 2、桥式整流电流流向过程 输入输出波形: 3、计算:Vo,lo,二极管反向电压。 学习心得

十三,Spring Boot 中注入 Servlet,Filter,Listener

十三&#xff0c;Spring Boot 中注入 Servlet&#xff0c;Filter&#xff0c;Listener 文章目录 十三&#xff0c;Spring Boot 中注入 Servlet&#xff0c;Filter&#xff0c;Listener1. 基本介绍2. 第一种方式&#xff1a;使用注解方式注入&#xff1a;Servlet&#xff0c;Fil…

【C++】——多态详解

目录 1、什么是多态&#xff1f; 2、多态的定义及实现 2.1多态的构成条件 ​2.2多态语法细节处理 2.3协变 2.4析构函数的重写 2.5C11 override 和 final关键字 2.6重载—重写—隐藏的对比分析 3、纯虚函数和抽象类 4、多态的原理分析 4.1多态是如何实现的 4.2虚函数…

OpenCV 2

目录 图像平滑处理 高斯与中值滤波 图像阈值 ​编辑 Canny边缘检测 非极大值抑制 边缘检测效果 轮廓检测方法 ​编辑 ​编辑​编辑 轮廓检测结果 轮廓特征与近似 图像平滑处理 以上两种出来的图片效果 以上的效果&#xff0c;因为填的是normalize False&#xff0c;越界…

零基础到项目实战:Node.js版Selenium WebDriver教程

在当今数字化时代&#xff0c;Web应用程序的质量和性能至关重要。为了确保这些应用的可靠性&#xff0c;自动化测试成为一种不可或缺的工具。Selenium&#xff0c;作为自动化测试领域的瑰宝&#xff0c;为我们提供了无限可能。本教程将深入介绍Selenium&#xff0c;以及如何结合…

硬盘数据恢复必备:4 款强大硬盘数据恢复软件推荐!

在数字化的时代&#xff0c;我们的生活和工作越来越离不开电脑&#xff0c;而硬盘作为重要的数据存储设备&#xff0c;一旦出现数据丢失的情况&#xff0c;往往会给我们带来极大的困扰。别担心&#xff0c;今天就为大家推荐四款强大的硬盘数据恢复软件&#xff0c;帮助你轻松找…

优化算法(三)—模拟退火算法(附MATLAB程序)

模拟退火算法&#xff08;Simulated Annealing, SA&#xff09;是一种基于概率的优化算法&#xff0c;旨在寻找全局最优解。该算法模拟金属退火过程中的物质冷却过程&#xff0c;逐渐降低系统的“温度”以达到全局优化的效果。它特别适用于解决复杂的组合优化问题。 一、模拟退…

[羊城杯 2020]Blackcat1

知识点&#xff1a;数组加密绕过 进入页面熟悉的web三部曲&#xff08;url地址&#xff0c;web源代码&#xff0c;web目录扫描&#xff09; url地址没有什么东西去看看源代码. 这有一个mp3文件点一下看看. 在最后面发现了 PHP源码. if(empty($_POST[Black-Cat-Sheriff]) || em…

Android Studio报错: Could not find pub.devrel:easypermissions:0.3.0, 改用linux编译

在Android studio中去编译开源的仓库&#xff0c;大概率就是各种编译不过&#xff0c;一堆错误&#xff0c;一顿改错&#xff0c;基本上会耗费非常多时间&#xff0c;比如&#xff1a; 这个就是改gradle版本&#xff0c;改成7.2 &#xff0c;修改完成之后&#xff0c;还有其他报…

翻页时钟 2.0-自动置顶显示,点击小时切换显示标题栏不显示标题栏-供大家学习研究参考

更新内容 自动置顶显示点击小时切换显示标题栏&#xff0c;&#xff08;显示标题栏后可移动时钟位置&#xff0c;鼠标拖动边框调整时钟大小&#xff09;不显示标题栏时&#xff0c;透明部分光标可穿透修正一个显示bu 下载地址&#xff1a; https://download.csdn.net/download…

iPhone 16系列:熟悉的味道,全新的体验

来看看iPhone 16和Plus这两个新成员&#xff0c;实话说&#xff0c;它们和之前曝光的样子几乎完全一致。下面我们就一起来细数一下这次的几大变化吧。 外观设计&#xff1a;焕然一新 首先&#xff0c;最显眼的变化就是后置镜头模组的布局调整为了垂直排列。这一改变使得整个背…

小程序开发设计-第一个小程序:安装开发者工具③

上篇文章导航&#xff1a; 小程序开发设计-第一个小程序&#xff1a;注册小程序开发账号②-CSDN博客https://blog.csdn.net/qq_60872637/article/details/142219035?spm1001.2014.3001.5501 须知&#xff1a;不同版本选项有所不同&#xff0c;并无大碍。 第一个小程序&#…

《黑神话悟空》开发框架与战斗系统解析

本文主要围绕《黑神话悟空》的开发框架与战斗系统解析展开 主要内容 《黑神话悟空》采用的技术栈 《黑神话悟空》战斗系统的实现方式 四种攻击模式 连招系统的创建 如何实现高扩展性的战斗系统 包括角色属性系统、技能配置文件和逻辑节点的抽象等关键技术点 版权声明 本…

中国书法—孙溟㠭浅析碑帖《爨宝子碑》

中国书法——孙溟㠭浅析碑帖《爨宝子碑》 《爨宝子碑》 全称是《晋故振威将军建宁太守爨宝子之墓》&#xff0c;此碑刻于东晋大亨四年&#xff08;公元405年&#xff09;属楷书体。 《爨宝子碑》 《爨宝子碑》 至清朝乾隆四十三年&#xff08;1778年&#xff09;在云南南宁&…

【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)

本文项目编号 T 041 &#xff0c;文末自助获取源码 \color{red}{T041&#xff0c;文末自助获取源码} T041&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…

PHP邮箱系统:从入门到实战搭建教程指南!

PHP邮箱系统配置教程&#xff1f;如何选用合适的PHP邮箱系统库&#xff1f; 为了满足个性化和定制化的需求&#xff0c;许多开发者选择使用PHP来搭建自己的邮箱系统。AokSend将带你从入门到实战&#xff0c;详细介绍如何搭建一个功能完善的PHP邮箱系统。 PHP邮箱系统&#xf…

C#软键盘设计字母数字按键处理相关事件函数

应用场景&#xff1a;便携式设备和检测设备等小型设备经常使用触摸屏来代替键盘鼠标的使用&#xff0c;因此在查询和输入界面的文本或者数字输入控件中使用软件盘来代替真正键盘的输入。 软键盘界面&#xff1a;软键盘界面实质上就是一个普通的窗体上面摆放了很多图片按钮&…

Golang | Leetcode Golang题解之第416题分割等和子集

题目&#xff1a; 题解&#xff1a; func canPartition(nums []int) bool {n : len(nums)if n < 2 {return false}sum, max : 0, 0for _, v : range nums {sum vif v > max {max v}}if sum%2 ! 0 {return false}target : sum / 2if max > target {return false}dp …

对象检测边界框损失 – 从IOU到ProbIOU

1.概述 目标检测损失函数的选择在目标检测问题建模中至关重要。通常&#xff0c;目标检测需要两个损失函数&#xff0c;一个用于对象分类&#xff0c;另一个用于边界框回归&#xff08;BBR&#xff09;。本文将重点介绍 IoU 损失函数&#xff08;GIoU 损失、DIoU 损失和 CIoU 损…