目录
10.二叉树(上)结构与遍历 总览
笔记思维导图链接
参考左程云体系算法课程笔记
参考慕课网算法体系课程笔记
常见题目汇总:
1:二叉树深度遍历
1.1 二叉树深度遍历递归实现
其实,每个节点在递归调用时,都被访问了三次,三种遍历,区别只是再哪一次访问时进行处理
- 前序遍历
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
preDFS(root, res);
return res;
}
private void preDFS(TreeNode node, List<Integer> list) {
if(node == null) return;
list.add(node.val); // 第一次访问时就处理节点
preDFS(node.left, list);
preDFS(node.right, list);
}
- 中序遍历
// 中序遍历
private void inDFS(TreeNode node, List<Integer> list) {
if (node == null) return;
inDFS(node.left, list);
list.add(node.val); // 第二次访问该节点时处理
inDFS(node.right, list);
}
- 后续遍历
// 后序遍历
private void posDFS(TreeNode node, List<Integer> list) {
if (node == null) return;
posDFS(node.left, list);
posDFS(node.right, list);
list.add(node.val); // 第三次访问该节点时处理
}
1.2 二叉树深度遍历非递归实现
前序遍历
题目链接:
题解:
- 1.由于是深度遍历,使用辅助空间栈来对应节点的访问和处理过程
- 即,访问即压入栈,处理即将此节点弹出栈进行处理
- 2.对于头,访问和处理都是先从头节点开始,故访问到根节点时,先压入栈,并随即处理栈顶元素
- 3.处理完栈顶元素后,开始访问并处理子节点,
- 由于栈的弹出顺序与入栈顺序相反,故,访问顺序为左右,压栈顺序为右左
- 左右均压入栈后,每次处理栈顶元素,即每次处理刚开始访问的头节点(相对于父节点的左节点)
- 第一轮左节点访问处理完后,再处理深度最深的右节点,即此时的栈顶元素,作为头节点循环进行
private List<Integer> list;
private List<Integer> list;
private Stack<TreeNode> stack;
private TreeNode cur;
// 前序遍历
public List<Integer> preorderTraversal(TreeNode root) {
list = new ArrayList<>();
if(root == null) return list; // 注意不输入节点时,返回[],不是null
// - 1.由于是深度遍历,使用辅助空间栈来对应节点的访问和处理过程
// - 即,访问即压入栈,处理即将此节点弹出栈进行处理
stack = new Stack<>();
// - 2.对于头,访问和处理都是先从头节点开始,故访问到根节点时,先压入栈,并随即处理栈顶元素
stack.push(root);
while(!stack.isEmpty()) {
cur = stack.pop(); // 保存要处理的节点
list.add(cur.val); // 处理刚访问的节点
// - 3.处理完栈顶元素后,开始访问并处理子节点,
// - 由于栈的弹出顺序与入栈顺序相反,故,访问顺序为左右,压栈顺序为右左
if(cur.right != null) stack.push(node.right);
if(cur.left != null) stack.push(node.left);
}
return list;
}
复杂度分析:
- 时间复杂度:O(n),
- 其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次
- 空间复杂度:O(n)。
- 空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
后序遍历
题目链接:
题解:
- 后续遍历顺序为左右头,将前序遍历该成头右左,再反转,即是左右头的顺序
- 可以直接使用库函数集合容器的reverse方法
- 也可以再使用一个栈来自己实现逆序操作
// 使用Collections.reverse(list)逆序
class Solution {
private List<Integer> list;
private Stack<TreeNode> stack;
private TreeNode cur;
public List<Integer> postorderTraversal(TreeNode root) {
list = new ArrayList<>();
if(root == null) return list; // 注意不输入节点时,返回[],不是null
// - 1.由于是深度遍历,使用辅助空间栈来对应节点的访问和处理过程
// - 即,访问即压入栈,处理即将此节点弹出栈进行处理
stack = new Stack<>();
// - 2.对于头,访问和处理都是先从头节点开始,故访问到根节点时,先压入栈,并随即处理栈顶元素
stack.push(root);
while(!stack.isEmpty()) {
cur = stack.pop(); // 保存要处理的节点
list.add(cur.val); // 处理刚访问的节点
// - 3.处理完栈顶元素后,开始访问并处理子节点,
// - 由于栈的弹出顺序与入栈顺序相反,故,访问顺序为右左,压栈顺序为左右
if(cur.left != null) stack.push(cur.left);
if(cur.right != null) stack.push(cur.right);
}
Collections.reverse(list);
return list;
}
}
// 使用两个栈实现逆序
class Solution {
private List<Integer> list;
private Stack<TreeNode> stack;
private Stack<TreeNode> stack2;
private TreeNode cur;
public List<Integer> postorderTraversal(TreeNode root) {
list = new ArrayList<>();
if(root == null) return list; // 注意不输入节点时,返回[],不是null
// - 1.由于是深度遍历,使用辅助空间栈来对应节点的访问和处理过程
// - 即,访问即压入栈,处理即将此节点弹出栈进行处理
stack = new Stack<>();
stack2= new Stack<>();
// - 2.对于头,访问和处理都是先从头节点开始,故访问到根节点时,先压入栈,并随即处理栈顶元素
stack.push(root);
while(!stack.isEmpty()) {
cur = stack.pop(); // 保存要处理的节点
stack2.push(cur); // 处理刚访问的节点
// - 3.处理完栈顶元素后,开始访问并处理子节点,
// - 由于栈的弹出顺序与入栈顺序相反,故,访问顺序为右左,压栈顺序为左右
if(cur.left != null) stack.push(cur.left);
if(cur.right != null) stack.push(cur.right);
}
while(!stack2.isEmpty()) {
list.add(stack2.pop().val);
}
return list;
}
}
中序遍历
题目链接:
题解:
- 1.由于访问的的顺序和处理的顺序不一致,需要使用指针指向要处理的节点
- 2.先访问:用指针,指向左分支中每个要访问的节点,先不处理,而是放入栈中,一直深到最左位置
- 初始stack为null,先不能加cur,与后面判断逻辑冲突
- 故要让循环能开始,需要再给定一个开始条件cur!=null;
- 3.再处理;指针指向要处理的节点,即栈顶元素
- 因为左边都访问完了,回到头节点的第二次访问时机进行处理
- 4.指针指向处理节点的右节点,以此节点为根节点,继续下一轮的访问处理,
- 维持左中右遍历顺序
// 中序遍历
public List<Integer> inorderTraversal(TreeNode root) {
list = new ArrayList<>();
if(root == null) return list;
// - 1.由于访问的的顺序和处理的顺序不一致,需要使用指针指向要处理的节点
stack = new Stack<>();
cur = root; // cur先指向根节点,深入,直到指向要处理的节点
// - 2.先访问:用指针,指向左分支中每个要访问的节点,先不处理,而是放入栈中,一直深到最左位置
while(cur != null || !stack.isEmpty()) { // cur != null让循环开始
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
// - 3.再处理;指针指向要处理的节点,因为左边都访问完了,回到头节点的第二次访问
cur = stack.pop();
list.add(cur.val);
// - 4.指针指向处理节点的右节点,以此节点为根节点,继续下一轮的访问处理,维持左中右遍历顺序
cur = cur.right;
}
return list;
}
2. 二叉树公共祖先问题
2.1 证明题:二叉树某一节点x祖先节点的交集
题意:
-
证明:已知X是二叉树中某一个节点, 整棵树的先序遍历结果, 整棵树后序遍历结果, 则有结论:
- X先序遍历之前的节点定义为集合A, X后序遍历之后的节点定义为集合B,
- 则 A∩B得到的结果有且仅是X的祖先节点
-
例如:二叉树结构如下:A,B集合的交集为a,c,均为X的祖先节点
题解:
-
证明的思路是:
- 1.先证明X的祖先节点一定出现在交集中
- 2.非X祖先的节点一定不出现在交集中
-
1.证明X的祖先节点一定出现在交集中
-
先序遍历顺序:头左右,
- 故,作为X的祖先节点,一定是X的头节点,顺序一定出现在X之前,
- 即祖先节点一定包含在A中
-
后序遍历顺序:左右头
- 故,作为X的祖先节点,一定是X的头节点,顺序一定出现在X之后,
- 即祖先节点一定包含在B中,
-
综上:X的祖先节点一定包含在A,B的交集中
-
-
2.非X祖先的节点一定不出现在交集中
-
X的孩子
-
先序遍历中,X孩子节点一定在X后面,故,A中一定没有,可排除
-
-
X作为左树姿态的右兄弟节点
-
在前序遍历中,X的右兄节点一定在X后面,故集合A中也一定没有,可排除
-
-
X作为右树姿态的左兄弟节点
- 在后序遍历中,X的左兄节点一定在X前面,故集合中一定没有,可排除
-
2.2 二叉搜索树的最近公共祖先
题目链接:
题意:
- 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先
- 最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。
- 题中说明:
- 所有节点的值都是唯一的。
- p、q 为不同节点且均存在于给定的二叉搜索树中
示例:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
题解:
二叉搜索树,根节点与子节点有大小关系,因此只需循环遍历节点,可以凭借此进行查找
- 1.从根节点开始遍历判断是不是满足的节点
- 2.主要解决走哪个分支,
- cur若不是最近公共祖先,p,q节点一定在cur的一边,继续深入找一边即可
- cur若是最近公共祖先,p,q节点一定在cur的两边,说明找到了
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode cur = root;
while(cur != null) {
if(cur.val > p.val && cur.val > q.val) { // 说明要找到节点在左子树中
cur = cur.left;
} else if(cur.val < p.val && cur.val < q.val) { // 说明要找的节点在右子树中
cur = cur.right;
} else {
return cur;
}
}
return cur;
}
}
复杂度分析:
2.3 二叉树的最近公共祖先
题目链接:
题意:
- 与上题类似,只不过给定的结果是二叉树,不是带有顺序的二叉搜索树
题解:
-
整体解决思路:
- 二叉树进行深度先序遍历寻找p,q,当遇到节点 p或 q 时返回。
- 从底至顶回溯,当节点 p, q 在节点 root的异侧时,节点 root即为最近公共祖先,则向上返回 root 。
-
解题步骤:
- 1.递归终止条件是:遍历的节点为null或p,q均返回当前节点
- 2.依次遍历左,右子树,并记录返回的节点值
- 3.对左右子树返回的结果进行判断
- 如果左右子树返回结果均为null,说明此root节点子树中没有p,q,将null返回
- 如果左右子树有一个子树找到了,不为null,说明target在此子树中,将此子节点往上回溯
- 如果左右子树均不为null,说明p,q在两侧,此节点就是要找的target,将此节点往上回溯
// 二叉树的最近公共祖先
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// - 1.递归终止条件是:遍历的节点为null或p,q均返回当前节点
if(root == null || root == p || root == q) return root;
// - 2.依次遍历左,右子树,并记录返回的节点值
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// - 3.对左右子树返回的结果进行判断
// - 如果左右子树返回结果均为null,说明此root节点子树中没有p,q,将null返回
// - 如果左右子树有一个子树找到了,不为null,说明target在此子树中,将此子节点往上回溯
if(left == null) return right;
if(right == null) return left;
// - 如果左右子树均不为null,说明p,q在两侧,此节点就是要找的target,将此节点往上回溯
return root;
}
复杂度分析:
- 时间复杂度 O(N) : 其中 N 为二叉树节点数;最差情况下,需要递归遍历树的所有节点。
- 空间复杂度 O(N) : 最差情况下,递归深度达到 N ,系统使用 O(N) 大小的额外空间