C++数据结构:二叉树之三(二叉搜索树扩展)

news2024/11/25 1:10:42

文章目录

  • 前言
  • 一、搜索父节点
  • 二、搜索子节点
  • 三、搜索前驱后继节点
  • 四、计算二叉树的高度
  • 五、测试
  • 总结


前言

我们接着写二叉树,在前文链接:《二叉树之二》中,我们生成了如下的有序二叉树,并且实现了插入、删除和四种遍历方法。今天我们继续实现一些二叉树的常用操作。在这里插入图片描述
我们还用上篇文章的有序二叉树,数据也不变了:int arr[10] = {5, 3, 6, 8, 9, 2, 4, 7, 10, 1}; 如上图所示


一、搜索父节点

在开始之前,我们得在原Btree类中加一个获取值的函数,因为原二叉树的Node节点定义成私有的。

在二叉树中,搜索父节点的方法通常需要从根节点开始遍历树,直到找到目标节点的父节点。下面是一个简单的搜索父节点的方法,它接受两个参数:根节点和目标节点的值。

T getValue(Node<T>* node){
    if (node == nullptr){
        throw std::invalid_argument("Node is null");
    }
    return node->val;
}

Node<T>* findParent(T v, Node<T>* p=nullptr){
    Node<T>* curr;
    if (!p) curr = root;
    else curr = p;
    if (curr == nullptr || curr->val == v){
        return nullptr;
    }
    if ((curr->left != nullptr && curr->left->val == v) || (curr->right != nullptr && curr->right->val == v)){
        return curr;
    }
    if (v < curr->val){
        return findParent(v, curr->left);
    } else{
        return findParent(v, curr->right);
    }
}

该方法首先检查根节点是否为空或者是否等于目标值。如果是,则返回空指针,因为根节点没有父节点。

接下来,该方法检查根节点的左右子节点是否等于目标值。如果是,则返回根节点,因为根节点是目标节点的父节点。

如果目标值小于根节点的值,则在左子树中递归查找;否则,在右子树中递归查找。可以这样使用:

auto par = t.findParent(8);
cout << t.getValue(par) << endl;
//返回值是6

这种方法的时间复杂度为 O(h),其中 h 是树的高度。对于平衡二叉树,时间复杂度为 O(log n),其中 n 是树中节点的总数,以后再学习平衡二叉树。

二、搜索子节点

我们在二叉树类中添加一个名为 findChild 的方法来搜索子节点。这个方法接受一个 T 类型的值作为参数,表示要搜索的节点的值。它返回一个 std::pair<Node, Node> 类型的值,表示找到的子节点。如果找到了左子节点,则第一个元素为左子节点;如果找到了右子节点,则第二个元素为右子节点;如果没有找到任何子节点,则两个元素都为 nullptr。

下面是 findChild 方法的实现代码:

pair<Node<T>*, Node<T>*> findChild(T v, Node<T>* p = nullptr){
    Node<T>* curr;
    if (!p) curr = root;
    else curr = p;
    if (curr == nullptr || curr->val == v){
        return make_pair(nullptr, nullptr);
    }
    if (curr->left != nullptr && curr->left->val == v){
        return make_pair(curr->left->left, curr->left->right);
    }
    if (curr->right != nullptr && curr->right->val == v){
        return make_pair(curr->right->left, curr->right->right);
    }
    if (v < curr->val) {
        return findChild(v, curr->left);
    } else {
        return findChild(v, curr->right);
    }
}

这个方法首先检查当前节点是否为空或者是否等于要搜索的值。如果是,则返回一个空的 pair 对象,表示没有找到任何子节点。

然后,该方法检查当前节点的左子节点和右子节点是否等于要搜索的值。如果找到了左子节点,则返回一个 pair 对象,其中第一个元素为左子节点的左子节点,第二个元素为左子节点的右子节点。如果找到了右子节点,则返回一个 pair 对象,其中第一个元素为右子节点的左子节点,第二个元素为右子节点的右子节点。

如果没有找到任何子节点,则该方法将在左子树或右子树中递归调用自身,以继续搜索。

我们可以使用这样使用 findChild 方法来搜索二叉树中指定节点的子节点:

auto chi = t.findChild(8);
cout << t.getValue(chi.first) << endl;
cout << t.getValue(chi.second) << endl;

返回值是7,9 实际使用中应该判断返回的是不是空节点再getValue

三、搜索前驱后继节点

在二叉搜索树中,一个节点的前驱节点是指其左子树中值最大的节点,而后继节点是指其右子树中值最小的节点。如果一个节点没有左子树,那么它的前驱节点是其第一个左祖先;如果一个节点没有右子树,那么它的后继节点是其第一个右祖先。前驱和后继节点在二叉搜索树的遍历和操作中非常有用。例如,在排序和搜索算法中,可以使用前驱和后继节点来快速定位给定值在有序二叉树中的位置。

