C++二叉搜索树BinarySearchTree

编程入门 行业动态 更新时间:2024-10-25 00:35:20

C++二叉搜索树<a href=https://www.elefans.com/category/jswz/34/1525753.html style=BinarySearchTree"/>

C++二叉搜索树BinarySearchTree

一、介绍

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

2.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值

3.它的左右子树也分别为二叉搜索树

因此,在二叉搜索树中,每个值都有对应且唯一的位置。

二、二叉搜索树的模拟实现

1.Insert 插入

插入一个数据,根据二叉搜索树的要求,需要找到合适的位置,并且进行链接

首先是找到要插入的位置,用一个cur遍历,当插入的值cur小走左边,比cur大则走右边,找到对应位置后,我们还需要链接,因此还需要一个父节点,可以定义一个parent一起走,找到后定义一个新节点插入数据,进行链接即可。

		//插入数据bool Insert(const K& key){node* cur = _root;node* parent = nullptr;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{return false;}}cur = new node(key);if (key < parent->_key){parent->_left = cur;}else{parent->_right = cur;}}

2.InOrder 中序遍历

实现了数据的插入以后,为了验证实现的有没有bug,可以先将中序遍历给实现,根据二叉搜索树的特性,中序变量刚好是升序排序,将数据乱序插入后,中序打印,观察是否完成排序

中序遍历的实现是通过递归方式,在类里面实现递归需要用到_root作为参数,但外部接口一般不允许其获得内部成员变量,哪怕是提供一个函数接口获取,使用上也比较麻烦,因此可以在实现的时候再嵌套一层函数,在内部传参

		//中序遍历void InOrder(){_InOrder(_root);cout << endl;}void _InOrder(const node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}

3.Find 查找

查找的实现非常简单,只需要cur变量去按照二叉搜索树的特性去遍历即可,若是没找到则返回空指针,找到返回节点地址

		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;}

4.Erase 删除

删除是本章模拟实现部分的重点,也是最为复杂的,要分类讨论被删除对象的各种情况,条件判断上较为复杂

1.被删除的节点是叶子节点(既没有左孩子,也没有右孩子),则直接删除即可

2.被删除的节点有一个孩子节点,则此时需要将孩子进行托孤(将孩子节点与被删除的节点的父节点进行链接),此时需要分类讨论,当被删除节点是其父节点的左孩子时,则托孤给父节点的左指针,被删除节点是右孩子时,则托孤给右节点

观察发现,其实情况1和情况2可以被并为一类,只有一个孩子或者没有孩子时,都按照托孤的思路执行,并且由于需要链接,因此在往下找被删除节点时,会同时记录它的父节点,父节点需要根据情况分类讨论用哪个指针继承孩子,并且还有考虑到一种特殊情况,就是删除根时,要特殊处理

3.当被删除的节点有两个孩子时,则需要用替代法,即找被删除节点左边最大的值,或者右边最小的值与被删除的值进行替换,然后再将用于替换位置的节点删掉

注意,左边最大值,就是从左子树的根开始,一直往右找,右边最小值,就从右子树的根开始,一直往左找,因此,拿右边最小值举例,右边最小值一定没有左孩子,而可能有右孩子需要链接,右孩子的链接同样需要分类讨论

参考代码:

		//删除bool Erase(const K& key){node* cur = _root;node* parent = nullptr;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 || cur->_right == nullptr)//托孤{if (cur == _root)//被删的为根时,需要更新根{if (cur->_left != nullptr){_root = cur->_left;}else{_root = cur->_right;}}else//分情况链接{if (parent->_left == cur)//要删除的节点在父节点的左边,则无论如何都是链接左边{if (cur->_left != nullptr){parent->_left = cur->_left;}else{parent->_left = cur->_right;}}else if (parent->_right == cur)//要删除的节点在父节点的左边,则无论如何都是链接左边{if (cur->_left != nullptr){parent->_right = cur->_left;}else{parent->_right = cur->_right;}}}delete cur;return true;}else//要删除的节点有两个孩子{node* min_right = cur->_right;node* pmin_right = cur;while (min_right->_left){pmin_right = min_right;min_right = min_right->_left;}cur->_key = min_right->_key;if (pmin_right->_left == min_right){pmin_right->_left = min_right->_right;}else{pmin_right->_right = min_right->_right;}delete min_right;return true;}}}//找不到要删除的对象return false;}

三、递归实现接口

