二叉树遍历算法和应用

二叉树是指度为 2 的树。它是一种最简单却又最重要的树,在计算机领域中有这广泛的应用。

二叉树的递归定义如下:二叉树是一棵空树,或者一棵由一个根节点和两棵互不相交的分别称为根节点的左子树和右子树所组成的非空树,左子树和右子树同样都是一棵二叉树。在二叉树中,每个节点的左子树的根节点被称为左孩子节点,右子树被称为右孩子节点。

二叉树通常使用如下数据结构来表示:

data 表示二叉树节点的数值,left_child 表示节点的左孩子节点,right_child 表示节点的右孩子节点。

struct node {int data;struct node *left_chile;struct node *right_child;
}

从二叉树的定义来看,有以下两点;

(1) 二叉树可以是一棵空树

(2) 二叉树的左子树和右子树不一定存在,可能同时不存在,可能同时存在,也可能左子树和右子树只存在一个

为什么要有二叉树 ?

在我们使用数据结构来存储数据的时候,最常见的两个使用场景是查找和插入。查找就是从数据结构中找到我们想要的值,插入是将一个新值插入到数据结构正确的位置。

在数据结构中,最常用的两个基础的数据结构是数组和链表,数组和链表的区别可以从如下 3 个方面来看:

① 插入元素。数组的插入效率比链表低,因为数组插入的时候,插入位置后边的元素都要向后移动;而向链表中插入元素的时候,只需要改变节点的指向就可以。前者的时间复杂度是 O(n) 的,后者的时间复杂度是 O(1) 的。

② 读取元素。读取数组中的第几个元素的时候,使用数组的下标直接读取就可以,而读取链表中的第几个元素的时候,需要对链表进行遍历。前者的时间复杂度是 O(1) 的,后者的时间复杂度是 O(n) 的。

③ cache。数组用一块连续的内存来维护,链表中每个节点的地址并不连续。从 cpu cache  LRU 算法的角度来看的话,数组中的元素离的更近,所以访问性能会更高。

从数组和链表的对比来看,没有哪种数据结构是具备所有的优点的。

二叉树有 3 个重要的变种,分别是二叉排序树,二叉平衡树,红黑树。

(1)二叉排序树

二叉排序数是一棵特殊的二叉树,在一般的二叉树中,区分左子树和右子树,但结点的值是无序的。在二叉排序树中,不仅区分左子树和右子树,而且整个树的节点是有序的。

二叉排序树又称二叉搜索树,它或者是一棵空树,或者是一棵非空二叉树,具备如下 3 个特点:

① 如果左子树非空,那么左子树上所有节点的值小于根节点的值

② 如果右子树非空,那么右子树上所有节点的值大于根节点的值,如果允许节点的值相等,那么大于等于

③ 左、右子树本身又各是一棵二叉排序树

从分析可以得出,在查找的性能上,二叉排序树的时间复杂度是 O(logn),相对于链表的 O(n),是有优化的。但是二叉排序树最差的情况,是每个节点都只包含一个子节点,这种情况下的时间复杂度就成为了 O(n),所以在二叉排序树的基础上出现了二叉平衡树。

(2)二叉平衡树

二叉平衡树首先是一颗二叉排序树,在此基础上又增加了一个条件,即每个节点的左子树和右子树的高度之差不大于 1。

有了这个限制之后,查找的时间复杂度就不会出现 O(n) 的情况。但是当向平衡树中插入元素的时候,为了保证插入之后二叉树还能满足平衡树的要求,需要进行旋转操作。对于频繁插入,删除的场景,旋转操作过于频繁,反而会增加时间的消耗。所以出现了红黑树,红黑树在查找和插入操作之间取得了平衡(这样的结论,只有专门研究红黑树算法的专业人员才能了解,在很多文章和书籍中也是这么说的,我们只需要知道这个事情即可)

(3)红黑树

红黑树的前提也是一颗二叉排序,在此基础上增加了以下 5 个条件:

① 红黑树中的节点要么是黑色,要么是红色

② 红黑树的根节点是黑色

③ 红黑树中红色节点不能连续,也就是说一个红色节点的父节点和子点都不能是红色