//搜索前驱节点
Node<T>* findPre(T v, Node<T>* p = nullptr){
    Node<T>* curr;
    if (!p) curr = root;
    else curr = p;
    if (curr == nullptr){
        return nullptr;
    }
    if (curr->val == v){
        if (curr->left != nullptr){
            Node<T>* temp = curr->left;
            while (temp && temp->right != nullptr){
                temp = temp->right;
            }
            return temp;
        } else{
            Node<T>* tmp = findParent(v);
            if (tmp->right == curr){
                return tmp;
            } else{
                while (tmp != root){
                    tmp = findParent(tmp->val);
                    if (tmp->val < v) return tmp;
                }
                return nullptr;    
            }
        }  
    } else if (v < curr->val){
        return findPre(v, curr->left);
    } else{
        return findPre(v, curr->right);
    }
}

//搜索后继节点
Node<T>* findSuc(T v, Node<T>* p = nullptr){
    Node<T>* curr;
    if (!p) curr = root;
    else curr = p;
    if (curr == nullptr){
        return nullptr;
    }
    if (curr->val == v){
        if (curr->right != nullptr){
            Node<T>* tmp = curr->right;
            while (tmp && tmp->left != nullptr){
                tmp = tmp->left;
            }
            return tmp;
        } else{
            Node<T>* tmp = findParent(v);
            if (tmp->left == curr){
                return tmp;
            } else{      
                while (tmp != root->right){
                    tmp = findParent(tmp->val);
                    if (tmp->val > v){
                        return tmp;
                    } 
                }
                return nullptr;
            }
        }
        
    } else if (v < curr->val){  
        return findSuc(v, curr->left);
    } else{
        return findSuc(v, curr->right);
    }
}

上面的代码中,findPre 和 findSuc 函数分别用于查找给定值的前驱和后继节点。

在 findPre 函数中,我们首先检查当前节点是否为 nullptr 或者是否等于给定值。如果是,则返回 nullptr。然后,我们检查当前节点的右子节点是否等于给定值。如果是,则返回右子节点的左子树中的最大值。如果不是,则根据给定值与当前节点的值的大小关系,在左子树或右子树中查找。

findSuc 函数与 findPre 函数类似,只是将右子节点和左子树替换为左子节点和右子树。返回的也是节点指针,用 getValue 方法可以得到值。需要注意的是本文的getValue方法遇到空指针会抛出错误。

可以看出搜索前驱和后续节点非常麻烦,那么有没有别的方法来实现呢?这就是另一种特别的二叉树结构,线索二叉树,线索二叉树就是把除第一个和最后一个节点外的nullptr指针换成指向本节点的前驱(左)或后续(右)因为和树的左右指针一样都是指针,为了区分是左右指针还是前驱后续指针,可以在节点类中加二个识别标志,比如0是左右指针,1是前驱后续指针这样子。一次性将前驱不是自己左指针后的节点、后续不是自己右指针后的都标好,在遍历的时候可以加快速度。

四、计算二叉树的高度

二叉树的高度等于其左右子树中最高的加一,计算二叉树的高度在算法上有很多用处。例如,可以用来判断一棵树是否平衡,也可以用来优化搜索算法等。

int getDepth(Node<T>* p = nullptr){
	Node<T>* curr;
	if (!p) curr = root;
	else curr = p;
	if (curr == nullptr) return 0;
	stack<pair<Node<T>*, int>> s;
	s.push({root, 1});
	int maxDepth = 0;
	while (!s.empty()){
	    Node<T>* curr = s.top().first;
	    int depth = s.top().second;
	    s.pop();
	    maxDepth = max(maxDepth, depth);
	    if (curr->left != nullptr) s.push({curr->left, depth + 1});
	    if (curr->right != nullptr) s.push({curr->right, depth + 1});
	}
	return maxDepth;
}

这个函数使用一个栈来实现DFS算法。它首先将根节点和深度1压入栈中。然后,当栈不为空时,函数弹出栈顶元素,并更新最大深度。接着,如果当前节点的左右子节点不为空,则将它们和深度加1压入栈中。最后,函数返回最大深度作为二叉树的高度。

在上面的代码中,当我们访问根节点的左右子节点时,我们会将它们的深度加1,然后将它们和新的深度压入栈中。这样,当我们再次访问这些子节点时,它们的深度就会正确地增加。

五、测试

