代码随想录算法训练营第14天|| 二叉树理论基础 || 递归遍历 || 迭代遍历 || 统一迭代
二叉树理论基础
二叉树的种类
- 满二叉树:只有度为0和度为2的结点,且度为0的结点都在同一层上
- 完全二叉树:只有最底层可能没满(但仍保持从左到右的顺序),其它层均满
- 二叉搜索树:左子树的结点均小于根节点,右子树的结点均大于根节点
- 平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
二叉树的存储方式
-
顺序存储(使用数组)
- 内存连续分布
- 父节点下标为i,左孩子就是2i+1,右孩子就是2i+2
-
链式存储(使用链表)
- 包含结点元素、左指针、右指针
- 通过指针将分布在各个地址的结点串起来
二叉树的遍历方式
深度优先遍历:向往深处走,遇到叶子结点再往回走
广度优先遍历:一层一层去遍历
- 深度优先
- 前序遍历
- 中序遍历
- 后序遍历
- 广度优先
- 层次遍历(迭代法)
栈就是递归的一种实现结构,其实前中后序遍历都可转化成使用栈实现非递归方式实现
二叉树的定义
顺序结构:直接使用数组
链式结构:关键得定义结点
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树的递归遍历
递归方法论:三要素
- 确定函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
以前序遍历为例:public List<Integer> preorderTraversal(TreeNode root) {
- 确定参数和返回值:因为需要返回一个List集合,且我们要递归遍历结点,所以参数为List和TreeNode,至于返回值,可以为null,也可以为List
- 确定终止条件:这里理所当然遍历到null就直接返回
- 确定单层递归的逻辑:由于是先序遍历,所以我们需要先存储根节点,再去遍历左结点,最后遍历右节点。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
preTraver(root, result);
return result;
}
public void preTraver(TreeNode node, List<Integer> result) {
if (node == null)
return;
result.add(node.val);
preTraver(node.left, result);
preTraver(node.right, result);
}
}
后序与中序只需调整一下代码顺序即可
二叉树的迭代遍历
递归的调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,递归返回时弹出
前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.add(root);
while (!stack.empty()) {
TreeNode node = stack.pop();
if (node == null)
continue;
result.add(node.val);
stack.add(node.right);
stack.add(node.left);
}
return result;
}
}
后序遍历:
- 只需修改一下代码顺序即可
- 先序遍历:中左右 --修改左右孩子入栈顺序–> 中右左 – 颠倒整体顺序 --> 左右中 即为后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.add(root);
while (!stack.empty()) {
TreeNode node = stack.pop();
if (node == null)
continue;
result.add(node.val);
stack.add(node.left);
stack.add(node.right);
}
Collections.reverse(result);
return result;
}
}
中序遍历:
- 中序遍历和前序遍历区别在于,前序遍历是访问元素的顺序与处理元素的顺序是一致的
- 后序遍历、中序遍历访问、处理元素顺序并不是一致,但后序遍历可通过调整先序遍历代码顺序得到,中序则不行
- 我们都知道我们的访问顺序肯定是从中开始的,而中序遍历确是左孩子开始,所以我们先一路向左,到头就处理,然后访问右孩子。
- 中序遍历(左中右)的流程
- 一路向左,为null时就弹出栈顶并处理,然后访问其右孩子(接着一路向左的操作),
- 假设其右孩子为null,继续弹出并处理,然后访问其右孩子
- 循环1/2步骤即可,直到指针和栈均为null
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode node = root;
while (node != null || !stack.empty()) {
if (node != null) {
stack.add(node);
node = node.left;
} else {
node = stack.pop();
result.add(node.val);
node = node.right;
}
}
return result;
}
}
二叉树的统一迭代法
在此前我们实现的迭代法中,除了先序和后序有关联,中序完全是另一个风格,栈和指针混用
之前我们使用栈,无法同时解决访问结点和处理结点不一致的情况,那我们就将访问的结点入栈,把要处理的结点入栈并且做标记
这里的标记则使用一个null指针作为标记,这种方法叫做标记法
以先序为例:
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<TreeNode>();
if (root != null)
stack.add(root);
while (!stack.empty()) {
TreeNode node = stack.pop();
if (node != null) {
if (node.right != null)
stack.add(node.right);
if (node.left != null)
stack.add(node.left);
stack.add(node);
stack.add(null);
} else {
node = stack.pop();
result.add(node.val);
}
}
return result;
}
}
中序和后序,只需要将
stack.add(node);
stack.add(null);
改变位置即可
本质就是标记一下处理结点,通过改变放入结点及其左右孩子的顺序,有点类似递归,做了标记的当前结点,但是如果左右孩子后入栈,就得先处理左右孩子最后才能到达当前结点,这就是后序遍历。