④ 从根节点开始,到叶子节点,每条路径上的黑色节点个数相等

⑤ 叶子节点是黑色,在红黑树中的叶子结点不是真实存在的,叶子结点在下图中用矩形来表示

在工程应用中,本人还没见过使用平衡树的场景,只见过使用红黑树的场景。

① 在 linux 内核中,维护 struct task_struct 使用了红黑树;维护 tcp 接收侧的乱序队列,使用了红黑树;epoll 中维护监听的 fd,也是用了红黑树

② 在 c++,java 内置的 map 数据结构中,使用了红黑树

本文只记录二叉树的基本算法,不涉及二叉排序树,二叉平衡树,红黑树相关的算法。

1 二叉树遍历

二叉树的遍历有 4 中算法:前序遍历,中序遍历,后序遍历以及层序遍历。前 3 种遍历算法中的前、中、后,说的是根节点的位置:前序遍历的顺序是根节点,左子节点,右子节点;中序遍历的顺序是左子节点,根节点,右子节点;后序遍历的顺序是左子节点,右子节点,根节点。不管哪种遍历,左子节点都在右子节点之前遍历。

如果了解图的遍历算法,可以知道图的遍历算法包括两种:深度优先遍历和广度优先遍历。二叉树的遍历方法中,前 3 种类似于图中的深度优先遍历,层序遍历类似于广度优先遍历。

二叉树跟递归算法联系比较紧密,递归算法有两点组成,一个是递归退出条件,一个是递归计算逻辑,也可以称为递归体,这一点和循环是有点相似的,循环也要有循环停止条件和循环处理逻辑。每种算法的对象都是围绕着一个数据结构,都需要有一个驱动力,这个驱动力往往就是对数据结构中的元素进行遍历,对于数组来说往往就是沿着数组下标进行遍历,对于链表来说往往就是从 head 开始沿着 next 向下遍历,对于二叉树来说就是沿着 left child 和 right child 进行遍历。对数据进行遍历是算法的基础。

递归算法,不太适合从一开始沿着算法逐渐向下思考,使用递归算法的时候我们可以使用一个最简单的场景来理解。比如,对于二叉树来说,我们以只有 3 个节点(一个 root,一个 left child,一个 right child)的二叉树来思考和理解。

1.1 前序遍历

力扣:. - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {if (root == nullptr) {return ret;}preScan(root);return ret;}void preScan(TreeNode *root) {ret.push_back(root->val);if (root->left) {preScan(root->left);}if (root->right) {preScan(root->right);}}private:vector<int> ret;
};

1.2 中序遍历

力扣:. - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {if (root == nullptr) {return ret;}midScan(root);return ret;}void midScan(TreeNode *root) {if (root->left) {midScan(root->left);}ret.push_back(root->val);if (root->right) {midScan(root->right);}}private:vector<int> ret;
};

1.3 后续遍历

力扣:. - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector<int> postorderTraversal(TreeNode* root) {if (root == nullptr) {return ret;}postScan(root);return ret;}void postScan(TreeNode *root) {if (root->left) {postScan(root->left);}if (root->right) {postScan(root->right);}ret.push_back(root->val);}private:vector<int> ret;
};

1.4 层序遍历