int main(){
    int arr[10] = {5, 3, 6, 8, 9, 2, 4, 7, 10, 1};
    Btree<int> t;
    for (int i=0; i<10; ++i) t.insert(arr[i]);
    //t.preOrder();
    //cout << endl;
    t.inOrder();
    cout << endl;
    //t.postOrder();
    //cout << endl;
    //t.layOrder();
    auto par = t.findParent(8);
    cout << t.getValue(par) << endl;
    auto chi = t.findChild(8);
    cout << t.getValue(chi.first) << endl;
    cout << t.getValue(chi.second) << endl;
    auto pre = t.findPre(4);
    cout << t.getValue(pre) << endl;
    auto suc = t.findSuc(6);
    cout << t.getValue(suc) << endl;
    cout << t.getDepth() << endl;
    return 0;
}

注意,文中省略了代码:#include <stack>#include <algorithm>using namespace std;


总结

本文介绍了关于有序二叉树的一些有用的扩展方法,比如搜索前驱后续节点可以用于查找树中小于且最接近某个值的节点,或大于且最接近的。熟练掌握这些方法,有助于后面更复杂的二叉树的学习。当然还有一些诸如将一棵二叉树插入为另一棵树的子树、求左子树或右子树的高度等文中未写的方法也值得掌握。主要是文章太长了,而且好像没人爱看,越复杂的东西看的人越少,虽然这个基本搜索二叉树也不见得有多复杂,只是相对代码量相对较多且充满了递归思想的代码。

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

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

相关文章

搭建 LNMP平台

搭建LNMP平台 一. 安装Nginx服务1.1 安装依赖包1.2 创建运行用户1.3 编译安装1.4 优化路径1.5 添加 Nginx 系统服务 二. 安装mysql服务2.1 安装Mysql环境依赖包2.2 创建运行用户2.3 编译安装2.4 修改mysql 配置文件2.5 更改mysql安装目录和配置文件的属主属组2.6 设置路径环境变…

C进阶:数据在内存中的存储(2)

六一儿童节快乐哇各位过期的小朋友们 引入&#xff1a; 在上一篇博文中&#xff0c;相信大家对于数据类型以及整数在内存中的存储有了一定了解&#xff0c;那么&#xff0c;浮点数是怎么在内存中存储的呢&#xff1f;下面来看一下我的讲解。 浮点数家族&#xff1a; 包括&#…

Python四则运算“计算器”——整数口算练习“小程序”

设定练习题目数量、最大取值整数数&#xff0c;即时判定&#xff0c;答对鼓励答错打印正确结果。 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&#xff0c;不仅仅是基础那么简…

python 1B 之路径

1. 绝对路径&#xff1a; os.path.abspath("") 翻译过来就是通过操作系统找到路径-绝对路径&#xff0c;os是operation system&#xff0c; abspath 是absolute path 比如现在运行的文件在service文件夹中&#xff0c;运行后&#xff1a; 2. 上层目录&#xff1a; 方…

快来,一起复习一下JDK动态代理和CGLib动态代理的区别

背景 工作也有四年了&#xff0c;基础的东西许久不看有些遗忘。一起来复习一下吧 JDK动态代理和CGLib的区别 JDK动态代理主要是针对类实现了某个接口&#xff0c;AOP则会使用JDK动态代理。它基于反射的机制实现&#xff0c;生成一个实现同样接口的一个代理类&#xff0c;然后…

MongoDB 学习

文章目录 前言1、MongoDB 的优势是什么2、部署2.1、Windows 系统中的安装启动2.2、Shell连接&#xff08;mongo命令&#xff09;2.3、Compass-图形化界面客户端2.4、Linux系统中的安装启动和连接 3、基本常用命令3.1、选择和创建数据库3.2、数据库删除3.3、集合操作3.3.1、集合…

HEVC预测编码介绍

介绍 ● 一幅图像内邻近像素之间有着较强的空间相关性&#xff0c;相邻图像之间也有很强的时间相关性&#xff1b; ● 预测编码&#xff08;Prediction Coding&#xff09;是指利用已编码的一个或几个样本值&#xff0c;根据某种模型或者方法&#xff0c;对当前的样本值进行预…

LeetCode572. 另一棵树的子树

题目 leetcode572. 另一棵树的子树 思路 递归解决。 ①递归的大问题化小----判断c是否为a子树可以转换为判断c是否为a左子树的子树或者c是否为a右子树的子树。 ②递归的结束条件-----如果两颗树相同&#xff0c;返回true&#xff1b;两棵树为空&#xff0c;返回true&#xff…

【十三】设计模式~~~行为型模式~~~中介者模式(Java)

中介者模式-Mediator Pattern【学习难度&#xff1a;★★★☆☆&#xff0c;使用频率&#xff1a;★★☆☆☆】 2.1. 模式动机 在用户与用户直接聊天的设计方案中&#xff0c;用户对象之间存在很强的关联性&#xff0c;将导致系统出现如下问题&#xff1a;系统结构复杂&#…

