C++(搜索二叉树)

目录

前言:

 1.二叉搜索树

1.1二叉搜索树的定义 

1.2二叉搜索树的特点 

2.二叉搜索树的实现 

2.1框架

2.2查找 

2.3插入 

 2.4删除

1.右子树为空 

 2.左子树为空

 3.左右都不为空

3.递归版本

3.1前序遍历

3.2中序遍历 

3.3后续遍历 

3.4查找(递归版) 

3.5插入(递归版) 

3.6删除(递归版)

4.内部函数补充 

 4.1销毁

4.2拷贝构造和赋值重载

5.应用场景

 5.1单key场景

 5.2key-value场景

6 面试经典题



前言:

二叉树在前面 数据结构阶段已经讲过,本节取名二叉树进阶是因为:
1. map set 特性需要 先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
2. 二叉搜索树的特性了解,有助于更好的理解 map set 的特性
3. 二叉树中部分面试题稍微有点难度 ,在最后对常见的面试题进行复盘
4. 有些 OJ 题使用 C 语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻
烦。
因此本节借二叉树搜索树,对二叉树部分进行收尾总结

 1.二叉搜索树

1.1二叉搜索树的定义 

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

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

        对于搜索二叉树,对数据进行查找时,理想情况下时间复杂度为logN(二分查找),搜索查找还是很快的。

1.2二叉搜索树的特点 

         对于搜索二叉树,对该树进行中序遍历,得到的就是升序结果。比如一颗二叉搜索树如下图所示,对其进行中序遍历得到的结果为【1,3,4,6,7,8,10,13,14】

2.二叉搜索树的实现 

         我们想对一颗二叉搜索树进行增删查改,我们就要种一棵树,这棵树上的果子就是节点。

2.1框架

         利用学过的类和对象、泛型编程,对搜索二叉树的框架进行搭建

namespace Cmx //创建一个属于自己的域
{template <class T>struct BSTreeNode{BSTreeNode<T>* _left;BSTreeNode<T>* _right;T _val;BSTreeNode(const T&val):_left(nullptr),_right(nullptr),_val(val){}};template <class T>class BSTree{typedef BSTreeNode<T> Node;public://需要后续实现的函数 增删改查,前后中遍历,递归版本private:Node* _root;};
}

 二叉搜索树的节点类需要写出构造函数,因为后面创建新节点时会用到;二叉搜索树的根可以给个缺省值 nullptr,确保后续判断不会出错.

2.2查找 

 因为是对确定的值进行查找,所以需要有比较的过程:        

        a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
        b、最多查找高度次,走到到空,还没找到,这个值不存在。
bool find(const T& key){Node* cur = _root;while (cur){if (cur->_val < key){cur = cur->_right;}else if (cur->_val > key){cur = cur->_left;}else{return true;}}return false;}

 当查找的值存在时

当查找的值不存在时 

2.3插入 

插入的具体过程如下:(非重复版)

        a. 树为空,则直接新增节点,赋值给root 指针
        b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

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

 2.4删除

        删除需要注意的地方很多,也是面试常考的地方,删除逻辑:

首先查找元素是否在二叉搜索树中,如果不存在,则返回 , 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有 4 中情况,实际情况 a 可以与情况 b 或者 c 合并起来,因此真正的删除过程
如下:
情况 b :删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点 -- 直接删除
情况 c :删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点 -- 直接删除
情况 d :在它的右子树中寻找中序下的第一个结点 ( 关键码最小 ) ,用它的值填补到被删除节点
中,再来处理该结点的删除问题 -- 替换法删除

1.右子树为空 

 右子树为空时,只 需要将其左子树与父节点进行判断链接即可,无论其左子树是否为空,都可以链接,链接完成后,删除目标节点

 

 2.左子树为空

 操作同上,转换托孤方向。

 3.左右都不为空

         当左右都不为空时,就有点麻烦了,需要找到一个合适的值(即 > 左子树所有节点的值,又 < 右子树所有节点的值),确保符合二叉搜索树的基本特点

        符合条件的值有:左子树的最右节点(左子树中最大的)、右子树的最左节点(右子树中最小的),将这两个值中的任意一个覆盖待删除节点的值,都能确保符合要求

