理论基础
种类:满二叉树、完全二叉树、二叉搜索树、平衡二叉搜索树
存储方式:链表和数组
遍历方式:深度优先遍历(前中后序遍历)、广度优先遍历(层次遍历)
递归遍历
递归算法怎么写:
- 确定递归函数的参数和返回值;
- 确定终止条件;
- 确定单层递归的逻辑。
递归的实现:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数。
144. 二叉树的前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
preorder(root,res);
return res;
}
public void preorder(TreeNode cur, List<Integer> res){
if(cur == null) return ;
res.add(cur.val);
preorder(cur.left,res);
preorder(cur.right,res);
}
}
94. 二叉树的中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root,res);
return res;
}
public void inorder(TreeNode cur, List<Integer> res){
if(cur == null) return ;
inorder(cur.left,res);
res.add(cur.val);
inorder(cur.right,res);
}
}
145. 二叉树的后序遍历
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
postOrder(root,res);
return res;
}
public void postOrder(TreeNode cur, List<Integer> res){
if(cur == null) return;
postOrder(cur.left,res);
postOrder(cur.right,res);
res.add(cur.val);
}
}
迭代遍历
144. 二叉树的前序遍历
遍历节点的顺序和处理节点(将元素放进结果集)的顺序一致,注意左右节点入栈时,得先进右再进左,才能达到中左右的遍历效果。这是因为栈是先入后出的。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
stack.offerFirst(root);
while(!stack.isEmpty()){
TreeNode cur = stack.pollFirst();
res.add(cur.val);
if(cur.right!=null) stack.offerFirst(cur.right);
if(cur.left!=null) stack.offerFirst(cur.left);
}
return res;
}
}
94. 二叉树的中序遍历
遍历节点的顺序和处理节点的顺序不一致,因此利用指针来遍历节点,利用栈来记录遍历过的节点。
- 当前节点若不为空则一直向左子节点遍历,直到为空
- 知道指针所对应的结点为空,则证明此时栈顶结点没有左孩子,将该栈顶结点弹出并用集合保存,再将指针指向该弹出结点的右孩子
- 重复上述过程,当指针指向空时则证明该子树遍历结束,若此时栈中没有元素可以弹出,则证明整颗树遍历完毕
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
while(cur!=null || !stack.isEmpty()){
if(cur!=null){
stack.offerFirst(cur);
cur = cur.left;
}else {
cur = stack.pollFirst();
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
}
145. 二叉树的后序遍历
解法一:利用二叉树的前序遍历
二叉树的前序遍历是中左右,先换成中右左,再反转结果数组,得到左右中的遍历顺序,即后序遍历。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
stack.offerFirst(root);
while(!stack.isEmpty()){
TreeNode cur = stack.pollFirst();
res.add(cur.val);
if(cur.left!=null) stack.offerFirst(cur.left);
if(cur.right!=null) stack.offerFirst(cur.right);
}
//Collections.reverse(res);
for(int i = 0, j = res.size() - 1; i < j; i++ , j--){
int tmp =res.get(j);
res.set(j,res.get(i));
res.set(i,tmp);
}
return res;
}
}
解法二:模拟栈
遍历节点的顺序和处理节点的顺序不一致,因此利用指针来遍历节点,利用栈来记录遍历过的节点。
- 当前节点若不为空则一直向左子节点遍历,直到为空
- 知道指针所对应的结点为空,则证明此时栈顶结点没有左孩子,将该栈顶结点弹出
- 若该栈顶节点是第一次出栈,则判断其右子树是否为空,若为空,则直接写入集合(左右中),若不为空,则将其重新入栈,先处理其右子树
- 若该栈顶节点是第二次出栈,说明其左右子树都已遍历完成,则直接写入集合(左右中)
- 通过标记的方法来判断栈顶节点是否二次出栈
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
//记录前一个处理的节点
TreeNode prev = null;
while(cur!=null || !stack.isEmpty()){
if(cur!=null){
//遍历到最左节点
stack.offerFirst(cur);
cur = cur.left;
}else{
cur = stack.pollFirst();
//cur.right == null :当前节点为子树根节点
//cur.right == prev : 右子树是上一次处理的节点则处理根节点
if(cur.right ==null || cur.right == prev){
res.add(cur.val);
//记录上一次处理的节点
prev = cur;
//这一步很重要,置空保证左子树不会被二次遍历。因为处理当前节点后,要向上走了
cur = null;
}else{
// 重新把根节点入栈,处理完右子树还要回来处理根节点
stack.offerFirst(cur);
cur = cur.right;
}
}
}
return res;
}
}
统一迭代
- 类似于递归的思想,比如中序遍历,就是先遍历左子树,然后处理根节点,再遍历右子树;
- 为了解决遍历与处理顺序不一致的问题,这里将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
- 标记方法:要处理的节点(访问过)放入栈之后,紧接着放入一个空指针作为标记。
//以后序遍历为例,注意入栈时顺序跟遍历顺序反着(左右中->中左右)
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if(root != null) stack.push(root);
while(!stack.isEmpty()){
TreeNode cur = stack.pop();
if(cur != null){
stack.push(cur);
stack.push(null);
if(cur.right != null) stack.push(cur.right);
if(cur.left != null) stack.push(cur.left);
}else{
res.add(stack.pop().val);
}
}
return res;
}
}
新的理解
深入理解前中后序
- 二叉树的前中后序遍历是什么,仅仅是三个顺序不同的 List 吗?
前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候,把代码写在不同位置,代码执行的时机也不同。(e.g. 倒序打印链表)
前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点。
- 前序位置的代码在刚刚进入一个二叉树节点的时候执行;
- 后序位置的代码在将要离开一个二叉树节点的时候执行;
- 中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
void traverse(TreeNode root) {
if (root == null) {
return;
}
// 前序位置
traverse(root.left);
// 中序位置
traverse(root.right);
// 后序位置
}
二叉树的所有问题,就是让你在前中后序位置注入巧妙的代码逻辑,去达到自己的目的,你只需要单独思考每一个节点应该做什么,其他的不用你管,抛给二叉树遍历框架,递归会在所有节点上做相同的操作。
2. 后序遍历有什么特殊之处?
- 前序位置的代码执行是自顶向下的,只能从函数参数中获取父节点传递来的数据;(如何打印出每一个节点所在的层数?)
- 中序位置(左中右)主要用在 BST 场景,BST 的中序遍历可以认为是遍历有序数组;
- 后序位置的代码执行是自底向上的,不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。(如何打印出每个节点的左右子树各有多少节点?)
只有后序位置才能通过返回值获取子树的信息。遇到子树问题 ,首先想给函数设置返回值,然后在后序位置做文章(e.g. 二叉树的直径)
3. 为什么多叉树没有中序遍历?
二叉树的每个节点只会进行唯一一次左子树切换右子树,而多叉树节点可能有很多子节点,会多次切换子树去遍历,所以多叉树节点没有「唯一」的中序遍历位置
两种解题思路
二叉树解题的思维模式分两类 (e.g. 二叉树的最大深度,前中后序遍历):
- 【遍历】:是否可以通过遍历一遍二叉树得到答案。可以的话,用一个traverse函数配合外部变量来实现。对应回溯算法核心框架
- 【分解问题】:是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案。可以的话,写出这个递归函数的定义,并充分利用这个函数的返回值。对应动态规划算法核心框架。
上述两种思维方式都需要思考:二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做?其他的节点由递归函数执行相同的操作。
递归遍历的返回值
- 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(113.路径总和ii)
- 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (236. 二叉树的最近公共祖先 )
- 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(112. 路径总和)