动态规划-硬币排成线

动态规划-硬币排成线 1 描述2 样例2.1 样例 1:2.2 样例 2:2.3 样例 3: 3 算法解题思路及实现3.1 算法解题分析3.1.1 确定状态3.1.2 转移方程3.1.3 初始条件和边界情况3.1.4 计算顺序 3.2 算法实现3.2.1 动态规划常规实现3.2.2 动态规划滚动数组 该题是lintcode的第394题&#x…

第二十一章 开发Productions - ObjectScript Productions - 延迟发送

文章目录 第二十一章 开发Productions - ObjectScript Productions - 延迟发送延迟发送 生成事件日志条目在 ObjectScript 中生成事件日志条目 第二十一章 开发Productions - ObjectScript Productions - 延迟发送 延迟发送 除了同步&#xff08;等待&#xff09;和异步&…

9秒被骗245万元?AI火了,骗子也来了!

生成式AI技术&#xff0c;如GPT-4等强大的语言模型的广泛普及&#xff0c;已经逐步开展应用。这种对未来技术的期待之余&#xff0c;不得不面对AI技术可能被滥用的风险&#xff0c;甚至已经有一些犯罪分子已开始巧妙地利用AI技术进行电信诈骗。 当下最积极学习的除了学生&#…

【高危】Apache bRPC <1.5.0 存在任意代码执行漏洞

漏洞描述 Apache bRPC 是C开发、由百度RPC发展而来的工业级 RPC 框架。 该项目受影响版本存在任意代码执行漏洞&#xff0c;由于server.cpp对于用户输入的pid_file使用wordexp展开。 具备bRPC控制权限的攻击者可在bRPC启动时通过控制pid_file参数注入恶意内容&#xff08;如…

DNS隧道流量分析

1.域名准备 选择哪家的云都没问题&#xff0c;国内云需要实名&#xff0c;不建议使用&#xff0c;这里我选择的TX云&#xff0c;因为之前注册过了&#xff0c;自己拿来做个流量分析不成问题 域名添加解析记录 需要准备自己的vps作为DNS隧道的服务端&#xff0c;且需要添加ns…

.nc文件根据经纬度提取点上数值python

.nc文件根据经纬度提取点上数值python 1、数据集2、代码部分3、完整代码 1、数据集 VOD Climate Archive &#xff08;VODCA&#xff09; 数据由维也纳工业大学提供&#xff08;https://doi.org/ 10.5281/zenodo.2575599&#xff09;。VODCA是一种空间分辨率为0.25的全球每日V…

【十一】设计模式~~~结构型模式~~~代理模式(Java)

【学习难度&#xff1a;★★★☆☆&#xff0c;使用频率&#xff1a;★★★★☆】 6.1. 模式动机 在某些情况下&#xff0c;一个客户不想或者不能直接引用一个对 象&#xff0c;此时可以通过一个称之为“代理”的第三者来实现 间接引用。代理对象可以在客户端和目标对象之间起…

网络流量管理系统

网络监控对于当今的组织至关重要&#xff0c;该过程的一个关键方面是分析网络的正常运行时间、可用性、性能和安全性。随着云和物联网技术越来越多地成为现代 IT 环境的一部分&#xff0c;网络管理员必须更加警惕他们授予访问权限的流量&#xff0c;包括确定谁在使用他们的网络…

【十四】设计模式~~~行为型模式~~~中介者模式(Java)

【学习难度&#xff1a;★★★☆☆&#xff0c;使用频率&#xff1a;★★★★★】 3.1. 模式动机 建立一种对象与对象之间的依赖关系&#xff0c;一个对象发生改变时将自动通知其他对象&#xff0c;其他对象将相应做出反应。在此&#xff0c;发生改变的对象称为观察目标&#…

提高测试效率5大捷径

1、优先级 测试任务和开发任务一样&#xff0c;都需要进行优先级排序。在测试工作中&#xff0c;优先进行级别高的测试任务&#xff0c;这样能够在无法保障测试周期的前提下&#xff0c;也不会对整体开发进度造成较大的影响。 提高测试效率5大捷径 2、重视测试策略 测试策略的基…

零代码,使用 Dify 两分钟接入企业微信

前置准备 企业微信的管理员权限 一个 Dify 的帐号 一个 Laf 云的帐号 &#xff08;可选&#xff09;一个 OpenAI 的 API Key。如果没有&#xff0c;可以使用 Dify 免费提供的 200 次调用机会用于测试。 &#xff08;可选&#xff09;在电脑上新建一个 env.txt 的文件&#…