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

🤡博客主页:醉竺

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

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


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


目录

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.xdnf.cn/news/146320.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机网络篇】计算机网络概述

本文主要介绍计算机网络第一章节的内容&#xff0c;文中的内容是我认为的重点内容&#xff0c;并非所有。参考的教材是谢希仁老师编著的《计算机网络》第8版。跟学视频课为河南科技大学郑瑞娟老师所讲计网。 文章目录 &#x1f3af;一.计算机网络的组成 ✨主要内容 1.边缘部…

seL4 Capabilities(翻自官网)(一)

官网教程链接: Capability 初始化Capabilities tutorials // 先使用repo拉取一下tutorials&#xff0c;然后执行repo sync&#xff0c;所有的教程都在里面&#xff0c;学习某个的时候只需要改变的是 --tut 后面的参数 ./init --tut capabilities # building the tutorial exe…

国内可以使用的ChatGPT服务【9月持续更新】

首先基础知识还是要介绍得~ 一、模型知识&#xff1a; GPT-4o&#xff1a;最新的版本模型&#xff0c;支持视觉等多模态&#xff0c;OpenAI 文档中已经更新了 GPT-4o 的介绍&#xff1a;128k 上下文&#xff0c;训练截止 2023 年 10 月&#xff08;作为对比&#xff0c;GPT-4…

演示:基于WPF的DrawingVisual开发的Chart图表和表格绘制

一、目的&#xff1a;基于WPF的DrawingVisual开发的Chart图表和表格绘制 二、预览 钻井井轨迹表格数据演示示例&#xff08;应用Table布局&#xff0c;模拟井轨迹深度的绘制&#xff09; 饼图表格数据演示示例&#xff08;应用Table布局&#xff0c;模拟多个饼状图组合显示&am…

git使用“保姆级”教程2——初始化及工作机制解释

1、设置用户签名 解释&#xff1a; 签名的作用就是用来&#xff1a;标识用户&#xff0c;以区分不同的开发人员简单来说&#xff1a;用来标识"你是谁"&#xff0c;在提交代码时&#xff0c;会显示提交代码的是谁&#xff0c;把设置的信息一起提交上去 设置&#xff…

weblogic CVE-2019-2725 靶场攻略

漏洞描述 wls9-async等组件为WebLogic Server提供异步通讯服务&#xff0c;默认应⽤于WebLogic部分版本。由于该 WAR包在反序列化处理输⼊信息时存在缺陷&#xff0c;攻击者通过发送精⼼构造的恶意 HTTP 请求&#xff0c;即可获得⽬标服务器的权限&#xff0c;在未授权的情况…

4.使用 VSCode 过程中的英语积累 - View 菜单(每一次重点积累 5 个单词)

前言 学习可以不局限于传统的书籍和课堂&#xff0c;各种生活的元素也都可以做为我们的学习对象&#xff0c;本文将利用 VSCode 页面上的各种英文元素来做英语的积累&#xff0c;如此做有 3 大利 这些软件在我们工作中是时时刻刻接触的&#xff0c;借此做英语积累再合适不过&a…

Codeforces Round 973 (Div. 2) F1. Game in Tree (Easy Version)(思维题 博弈)

题目 思路来源 乱搞ac 题解 两个人的策略是一样的&#xff0c;把1到u的路径标记&#xff0c; 如果能走旁边的链&#xff08;也就是当前点&#xff0c;刨去标记链以外的子树中最长的链&#xff09;&#xff0c; 使得对面走剩余的连通块无法比你大&#xff0c;就走旁边的链&…

【计算机网络】网络层协议解析

网络层的两种服务IPv4分类编址划分子网无分类地址 IPv4地址应用IP数据报的发送和转发过程主机发送IP数据报路由器转发IP数据报 IPv4数据报首部格式ICMP网际控制报文协议虚拟专用网VPN与网络地址转换NAT 网络层主要任务是实现网络互连&#xff0c;进而实现数据包在各网络之间的传…

充电桩项目:前端实现

上次基于VueElement plus实现了充电桩项目后台管理系统的基本架子。 后端管理 员工管理 这次&#xff0c;又把用户端的基本架子搭建完毕&#xff1a;VueVant 首页 个人中心 充值 选择充值方式 优惠券中心 已过期优惠券 用户登录 用户注册 慢慢项目就有点样子了&#xff0c;代码…