力扣:. - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {std::queue<TreeNode *> q;std::vector<vector<int>> ret;if (root == nullptr) {return ret;}q.push(root);while (!q.empty()) {// 用来保存二叉树一层的数据// 如果只是遍历,不用保存数据的话,那么是用不着这个数据结构的vector<int> line;int q_size = q.size();for (int i = 0; i < q_size; i++) {TreeNode *node = q.front();q.pop();line.push_back(node->val);if (node->left) {q.push(node->left);}if (node->right) {q.push(node->right);}}ret.push_back(line);}return ret;}
};

2 二叉树遍历的应用

2.1 求二叉树高度

力扣:. - 力扣(LeetCode)

求二叉树的高度,也是使用递归算法。有两种算法:

(1)求出左子树和右子树的高度,然后两者的较大值加 1 就是二叉树的高度

(2)使用动态规划的算法,每一次递归计算的时候,相当于向下增加了一层,在递归函数的形参中实时维护已经遍历的二叉树的高度,二叉树遍历完之后,最大值保存的就是二叉树的高度

2.1.1 左子树和右子树较大值

int maxDepth(struct TreeNode* root) {if (root == NULL) {return 0;}int left_height = 0;int right_height = 0;if (root->left) {left_height = maxDepth(root->left);}if (root->right) {right_height = maxDepth(root->right);}if (left_height > right_height) {return 1 + left_height;} else {return 1 + right_height;}
}

2.1.2 动态规划

class Solution {
public:int maxDepth(TreeNode* root) {maxDepthHelper(root, 0);return max_depth_;}void maxDepthHelper(TreeNode *root, int depth) {if (root == nullptr) {return;}depth++;if (depth > max_depth_) {max_depth_ = depth;}if (root->left) {maxDepthHelper(root->left, depth);}if (root->right) {maxDepthHelper(root->right, depth);}}private:int max_depth_ = 0;
};

2.2 求二叉树宽度

求二叉树的宽度和求二叉树的高度,算法是类似的,都可以通过动态规划的方式来进行。

在递归函数中用一个形参实时表示当前遍历的是哪一层的节点,每遍历一个节点都将这一层的宽度加 1。使用一个数组保存每一层的宽度,遍历结束之后,再遍历数组找到宽度最大的这一层。

class Solution {
public:int maxWidth(TreeNode* root) {maxWidthHelper(root, 0);for (int i = 0; i < max_depth_; i++) {if (width[i] > max_width_) {max_width_ = width[i];}}return max_width_;}void maxDepthHelper(TreeNode *root, int depth) {if (root == nullptr) {return;}width[depth] = width[depth] + 1;depth++;if (depth > max_depth_) {max_depth_ = depth;}if (root->left) {maxWidthHelper(root->left, depth);}if (root->right) {maxWidthHelper(root->right, depth);}}private:int max_width_ = 0;int max_depth_ = 0;int width[100] = {0}; // 定义一个保存每一层宽度的数组,假设二叉树的高度不会超过 100
};

2.3 二叉树路径

力扣:. - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector<string> binaryTreePaths(TreeNode* root) {tmp_data_path.push_back(root->val);binaryTreeScan(root);return ret;}void binaryTreeScan(TreeNode *root) {if (root->left == nullptr && root->right == nullptr) {string str_path = makePath();ret.push_back(str_path);return;}if (root->left) {// 使用递归算法找路径的典型代码// 三段式:push back --> 递归运算 --> pop back// 为什么递归运算结束之后,需要将元素 pop 出来// 因为通过递归算法,和这个节点相关的路径都已经遍历到了// 后边再遍历的路径,与这个节点无关了,所以需要把这个节点 pop 出来// 以防影响后边的路径// 图的遍历找路径的算法题中,主要的递归逻辑也是类似的tmp_data_path.push_back(root->left->val);binaryTreeScan(root->left);tmp_data_path.pop_back();}if (root->right) {tmp_data_path.push_back(root->right->val);binaryTreeScan(root->right);tmp_data_path.pop_back();}}string makePath() {int size = tmp_data_path.size();std::string str;for (int i = 0; i < size; i++) {if (i <  size - 1) {str += std::to_string(tmp_data_path[i]) + "->";} else {str += std::to_string(tmp_data_path[i]);}}return str;}private:vector<string> ret;vector<int> tmp_data_path;
};

2.4 二叉树翻转

力扣:. - 力扣(LeetCode)

/*** Definition for a binary tree node.* struct TreeNode {*     int val;*     TreeNode *left;*     TreeNode *right;*     TreeNode() : val(0), left(nullptr), right(nullptr) {}*     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:TreeNode* invertTree(TreeNode* root) {if (root == nullptr) {return root;}binaryTreeScan(root);return root;}void binaryTreeScan(TreeNode *root) {TreeNode *left = root->left;root->left = root->right;root->right = left;if (root->left) {binaryTreeScan(root->left);}if (root->right) {binaryTreeScan(root->right);}}
};

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

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

相关文章

DC-3渗透测试复现

DC-3渗透测试复现 目的&#xff1a; 获取最高权限以及5个flag 过程&#xff1a; 信息打点-sql注入-反弹shell- pkexec提权&#xff08;CVE-2021-4034&#xff09; 环境&#xff1a; 攻击机&#xff1a;kali(192.168.85.136) 靶机&#xff1a;DC_3(192.168.85.133) 复现…

SpringBoot中使用Jackson序列化返回

SpringBoot中使用Jackson序列化返回 在Spring Boot应用中&#xff0c;使用Jackson库来处理JSON的序列化和反序列化是一种常见的做法。Jackson是一个高效的JSON处理器&#xff0c;广泛用于Java环境中&#xff0c;尤其是在与Spring框架集成时。本文将详细介绍如何在Spring Boot中…

MATLAB数据类型和运算符+矩阵创建

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 ✍一、MATLAB数据类型和运算符&#x1f48e;1 MATLAB的数据类型&#x1f339;…

LeetCode 24. 两两交换链表中的节点

解题思路 这道题用画图的方法是比较好的。 相关代码 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.…

C语言输出不同颜色的字体

本文章在Linux进行演示&#xff01;&#xff01;&#xff01; 使用C语言输出不同颜色字体和背景 格式&#xff1a; printf("\033[字体背景颜色;字体颜色m字符串\033[0m"); 上边的 \033 也可以用 \e 来代替。 字体颜色与字符的对应关系 字符颜色30黑色31红…

经验分享:云知识库才是智能时代企业的最优选择

我知道你们可能会有点困惑&#xff0c;云知识库是什么&#xff1f;为什么它会是智能时代企业的最优选择&#xff1f;不要紧张&#xff0c;坐稳了&#xff0c;我带你们慢慢了解这个鲜为人知的神器。 首先&#xff0c;让我们说说什么是"云知识库"。云知识库就像一个在线…

uniapp APP真机调试接口请求不到服务器解决方法

项目场景&#xff1a; 在使用Hbuilder开发uniapp的过程中&#xff0c;出现了两个在 Chrome 调试中正常&#xff0c;但打包后异常的问题&#xff0c;特此记录。 问题描述 在 H5 端请求接口正常请求。 App 端 请求接口&#xff0c;提示 "{"errMsg":"reque…

常见的垃圾回收器(下)

文章目录 G1ShenandoahZGC 常见垃圾回收期&#xff08;上&#xff09; G1 参数1&#xff1a; -XX:UseG1GC 打开G1的开关&#xff0c;JDK9之后默认不需要打开 参数2&#xff1a;-XX:MaxGCPauseMillis毫秒值 最大暂停的时间 回收年代和算法 ● 年轻代老年代 ● 复制算法 优点…

常见的垃圾回收算法

文章目录 1. 标记清除算法2. 复制算法3. 标记整理算法4. 分代垃圾回收算法 1. 标记清除算法 核心思想&#xff1a; 标记阶段&#xff0c;将所有存活的对象进行标记。Java中使用可达性分析算法&#xff0c;从GC Root开始通过引用链遍历出所有存活对象。清除阶段&#xff0c;从…

如何使用 ArcGIS Pro 制作热力图

热力图是一种用颜色表示数据密度的地图&#xff0c;通常用来显示空间分布数据的热度或密度&#xff0c;我们可以通过 ArcGIS Pro 来制作热力图&#xff0c;这里为大家介绍一下制作的方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的POI数…

不得不掌握的性能测试监控神器 -nmon 工具详解

做性能测试时候进行结果收集和监控是我们必须要进行的一个步骤&#xff0c;所以选择一个好的监控工具能帮我们省事省心。我们今天给大家介绍的监听工具nmon&#xff0c;就是一个非常强大而且市场上也非常主流应用的一个监控工具。 做性能测试时候进行结果收集和监控是我们必须要…

vox2vec论文速读

vox2vec: A Framework for Self-supervised Contrastive Learning of Voxel-Level Representations in Medical Images 摘要 本文介绍了 vox2vec——一种体素级表示的自监督学习 &#xff08;SSL&#xff09; 对比方法 vox2vec 表示由特征金字塔网络 &#xff08;FPN&#xf…