        这里找的是待删除节点 左子树的最右节点

为什么找 左子树的最右节点或右子树的最左节点的值覆盖 可以符合要求?

因为左子树的最右节点是左子树中最大的值,> 左子树所有节点(除了自己),< 右子树所有节点
右子树的最左节点也是如此,都能符合要求
通俗理解:需要找待删除节点的值的兄弟来镇住这个位置,而它的兄弟自然就是 左子树最右节

bool Erase(const T& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_val < key){parent = cur;cur = cur->_right;}else if (cur->_val > key){parent = cur;cur = cur->_left;}else{//找到该值//1.该节点左为空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;}}}else  //左右都不为空{Node* parent = cur;Node* leftMax = parent->_left;while (leftMax->_right){parent = leftMax;leftMax = leftMax->_right;}swap(cur->_val, leftMax->_val);if (parent->_left = leftMax){parent->_left = leftMax->_left;}else{parent->_right = leftMax->_left;}cur = leftMax;}delete cur;return true;}}return false;}

点 和 右子树最左节点,配合中序遍历结果可以确认

注意:

涉及更改链接关系的操作,都需要保存父节点的信息
右子树为空、左子树为空时,包含了删除 根节点 的情况,此时 parent 为空,不必更改父节点链接关系,更新根节点信息后,删除目标节点即可,因此需要对这种情况特殊处理
右子树、左子树都为空的节点,包含于 右子树为空 的情况中,自然会处理到
左右子树都不为空的场景中,parent 要初始化为 cur,避免后面的野指针问题


3.递归版本

我将二叉搜索树树的前序后序中序遍历放在这里,因为对于不同序的访问,我是利用递归实现的。

包括之前的插入,删除,查找我都要用递归实现。 

3.1前序遍历

前序:根 -> 左 -> 右

在递归遍历时,先打印当前节点值(根),再递归左子树(左),最后递归右子树(右)

因为这里是一个被封装的类,所以面临着一个尴尬的问题:二叉搜索树的根是私有,外部无法直接获取

解决方案:

公有化(不安全,也不推荐)
通过函数获取(安全,但用着很别扭)
将这种需要用到根的函数再封装(完美解决方案)
这里主要来说说方案3:类中的函数可以直接通过 this 指针访问成员变量,但外部可没有 this 指针,于是可以先写一个外壳(不需要传参的函数),在这个外壳函数中调用真正的函数即可,因为这个外壳函数在类中,所以此时可以通过 this 指针访问根 _root

具体操作如下:

	//===遍历===void PrevOrder(){_PrevOrder(_root);}protected:void _PrevOrder(const Node* root){if (root == nullptr)return;//前序:根左右cout << root->_key << " ";_PrevOrder(root->_left);_PrevOrder(root->_right);}

  

3.2中序遍历 

中序:左 -> 根 -> 右

在递归遍历时,先递归左子树(),再打印当前节点值(),最后递归右子树(

中序遍历也需要用到根,同样对其进行再封装

		void InOrder(){_InOrder(_root);}protected:void _InOrder(const Node* root){if (root == nullptr)return;//中序:左根右_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}

3.3后续遍历 

后序:左 -> 右-> 根

在递归遍历时,先递归左子树(),再递归右子树(),最后打印当前节点值(

一样需要进行再封装

		void PostOrder(){_PostOrder(_root);}
protected:void _PostOrder(const Node* root){if (root == nullptr)return;//后序:左右根_PrevOrder(root->_left);_PrevOrder(root->_right);cout << root->_key << " ";}

3.4查找(递归版) 

递归查找逻辑:如果当前根的值 < 目标值,递归至右树查找;如果当前根的值 > 目标值,递归至左树查找;否则就是找到了,返回 true

因为此时也用到了根 _root,所以也需要进行再封装

		//===递归实现===bool FindR(const K& key) const{return _FindR(_root, key);}
protected://递归实现bool _FindR(Node* root, const K& key) const{//递归至叶子节点也没找到if (root == nullptr)return false;//递归至右树if (root->_key < key)return _FindR(root->_right, key);//递归至左树else if (root->_key > key)return _FindR(root->_left, key);//找到了elsereturn true;}

3.5插入(递归版) 

基于递归查找的逻辑,实现递归插入

此时用到了一个很nb的东西:引用,实际插入时,甚至都不需要改链接关系,直接赋值即可

		bool InsertR(const K& key){return _InsertR(_root, key);}
protected:bool _InsertR(Node*& root, const K& key){if (root == nullptr){//得益于引用,可以对不同栈帧中的值进行修改root = new Node(key);return true;}//递归至右树if (root->_key < key)return _InsertR(root->_right, key);//递归至左树else if (root->_key > key)return _InsertR(root->_left, key);//冗余了,无法插入elsereturn false;}

 因为此时的参数是 节点指针的引用,所以在 保持原有链接属性的前提下,改变当前节点,即插入节点

3.6删除(递归版)

递归删除时也使用了引用,这样可以做到 在不同的栈帧中,删除同一个节点,而非临时变量

同时递归删除还用到了一种思想:转换问题的量级

比如原本删除的是根节点,但根节点之下还有很多节点,直接删除势必会造成大量的链接调整,于是可以找到 “保姆”(左子树的最右节点或右子树的最左节点),将 “保姆” 的值与待删除节点的值交换,此时递归至保姆所在的子树进行删除

因为保姆必然只带一个子树或没有子树,所以删除起来很简单

		bool EraseR(const K& key){return _EraseR(_root, key);}
protected:bool _EraseR(Node*& root, const K& key){if (root == nullptr)return false;if (root->_key < key)return _EraseR(root->_right, key);else if(root->_key > key)return _EraseR(root->_left, key);else{Node* del = root;	//需要保存一下待删除的节点信息//如果右树为空,则直接将左树覆盖上来if (root->_right == nullptr)root = root->_left;//如果左树为空,则直接将右树覆盖上来else if (root->_left == nullptr)root = root->_right;else{//递归为小问题去处理Node* maxLeft = root->_left;while (maxLeft->_right)maxLeft = maxLeft->_right;//注意:需要交换std::swap(root->_key, maxLeft->_key);//注意:当前找的是左子树的最右节点,所以递归从左子树开始return _EraseR(root->_left, key);}delete del;	//释放节点return true;}}

注意:

再次递归时,需要传递 root->_left 而非 maxLeft,因为此时的 maxLeft 是临时变量,而函数参数为引用
传递 root->_left 的原因:找的保姆出自左子树的最右节点,所以要求左子树中找,不能只传递 root,这样会导致查找失败 -> 删除失败
要使用 swap 交换 maxLeft->_key 与 key,然后递归时,找的就是 key;如果不使用交换而去使用赋值,那么递归查找的仍是 maxLeft->_key,类似于迭代删除时,将多余的节点删除

4.内部函数补充 

 4.1销毁

创建节点时,使用了 new 申请堆空间,根据动态内存管理原则,需要使用 delete 释放申请的堆空间,但二叉搜索树是一棵树,不能直接释放,需要 递归式的遍历每一个节点,挨个释放

释放思路:后序遍历思想,先将左右子树递归完后,才释放节点

		~BSTree(){destory(_root);}
protected://细节问题void destory(Node*& root){if (root == nullptr)return;//后序销毁destory(root->_left);destory(root->_right);delete root;root = nullptr;}

4.2拷贝构造和赋值重载

        

	BSTree(const BSTree<T>& t){_root = (t._root);}BSTree<T>& operator=(BSTree<T> t){swap(_root, t._root);return *this;}

5.应用场景

 5.1单key场景

key 模型的应用场景:在不在

  • 门禁系统
  • 车库系统
  • 检查文章中单词拼写是否正确

这些都是可以利用 key 模型解决,其实我们上面写的就是 key 模型,下面通过一段演示代码,展示 key 模型实现 单词查找系统

void BSTreeWordFind()
{vector<string> word = { "apple", "banana", "milk", "harmony" };Yohifo::BSTree<string> table;for (auto e : word)table.Insert(e);string str;while (cin >> str){if (table.Find(str))cout << "当前单词 " << str << " 存在于词库中" << endl;elsecout << "当前单词 " << str << " 没有在词库中找到" << endl;}
}

 5.2key-value场景

key / value 的模型:应用搜索场景

中英文互译字典
电话号码查询快递信息
电话号码 + 验证码
key / value 模型比 key 模型 多一个 value,即 kv 模型除了可以用来查找外,还可以再带一个值用于统计,这其实就是哈希的思想(建立映射关系)

kv 模型需要将代码改一下,新增一个模板参数 class value,插入时新增一个参数,同时操作也会有轻微改动,查找时返回的不再是 bool ,而是指向当前节点的指针,其他操作可以不用变

注:即使是 kv 模型,也只是将 key 作为查找、插入、删除的依据,基本逻辑与 value 没有任何关系,value 仅仅起到一个存储额外值的作用

将代码进行小改动,具体可查看源码

实现一个简单的中英词典

void BSTreeDictionary()
{vector<pair<string, string>> word = { make_pair("apple", "苹果"), make_pair("banana", "香蕉"), make_pair("milk", "牛奶"), make_pair("harmony", "鸿蒙")};Yohifo::BSTreeKV<string, string> table;for (auto e : word)table.InsertR(e.first, e.second);string str;while (cin >> str){Yohifo::BSTreeNodeKV<string,string>* ret = table.FindR(str);if (ret)cout << "当前单词 " << str << " 存在于词库中,翻译为 " << ret->_value << endl;elsecout << "当前单词 " << str << " 没有在词库中找到" << endl;}
}

6 面试经典题

1. 二叉树创建字符串。 OJ 链接
2. 二叉树的分层遍历 1 OJ 链接
3. 二叉树的分层遍历 2 OJ 链接
4. 给定一个二叉树 , 找到该树中两个指定节点的最近公共祖先 。 OJ 链接
5. 二叉树搜索树转换成排序双向链表。 OJ 链接
6. 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ 链接
7. 根据一棵树的中序遍历与后序遍历构造二叉树。 OJ 链接
8. 二叉树的前序遍历,非递归迭代实现 。 OJ 链接
9. 二叉树中序遍历 ,非递归迭代实现。 OJ 链接
10. 二叉树的后序遍历 ,非递归迭代实现。 OJ 链接

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

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

相关文章

【日常学习笔记】gflags

https://mp.weixin.qq.com/s/FFdAUuQavhD5jCCY9aHBRg gflags定义的是全局变量&#xff0c;在main函数后&#xff0c;添加::gflags::ParseCommandLineFlags函数&#xff0c;就能解析命令行&#xff0c;在命令行传递定义的参数。 在程序中使用DEFINE_XXX函数定义的变量时&#x…

Ubuntu 22.04 apt 安装 ros1 ros Noetic Ninjemys

众所周知 ros2还有很多功能没有移植&#xff0c;而ros1官方不再支持 ubuntu 20.04 之后的版本。另一方面Ubuntu 22.04 更新了很多对新硬件的驱动&#xff0c;有更好的兼容性和体验&#xff0c;这就变的很纠结。 如果想在 22.04 使用最新版本的 ros noetic 只有自己编译一个办法…

HTML 曲线图表特效

下面是代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>基于 ApexCharts 的 HTML5 曲线图表DEMO演示</title><style> body {background: #000524; }#wrapper {padding-top: 20px;background: #000524;b…

第二证券:大金融板块逆势护盘 北向资金尾盘加速净流入

周一&#xff0c;A股商场低开低走&#xff0c;沪指收盘失守2800点。截至收盘&#xff0c;上证综指跌2.68%&#xff0c;报2756.34点&#xff1b;深证成指跌3.5%&#xff0c;报8479.55点&#xff1b;创业板指跌2.83%&#xff0c;报1666.88点。沪深两市合计成交额7941亿元&#xf…

WEB安全渗透测试-pikachuDVWAsqli-labsupload-labsxss-labs靶场搭建(超详细)

目录 phpstudy下载安装 一&#xff0c;pikachu靶场搭建 1.下载pikachu 2.新建一个名为pikachu的数据库 3.pikachu数据库配置 ​编辑 4.创建网站 ​编辑 5.打开网站 6.初始化安装 二&#xff0c;DVWA靶场搭建 1.下载DVWA 2.创建一个名为dvwa的数据库 3.DVWA数据库配…

微信小程序(十八)组件通信(父传子)

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.组件属性变量的定义 2.组件属性变量的默认状态 3.组件属性变量的传递方法 解释一下为什么是父传子&#xff0c;因为组件是页面的一部分&#xff0c;数据是从页面传递到组件的&#xff0c;所以是父传子&#xf…

防火墙的用户认证

目录 1. 认证的区别 2. 用户认证的分类 区别&#xff1a; 3. 上网用户认证的认证方式 3.1 置用户认证的位置&#xff1a; 3.1.1 认证域 创建认证域&#xff1a; 新建一个用户组&#xff1a; 新建一个用户 创建安全组 4. 认证策略 4.1 认证策略方式&#xff1a; 4.2…

MR image smoothing or filtering 既 FWHM与sigma之间的换算关系 fslmaths -s参数

这里写目录标题 FWHM核高斯核中的sigma是有一个换算公式&#xff1a;结果 大量的文献中都使用FWHM 作为单位&#xff0c;描述对MR等数据的平滑&#xff08;smoothing&#xff09;或者滤波&#xff08;filtering&#xff09;过程。FWHM 通常是指full width at half maximum的缩写…

【新书推荐】3.5 char类型

本节必须掌握的知识点&#xff1a; 示例十 代码分析 汇编解析 3.5.1 示例十 char类型是比较古怪的&#xff0c;int\short\long类型如果在使用时不指定signed还是unsigned时都默认是signed&#xff0c;但char不一样&#xff0c;编译器可以实现为带符号的&#xff0c;也可以实现…

Flink实现数据写入MySQL

先准备一个文件里面数据有&#xff1a; a, 1547718199, 1000000 b, 1547718200, 1000000 c, 1547718201, 1000000 d, 1547718202, 1000000 e, 1547718203, 1000000 f, 1547718204, 1000000 g, 1547718205, 1000000 h, 1547718210, 1000000 i, 1547718210, 1000000 j, 154771821…

【QT】文件目录操作

目录 1 文件目录操作相关的类 2 实例概述 2.1 实例功能 2.2 信号发射者信息的获取 3 QCoreApplication类 4 QFile类 5 QFilelnfo类 6 QDir类 7 QTemporaryDir和QTemporaryFiIe 8 QFiIeSystemWatcher类 文件的读写是很多应用程序具有的功能&#xff0c;甚至某些应用程序就是围绕…

内存管理(mmu)/内存分配原理/多级页表

1.为什么要做内存管理&#xff1f; 随着进程对内存需求的扩大&#xff0c;和同时调度的进程增加&#xff0c;内存是比较瓶颈的资源&#xff0c;如何更好的高效的利于存储资源是一个重要问题。 这个内存管理的需求也是慢慢发展而来&#xff0c;早期总线上的master是直接使用物…

Oracle篇—分区索引的重建和管理(第三篇,总共五篇)

☘️博主介绍☘️&#xff1a; ✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ ✌✌️擅长Oracle、MySQL、SQLserver、Linux&#xff0c;也在积极的扩展IT方向的其他知识面✌✌️ ❣️❣️❣️大佬们都喜欢静静的看文章&#xff0c;并且也会默默的点赞收藏加关注❣…

ES的一些名称和概念总结

概念 先看看ElasticSearch的整体架构&#xff1a; 一个 ES Index 在集群模式下&#xff0c;有多个 Node &#xff08;节点&#xff09;组成。每个节点就是 ES 的Instance (实例)。每个节点上会有多个 shard &#xff08;分片&#xff09;&#xff0c; P1 P2 是主分片, R1 R2…

达梦数据库——记录一次离谱的登录失败报错

好久没更新了哇 前面有整理过一些常见的数据库登录失败问题哈&#xff0c;今天记录一个遇到概率比较小&#xff0c;但碰上了一般不太容易找到原因的登录失败问题。 今天给客户同时初始化了三台服务器数据库&#xff0c;惟独这一台死活登不进去&#xff0c;满脑子问号&#xf…

【论文解读】Object Goal Navigation usingGoal-Oriented Semantic Exploration

论文&#xff1a;https://devendrachaplot.github.io/papers/semantic-exploration.pdf 代码&#xff1a;https://github.com/devendrachaplot/Object-Goal-Navigation 项目&#xff1a; Object Goal Navigation using Goal-Oriented Semantic Exploration example&#xff1…

2、鼠标事件、键盘事件、浏览器事件、监听事件、冒泡事件、默认事件、属性操作

一、鼠标事件 1、单击事件&#xff1a;onclick <body><header id"head">我是头部标签</header> </body> <script> var head document.getElementById("head")head.onclick function () {console.log("我是鼠标单击…

金蝶云星空--写插件不重启IIS热更新简单配置指南

云星空7.5版本&#xff0c;以简单方式配置并测试了热更新的实现方式可行&#xff0c;操作如下&#xff08;7.5外版本没试过&#xff0c;大家可试下&#xff09;&#xff1a; 1、打开WebSite\App_Data\Common.config&#xff0c;修改appSettings&#xff0c;设置IsEnablePlugIn…

go slice 扩容实现

基于 Go 1.19。 go 的切片我们都知道可以自动地进行扩容&#xff0c;具体来说就是在切片的容量容纳不下新的元素的时候&#xff0c; 底层会帮我们为切片的底层数组分配更大的内存空间&#xff0c;然后把旧的切片的底层数组指针指向新的内存中&#xff1a; 目前网上一些关于扩容…

redis源码之:clion搭建cluster环境

cluster集群通常每个node节点都是一主N从的模式&#xff0c;此处为简化环境搭建&#xff0c;所有node节点均只有一个主节点。 在clion环境中&#xff0c;为方便debug&#xff0c;需要通过配置多个cmake application实现redis-server、redis-cli等源码debug模式启动。 一、配置…