远程桌面连接工具Microsoft Remote Desktop Beta for Mac

Microsoft Remote Desktop Beta for Mac 是一款功能强大的远程桌面连接工具&#xff0c;具有以下功能特点&#xff1a; 软件下载地址 跨平台连接&#xff1a; 允许 Mac 用户轻松连接到运行 Windows 操作系统的计算机&#xff0c;打破了操作系统的界限&#xff0c;无论这些 Wi…

Shiro-550—漏洞分析(CVE-2016-4437)

文章目录 漏洞原理源码分析加密过程解密过程 漏洞复现 漏洞原理 Shiro-550(CVE-2016-4437)反序列化漏洞 在调试cookie加密过程的时候发现开发者将AES用来加密的密钥硬编码了&#xff0c;并且所以导致我们拿到密钥后可以精心构造恶意payload替换cookie&#xff0c;然后让后台最…

基于无人机影像的可见光单木分割数据集-json格式

基于无人机影像的可见光单木分割数据集&#xff0c;共1700张影像&#xff0c;数据集大小3.6GB&#xff0c;分割标注采用标准json格式。 该数据集是一个专门用于基于无人机可见光影像进行单木分割的数据集&#xff0c;旨在帮助研究人员和开发者训练和评估基于深度学习的图像分割…

EndNoteX9快捷插入引用文献的教程以及出现的一些问题的解决方法(一)

使用EndNote向Word文档中插入引用文献时报错如图1所示&#xff1a; 解决方法为&#xff1a; 采用管理员身份运行Word与EndNote软件 当电脑中安装好Word与EndNote两款软件之后如何在Word中快速插入引用文献 &#xff08;一&#xff09;直接打开Word如图2&#xff0c;3&#x…

VisionPro - 基础 - 00 模板匹配技术和在VP中的使用 - PMAlign - PatMax - (3)

前言&#xff1a; 针对PatMax 的高级应用和原理&#xff0c;在这一节继续进行说明&#xff1a;这一节主要考虑的是PatMax模板匹配的原理&#xff1a; How PatMax Finds Patterns in an Image PatMax 模板匹配原理 1 Run-time Space When you search for a PatMax pattern in …

生信初学者教程(五):R语言基础

文章目录 数据类型整型逻辑型字符型日期型数值型复杂数数据结构向量矩阵数组列表因子数据框ts特殊值缺失值 (NA)无穷大 (Inf)非数字 (NaN)安装R包学习材料R语言是一种用于统计计算和图形展示的编程语言和软件环境,广泛应用于数据分析、统计建模和数据可视化。1991年:R语言的最…

DOCKER 数据库管理软件自己开发--———未来之窗行业应用跨平台架构

- 数据异地容灾服务--未来之窗智慧数据服务 DATA REMOTE DISASTER RECOVERY SERVICE -CyberWin Future Docker-数据查看 CyberWin DATA Viewer 1.docker 样式 mysqli://root:密码172.17.0.2:端口/数据库 阿雪技术观 拥抱开源与共享&#xff0c;见证科技进步奇迹&#xff0c;…

25届计算机专业毕设选题推荐-基于python+Django协调过滤的新闻推荐系统

&#x1f496;&#x1f525;作者主页&#xff1a;毕设木哥 精彩专栏推荐订阅&#xff1a;在 下方专栏&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; 实战项目 文章目录 实战项目 一、基于协调过滤的新闻推荐系统…

MySQL的登陆错误:ERROR 1049 (42000): Unknown database ‘root‘

MySQL的登陆错误&#xff1a;ERROR 1049 (42000): Unknown database ‘root’ 安装MySQL的时候&#xff0c;到网上查的命令行登陆MySQL的方法都是mysql -u root -p password mysql -r root -p 123456但是奇怪的是这条命令我输进去死活都不对&#xff0c;它都会要求再输入一遍…

how can I train a OpenAI fine tuned model with more prompts

题意&#xff1a;我如何使用更多提示来训练一个 OpenAI 微调模型&#xff1f; 问题背景&#xff1a; I fine-tuned OpenAI model with some prompts following this documentation it succeeded and created a new model in the playground. How I can retrain (fine-tune) th…