二叉树
基础概念
在正式的刷题之前,需要了解一下二叉树的基础概念。
首先,给出链表式二叉树的结构体:
struct TreeNode {
int val; //表示结点的值
TreeNode *left;
TreeNode *right;
TreeNode(x): val(x), left(nullptr), right(nullptr) {}
};
还可以有数组形式的二叉树,此处借用一张随想录中的图:
数组形式的二叉树中,如果父结点的数组下标为i
,那么左孩子的下标就是i * 2 + 1
,右孩子的下标就是i * 2 + 2
。
普通的二叉树如下图所示:
而在解题过程中,常用到的两种树的结构有两种:满二叉树与完全二叉树
-
满二叉树:顾名思义,这棵树是硕果累累的,结点上没有空缺,如下图所示;
-
完全二叉树:完全的意思,从中文的含以上,和满近似,但是其本质是从存储结构的角度命名的。知乎上有位博主讲得很好,文章如下完全二叉树看起来并不完全,为什么叫完全二叉树呢? - 知乎 (zhihu.com)
简而言之,完全二叉树在存储上,空间连续且无浪费,但是非完全二叉树,会造成连续的空间内有许多地方无数可放。
而从数值的角度出发,解题过程中常用到的是二叉搜索树与平衡二叉搜索树:
-
二叉搜索树(二叉排序树、二叉查找树):数值存储上有序,
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
其常用操作有:查找、插入、删除,这几个操作在之后的写题过程中都会遇到。
-
平衡二叉搜索树:AVL(Adelson-Velsky and Landis)树,它是一棵空树,或者它的左右两个子树的高度差的绝对值不超过1,且两个左右子树都是一棵平衡二叉树
平衡二叉搜索树的常用操作有:查找,插入,删除。各操作的时间复杂度与树的高度成正比,这一点还是很好理解的。
写到这里,想起来红黑树,在此也简单说一下:
红黑树,是实际应用中最常用的平衡二叉搜索树,为什么叫红黑树呢?是因为与普通二叉树不同,其在每个结点增加了一个用于记录结点颜色的标志位:Red or Black。
通过颜色的约束,任意一条从根到叶子结点路径上,红黑树可以保证最长路径不超过最短路径的二倍。
借鉴博客红黑树、平衡二叉查找树_辉常努腻的博客-CSDN博客_java红黑树与平衡二叉树中的一张图:
最短路径是全黑结点,最长路径是红黑结点,从根节点到叶子结点的路径上黑色结点的数量相同时,最长路径就是最短路径的两倍。
为了满足此特性,节省搜索的时间复杂度,红黑树的性质有以下几点:
- 根是黑色,其余结点是红色或是黑色
- 叶子结点都是黑色
- 红色结点的子结点是黑色,其父结点也是黑色,从根结点到叶子结点的路径上不会有两个连续的红色结点
- 同一层的任意结点到叶子结点的路径上,包含相同数量的黑色结点
此处就不再赘述其查找、删除,插入的相关操作了。有兴趣的同学可以参考上面提到的那篇博客,博主写的很好。
接下来要讲的就是重中之重,二叉树的遍历方式:
从遍历方式上讲,分为两类:
-
深度优先遍历(Depth First Search):从某个节点出发,一直遍历到叶子结点,然后逐步往上层返回;
又可以分为三种遍历方式:
- 前序遍历:“根左右”
- 中序遍历:“左根右”
- 后序遍历:“左右根”
每种遍历方式都可以有两种写法:递归法和迭代法。
-
广度优先遍历(Breath First Search):从某个节点出发,先遍历完与这个节点同层的所有节点。其对应的遍历方式为层序遍历,写法为迭代法。
以下就来介绍一下二叉树的递归遍历写法,补充一些我跟着代码随想录刷题过程中的一些想法:
递归遍历,顾名思义,往往需要在递归函数中调用递归函数,由于深度遍历的思想就类似于堆的调用,所以在求解深度遍历时,自然就会想到使用递归函数。
其实无论是在哪里使用递归函数,都需要明确三点:
- 返回类型(其实也是返回值)和传入参数,就是函数声明的那一行
- 处理逻辑:何时调用递归函数,前后需要做哪些处理,这是函数体内核心的那N行
- 终止条件:由于递归函数的自身调用性,必须要判断何时终止,就是何时弹回上一层,这是初入函数的那几行
遍历方法一共有三种,类似于随想录,我们先以前序遍历二叉树的各结点并保存这一场景为例:
-
返回类型,由于我们会将结果保存在一个vector里,所以函数无须返回任何值,只需要在函数体内将结果保存就可以;
-
传入参数,这就简单了,一定要传入的是当前遍历到的结点,另一个就是我们用于记录结点数值的vec(传引用,避免拷贝浪费时间)
-
处理逻辑,由于这个例子很简单,只是遍历顺序不一样,前序遍历为“中左右”,所以自然要先将当前结点数值加入vector,之后将当前结点的左、右孩子依次传入递归函数。代码近似如下:
vec.push_back(cur->val); // 中 traversal(cur->left, vec); // 左 traversal(cur->right, vec); // 右
-
确定终止条件:由于这个场景很简单,当我们遍历到空结点的时候,因为没有值,就直接返回就可以了
此场景下,前序遍历的代码类似如下:
注:随想录中使用traversal
(遍历)用作递归函数的名字,而我习惯使用recursion
(递归)
void recursion(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
vec.push_back(cur->val); // 中
recursion(cur->left, vec); // 左
recursion(cur->right, vec); // 右
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
recursion(root, result);
return result;
}
自然而然,中序遍历的递归函数如下:
void recursion(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
recursion(cur->left, vec); // 左
vec.push_back(cur->val); // 中
recursion(cur->right, vec); // 右
}
后序遍历的递归函数如下:
void recursion(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
recursion(cur->left, vec); // 左
recursion(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
此时就可以使用递归法完成LeetCode上对应的三道题,递归函数在上面已有,修改一下前序遍历的主函数即可