递归实现在思路上相对没那么直接,但是在代码上会简洁很多,当然在类里面递归都需要封装一层

1.Finer

递归实现查找很简单,遇到空则说明没找到,则返回nullptr,值比根小则往左子树找,值比根大则在右子树找,找到返回地址。

		node* Findr(const K& key){return _Findr(_root, key);}node* _Findr(node* root, const K& key){if (root == nullptr){return nullptr;}if (key < root->_key){return _Findr(_root->_left, key);}else if (key > root->_key){return _Findr(_root->_right, key);}else{return root;}}

2.Insertr

在插入数据中,有个很巧妙的对引用的运用,就是在传root指针时,传引用,则往下递归的root就是上一层指针的别名,因此在找到需要插入的位置时,不需要多记录一个父节点去进行链接,而是此时的root就是上一层父节点的指针的别名,因此可以直接new一个节点进行链接

		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->_left, key);}else if(key > root->_key){return _Insertr(root->_right, key);}else{return false;}}

3.Eraser

删除的思路和非递归的一样,递归部分主要可以利用root指针传引用的巧妙之处去省下很多处理

参考代码:

		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->_left, key);}else if (key > root->_key){return _Eraser(root->_right, key);}else{node* del = root;if (del->_left == nullptr){root = root->_right;}else if (del->_right == nullptr){root = root->_left;}else{node* min_right = del->_right;while (min_right->_left){min_right = min_right->_left;}root->_key = min_right->_key;return _Eraser(root->_right, min_right->_key);}delete del;return true;}}

四、构造与析构

1.构造函数

默认的构造即可,写一份出来是为了写拷贝构造时也能调默认的构造

		BSTree():_root(nullptr){}

2.拷贝构造

拷贝构造只需要递归遍历,前序遍历拷贝节点即可,可以用引用传root

		BSTree(const BSTree& n):_root(nullptr){copy(_root, n._root);}void copy(node*& root,const node* n){if (n == nullptr)return;root = new node(n->_key);copy(root->_left, n->_left);copy(root->_right, n->_right);}

3.赋值重载operator=

		BSTree& operator=(BSTree tmp){swap(_root, tmp._root);return *this;}

4.析构函数

		~BSTree(){Destroy(_root);}void Destroy(node*& root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;root = nullptr;}

五、二叉搜索树的应用场景

1. K模型

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

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下: 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树,在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

K模型针对的是“在不在”的问题

2.KV模型

KV模型比起K模型多了一个关键字val,每一个关键码key,都有与之对应的值Value,即<Key,Value>的键值对。

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

再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是就构成一种键值对;

3.实际场景应用

对应K模型的底层,就是最初我们实现的那一个版本,只需要稍微修改一下,就可以改成KV模型

在输入部分(构造,拷贝构造,节点定义,插入等等),多加一个参数value,基本逻辑都是用key去操作,因此不需要做大变动

例一:中英互译

void TestBSTree3()
{// 输入单词,查找单词对应的中文翻译BSTree<string, string> dict;dict.Insert("string", "字符串");dict.Insert("tree", "树");dict.Insert("left", "左边、剩余");dict.Insert("right", "右边");dict.Insert("sort", "排序");// 插入词库中所有单词string str;while (cin>>str){BSTreeNode<string, string>* ret = dict.Find(str);if (ret == nullptr){cout << "单词拼写错误,词库中没有这个单词:" <<str <<endl;}else{cout << str << "中文翻译:" << ret->_value << endl;}}
}

例二:水果统计

void TestBSTree4()
{// 统计水果出现的次数string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", 
"苹果", "香蕉", "苹果", "香蕉" };BSTree<string, int> countTree;for (const auto& str : arr){// 先查找水果在不在搜索树中// 1、不在,说明水果第一次出现,则插入<水果, 1>// 2、在,则查找到的节点中水果对应的次数++//BSTreeNode<string, int>* ret = countTree.Find(str);auto ret = countTree.Find(str);if (ret == NULL){countTree.Insert(str, 1);}else{ret->_value++;}}countTree.InOrder();
}

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

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

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

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

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

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

因此后续会继续学习平衡搜索二叉树:AVL树和红黑树,解决上面歪脖子的问题

总结

本章介绍了搜索二叉树的特性,并且用C++模拟实现,详细分析了代码思路

更多推荐

C++二叉搜索树BinarySearchTree

本文发布于:2023-11-17 02:40:06,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1636992.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:BinarySearchTree

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!