🤡博客主页:醉竺
🥰本文专栏:《高阶数据结构》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨
目录
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左侧二叉查找树的查找效率也是更高的。
不难看出, 查找长度与树的高度是成正比的,也就是说,二叉查找树的查找效率主要取决于树的高度。在查找操作中,需要对比的节点次数一定不会超过该树的高度。
- 如果是一棵满二叉树或者完全二叉树,那么根据二叉树的性质五,该二叉树的高度为。换句话说,对于有n个节点的二叉树,它的最小高度是,这意味着查找操作最好情况时间复杂度为(n代表该二叉树的节点数量)。
- 如果一棵二叉树的高度和节点数相同,也就是一棵斜树,其高度为n,这意味着查找操作最坏情况时间复杂度为O(n), 看起来已经是一个链表了。
那么为了提高查找效率,应该尽可能地让二叉查找树的高度变得最小(尽可能接近)。也就是说,在创建二叉查找树时,应该尽可能让该二叉查找树保持左右节点的平衡,从而引出平衡二叉树的概念。所谓平衡二叉树,就是该树上任意节点的左子树和右子树深度之差不超过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;
}
-
找出中序遍历序列中当前节点的前趋和后继节点
解决这个问题的方法有很多,书写的程序代码也各不相同。如果每个节点要有一个指向父节点的指针,那么解决起来可能更容易一些,如果没有指向父节点的指针,那么一般就要从根节点开始找起。
但不管怎样,一定要把握住两个原则。
-
当前节点的前趋节点一定是比当前节点值小的,也是再往前的一系列节点中最大的。
-
当前节点的后继节点一定是比当前节点值大的,也是再往后的一系列节点中节点值最小的。
//找按中序遍历的二叉查找树中当前节点的前趋节点
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;
}
运行结果: