二叉树的遍历整理
契机:145. 二叉树的后序遍历题,用递归解决很简单,并且有统一的模板,但是用迭代方法就没有统一的模板,因此想总结一下4类遍历的解法。方便复习
前序遍历:1-2-4-5-8-3-6-9-7-10
中序遍历:4-2-8-5-1-6-9-3-10-7
后序遍历:4-8-5-2-9-6-10-7-3-1
1. 前序遍历
前序遍历:根 – 左 – 右
递归解法:
/**
* Definition for a binary tree node.
* 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;
* }
* }
*/
class Solution {
private void dfs(TreeNode root, List<Integer> res){
if(root == null) return;
res.add(root.val);
dfs(root.left, res);
dfs(root.right, res);
}
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res; // 排除特殊情况
dfs(root, res);
return res;
}
}
迭代解法:
很明显,我们需要用到栈来模拟整个递归过程。栈顶指针就是我们当前循环的根节点,取出之后需要获得其左右结点,并且将非null的左右结点压入栈,由于栈是后进先出,所以需要先压右孩子、后压左孩子
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res; // 排除特殊情况
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur = stack.pop();
res.add(cur.val);
if(cur.right != null) stack.push(cur.right);
if(cur.left != null) stack.push(cur.left);
}
return res;
}
}
——前序遍历的迭代解法的关键:先压右孩子后压左孩子
2. 中序遍历
中序遍历的顺序:左 - 根 - 右
递归解法:
class Solution {
private void dfs(TreeNode root, List<Integer> res){
if(root == null) return;
dfs(root.left, res);
res.add(root.val);
dfs(root.right, res);
}
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res; // 排除特殊情况
dfs(root, res);
return res;
}
}
可以发现和前序遍历的递归解法是统一的,只需要修改root的记录顺序即可
迭代解法:
实践发现,不能将前序遍历的迭代解法修改得到。原因是:前序遍历中,当前结点就是我们找的结点,可以将其从栈中弹出,并将左右孩子压入栈;但是中序遍历:当前节点需要在遍历完左孩子后才能从栈中弹出,所以当前结点获取之后不能出栈,而是要压左右孩子进栈,但是再次遍历到该结点的时候并不知道其已经遍历过了,所以会重复循环。
根据题解的提示:在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素
中序遍历:找最左结点,然后向上返回,然后找最近结点的右子树的最左,然后再向上,依次类推:
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res;
LinkedList<TreeNode> stack = new LinkedList<>();
while(!stack.isEmpty() || root != null){
while(root != null){ // 一直向左走,找到最左结点,期间碰到的所有结点均入栈
stack.push(root);
root = root.left;
} // 此时已经到底了,此时指向的是最左结点的左null,此时的栈顶就是最左结点
root = stack.pop(); // 最左结点出栈,然后去找该结点的右节点(如果该右节点不为null,则找该右节点的左孩子则再次循环入栈....)
res.add(root.val);
root = root.right;
}
return res;
}
}
——迭代的思想:循环找到最左结点,并且将路径上所有的结点均入栈(就是根结点),直到找到null。然后出栈,此时结点就是最左结点(其左子树已经遍历完成了,null),然后去看其右子树,然后对右子树再次循环入栈左孩子…,如果右子树不存在,即右节点为null,那么去找最近还未遍历的根节点(此时的栈顶)
3. 后序遍历
递归解法:
class Solution {
private void dfs(TreeNode root, List<Integer> res){
if(root == null) return;
dfs(root.left, res);
dfs(root.right, res);
res.add(root.val);
}
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res; // 排除特殊情况
dfs(root, res);
return res;
}
}
迭代解法:
题解提供了一个很巧妙的方法
从前序遍历开始考虑:中左右 – 中右左 – 左右中,从第一步到第二步就是代码将左右压栈的顺序变换。第二步到第三步就是将结果翻转一下即可
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if(root == null) return res; // 排除特殊情况
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode cur = stack.pop();
res.add(cur.val);
if(cur.left != null) stack.push(cur.left); // 压栈顺序相反
if(cur.right != null) stack.push(cur.right);
}
Collections.reverse(res); // 全部翻转
return res;
}
}
——迭代解法:前序遍历和后序遍历可以统一,稍微修改一下就可以统一,并且完全根据栈的顺序来遍历
中序遍历稍微复杂一点,需要用一个指针来指示当前遍历的节点,主题思想是要找到最左结点然后再返回
4. 层序遍历
思想是:BFS,对每一层进行遍历,然后在遍历的过程中将其压入队列,先入先出
很多题都是基于层序遍历来实现的,所以记忆比较深:
/* TreeNode类,是树节点的属性和函数 */
public class TreeNode {
int val; // 节点的值
TreeNode left; // 左孩子节点
TreeNode right; // 右孩子节点
TreeNode(int x) { val = x; }
}
public void levelOrder(TreeNode root){
if(root == null) return null; // 处理特殊情况
LinkedList<TreeNode> queue = new LinkedList<TreeNode>(); // 存放当前层级的节点
queue.offer(root); // 将根节点压入栈
int depth = 0; // 记录树的深度
while(!queue.isEmpty()){ // 开始迭代——当前层存在需要遍历的节点
int size = queue.size();
while(size > 0){ // 遍历当前层的所有节点
TreeNode current = queue.poll(); // 取出队首元素
if(current.left != null) queue.offer(current.left); // 将当前节点的左右孩子节点均加入到队尾
if(current.right != null) queue.offer(current.right);
size--;
}
depth++;
}
}
ps:根据两个遍历来构造整棵二叉树
剑指 Offer 07. 重建二叉树
很惭愧的是,之前刷题的时候解出来了,而再次做这题的时候又犯了老错误,只构建了框架,而边界判断错误了。
首先,根据前序遍历和中序遍历的特点:
- 前序遍历,第一个结点就是根节点;
- 中序遍历,找到这个根节点(index),根结点的左右就是对应的左右子树;
- 而根据该index,可以知道前序遍历的左子树的长度,而剩余的就是右子树的长度。
——注意,这边不能根据index,而直接推到prevorder的index,而是要根据in_left 和index计算出左子树的长度,而确定前序遍历的左右子树的分界线
eg:[1,2,3],[3,2,1]就无法推出正确的
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private int findIndex(int[]inorder, int target){ // 找到根节点target在中序遍历的位置
for(int i = 0; i < inorder.length; i++){
if(target == inorder[i]) return i;
}
return -1;
}
private TreeNode dfs(int[] preorder, int[] inorder, int pre_left, int pre_right, int in_left, int in_right){
if(pre_left > pre_right){
return null;
}
TreeNode root = new TreeNode(preorder[pre_left]); // 前序遍历的第一个结点就是根节点
int index = findIndex(inorder, preorder[pre_left]); // 找到中序遍历中的根节点的index,就是分界线
int left_length = index - in_left; // 获得左子树的长度
root.left = dfs(preorder, inorder, pre_left + 1, pre_left + left_length, in_left, index - 1);
root.right = dfs(preorder, inorder, pre_left + left_length + 1, pre_right, index + 1, in_right);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
return dfs(preorder, inorder, 0, preorder.length - 1, 0, inorder.length - 1);
}
}
改进:可以将inOrder的数字存放在哈希表中,从而避免每次都需要查找,key=inOrder[i],value=i
参考:
- https://leetcode-cn.com/problems/binary-tree-postorder-traversal/solution/bang-ni-dui-er-cha-shu-bu-zai-mi-mang-che-di-chi-t/
- https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by–22/