代码随想录训练营D14-二叉树篇 p1 | 理论基础、递归遍历、迭代遍历、统一迭代
文章中的正经图片(非手划拉的)均源自代码随想录文章。
(〇)前瞻
(一)理论基础
需要了解 二叉树的种类,存储方式,遍历方式 以及二叉树的定义
1. 二叉树的种类
1.1 满二叉树
满二叉树,叶子结点都在最下面一层,并且除叶子结点外,都是度为2的结点。
高度为h,总结点数为n = 2^h - 1;h = log2(n + 1)
1.2 完全二叉树
在满二叉树的基础上,底层没有填满。但底层叶子结点一定从左到右连续的。
1.3 二叉搜索树
二叉搜索树是一个有序树
左孩子 < 父 < 右孩子
1.4 平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
最右一个不是平衡二叉树。对于值为10的根结点,左子树高度为2,右子树高度为0,差值== 2 > 1。
1.5 大顶堆、小顶堆
大根堆:完全二叉树基础上,根 >= 孩子
小根堆:完全二叉树基础上,根 <= 孩子
(对比二叉排序树BST:左<=根<=右)
1.6 编程语言中底层结构
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。
java中的容器
PriorityQueue:大顶堆、小顶堆(完全二叉树)
TreeMap:底层红黑树
TreeSet:基于TreeMap,
hashset、hashmap:散列表
2. 存储方式
2种存储方式
1.链式存储
就是链表
2.顺序存储
顺序存储中,父结点与孩子结点下标的关系。如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
3. 二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历(先沿一条路走到黑,再转其他;前中后序都是指根结点的位置)
-
- 前序遍历-根左右(递归法,迭代法)
-
- 中序遍历-左根右(递归法,迭代法)
-
- 后序遍历-左右根(递归法,迭代法)
- 广度优先遍历(对于树就是一层一层;对于图,一圈一圈遍历)
-
- 层次遍历(迭代法)
由于递归的底层就是通过栈完成的。所以前中后序中,可以手动通过栈来实现的,来完成非递归的代码。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
4.二叉树的定义
public class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(){}
TreeNode(int val){
this.val = val;
}
TreeNode(int val, TreeNode left,)
}
(二) 递归遍历 (必须掌握)
二叉树的三种递归遍历掌握其规律后,其实很简单
1. 递归三步骤
以前序遍历为例:
1)确定递归函数的参数和返回值
由于要打印出前序结点遍历后的结果,所以参数中除了传入根节点外,还要传入一个集合list用于存放遍历后的结果。初次之外,再不需要数据,因此返回值是void
void traversal(TreeNode tree, List<Integer> list)
2)确定终止条件
遍历到null空结点,自然本层递归要结束了。return 到上一层递归。
if(cur == null) return;
3)确定单层递归的逻辑
这里是前序遍历,是根左右的顺序。即先存根结点的数值,再去递归左子树、右子树。
list.add(cur.val);
traversal(cur.left, list);
traversal(cur.right, list);
2. (题目一)144. 二叉树的前序遍历
2.1 代码
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
traversal(root, list);
return list;
}
void traversal(TreeNode node, List<Integer> list){
if(node == null){
return;
}
//先序:根左右
list.add(node.val);
traversal(node.left, list);
traversal(node.right, list);
}
}
3. (题目二)145. 二叉树的后序遍历
3.1 代码
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
traversal(root, list);
return list;
}
void traversal (TreeNode node, List<Integer> list){
if(node == null){
return;
}
//后序:左右根
traversal(node.left, list);
traversal(node.right, list);
list.add(node.val);
}
}
4. (题目三)94. 二叉树的中序遍历
4.1 代码
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
traversal(root, list);
return list;
}
void traversal (TreeNode node, List<Integer> list){
//中序遍历--左根右
if(node == null){
return;
}
traversal(node.left, list);
list.add(node.val);
traversal(node.right, list);
}
}
(三) 迭代遍历 (基础不好的录友,迭代法可以放过)
迭代法,即非递归。由于编程语言的底层实现递归使用的是栈。所以理论上,在二叉树的相关题目中,能使用递归法解决的,也都能使用迭代法解决。
关于二叉树结点的操作一共有两步:访问结点(如何遍历)、处理结点(将结点值存入数组)
1. (题目一)144. 二叉树的前序遍历
1.1 思路
如何使用栈来模拟前序遍历的过程?
前序遍历 :根左右
首先根结点入栈。
开始循环,条件是栈不为空。
栈顶出栈,其数值填入result数组中;
如果出栈结点有右左孩子,那么出栈结点的右!左!孩子依次入栈(先入栈的右孩子,会在后面出栈;这样才符合前序遍历的中左右,右孩子后出)
直到循环结束,返回数组result
为什么能够通过出栈结点,找到其左右孩子呢?因为栈中存的数据可以是TreeNode类型的。
1.2 代码
public List<Integer> preorderTraversal1(TreeNode root) {
List<Integer> result = new ArrayList<>();
//判空
if(root == null){
return result;
}
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if(node.right != null){
stack.push(node.right);
}
if(node.left != null){
stack.push(node.left);
}
}
return result;
}
2. (题目二)145. 二叉树的后序遍历
2.1 思路
如何使用栈来模拟后序遍历的过程?
后序遍历 :左右根
先序遍历是:根左右(在代码中是根右左,因为栈先进后出的特性);
将先序中代码部分左右调换一下,此时整体求的是根右左(此时代码是根左右);
再将整体reverse一下,就是左右根。
2.2代码
public List<Integer> postorderTraversal1(TreeNode root) {
List<Integer> result = new ArrayList<>();
//判空
if(root == null){
return result;
}
//创建栈用于迭代,并且循环前 根结点入栈
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while(!stack.isEmpty()){
//栈顶弹栈 将数值存入数组, 将其左!右!孩子入栈
TreeNode node = stack.pop();
result.add(node.val);
if(node.left != null){
stack.push(node.left);
}
if(node.right != null){
stack.push(node.right);
}
}
//再对结果数组进行翻转
Collections.reverse(result);
return result;
}
3. (题目三)94. 二叉树的中序遍历
3.1 思路
如何使用栈来模拟后序遍历的过程?
后序遍历 :左右根
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
1)处理:将元素放进result数组中
2)访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
中序遍历。左根右
所以先一路向左,一路压栈,直到左孩子为null
开始弹栈,现在弹出的当前结点,相当于刚刚为null的左孩子的父结点,所以针对这三口之家,现在要去看当前结点的右结点了。右结点又可以看做新的父结点,开始一路向左,直到左孩子为null。
总结:根结点不为null,就一路向左,并压栈;
直到左孩子结点为null,弹栈一个结点,并将方向转为当前的right(转完之后还是要继续一路向左)
综上,循环条件是栈不空 或 当前结点不是null
3.2代码
public List<Integer> inorderTraversal1(TreeNode root) {
List<Integer> result = new ArrayList<>();
//判空
if(root == null){
return result;
}
Deque<TreeNode> stack = new ArrayDeque<>();
while(!stack.isEmpty() || root != null){
if(root != null){
stack.push(root);
root = root.left;
continue;
}
root = stack.pop();
result.add(root.val);
root = root.right;
}
return result;
}
4. 实现过程中的问题
使用Deque作为栈时,使用的api应是:pop、push
作为队列时,使用的是add、poll
(四)统一迭代 (基础不好的录友,迭代法可以放过)
统一迭代 (基础不好的录友,迭代法可以放过)
先跳过啦
(五) 今日收获,记录一下自己的学习时长
收获挺多,至少不得四五个小时?!