Java数据结构——二叉树相关OJ题目详解

回顾:Java数据结构------二叉树(Binary Tree)详解

前面一篇文章已经整理过二叉树的基本概念、存储方式和几种遍历方式。

但是二叉树真正想学明白,不能只停留在概念上,还是要回到题目里。

因为很多二叉树题看起来不一样:

  • 有的是判断两棵树是否相同
  • 有的是判断一棵树是不是另一棵树的子树
  • 有的是翻转二叉树
  • 有的是构建二叉树
  • 有的是最近公共祖先

但真正写代码的时候,本质上都绕不开一个问题:

当前我站在这个节点上,我应该做什么?

只要能把当前节点的事情想清楚,再想明白左右子树要返回什么,很多递归题其实就顺了。

为了方便演示,下面统一使用这个节点结构:

java 复制代码
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

一、检查两棵树是否相同(LeetCode 100)

题目链接:100. 相同的树

核心:同步比较两棵树的当前位置,结构和值都要对上。

1. 这题怎么想

判断两棵树是否相同,不是只看根节点,也不是只看节点个数。

真正要判断的是:

text 复制代码
两棵树在每一个对应位置上,结构一样,节点值也一样。

所以递归时不能只拿一个节点,而是要同时拿到两个节点:pq

当前就站在 pq 这两个位置上,有下面几种情况:

  1. p == null && q == null

    • 两棵树在当前位置都没有节点。
    • 这个位置是对上的。
    • 返回 true
  2. p == null || q == null

    • 只有一边有节点。
    • 结构已经不一样。
    • 返回 false
  3. p.val != q.val

    • 结构暂时对上了,但节点值不同。
    • 返回 false
  4. 当前节点值相同

    • 当前这一层通过。
    • 继续比较左子树和右子树。

最后的递归关系就是:

text 复制代码
p.left  要和 q.left  相同
p.right 要和 q.right 相同

这两个条件必须同时成立,所以最后使用 &&

2. 代码实现

java 复制代码
public boolean isSameTree(TreeNode p, TreeNode q) {
    if (p == null && q == null) {
        return true;
    }

    if (p == null || q == null) {
        return false;
    }

    if (p.val != q.val) {
        return false;
    }

    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

3. 容易错点

这里函数返回的不是"当前两个节点是否相等",而是:

text 复制代码
以 p 和 q 为根的两棵子树是否完全相同。

所以最后必须是左右子树都相同,不能写成 ||

二、另一棵树的子树(LeetCode 572)

题目链接:572. 另一棵树的子树

核心:在主树里找一个起点,再判断从这个起点开始是否和 subRoot 完全相同。

1. 这题怎么想

这道题可以直接复用上一题的思想。

判断 subRoot 是否是 root 的子树,本质上不是找某几个相同的节点值,而是在 root 这棵大树里找一个节点:

text 复制代码
以这个节点为根的整棵子树 == subRoot

所以这里有两层事情:

  1. 在主树 root 中寻找可能的起点。
  2. 每到一个起点,就判断这棵子树是否和 subRoot 完全相同。

当前站在主树的某个节点 root 上:

  • 如果 root == null

    • 说明这条路径已经走到底。
    • 还没有找到匹配的子树。
    • 返回 false
  • 如果 root != null

    • 先把当前节点当成候选起点。
    • 调用 isSameTree(root, subRoot) 判断。

如果当前节点可以作为起点,整道题直接返回 true

如果当前节点不行,再去左子树和右子树继续找:

text 复制代码
左边找到可以,右边找到也可以。

所以这里用 ||

2. 代码实现

java 复制代码
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
    if (root == null) {
        return false;
    }

    if (isSameTree(root, subRoot)) {
        return true;
    }

    return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}

private boolean isSameTree(TreeNode p, TreeNode q) {
    if (p == null && q == null) {
        return true;
    }

    if (p == null || q == null) {
        return false;
    }

    if (p.val != q.val) {
        return false;
    }

    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}

3. 容易错点

当前节点值和 subRoot 根节点值相同,只能说明"有可能匹配"。

真正要判断的是从当前节点往下的整棵子树是否完全一致,所以必须调用"相同的树"那套判断逻辑。

三、翻转二叉树(LeetCode 226)

题目链接:226. 翻转二叉树

核心:每到一个节点,就交换它的左右子树。

1. 这题怎么想

翻转二叉树不是交换节点值,而是交换左右子树的引用。

当前站在节点 root 上,它要做的事情非常明确:

text 复制代码
root.left 和 root.right 交换

交换完当前节点以后,当前这一层就处理完了。

但是子树内部还没有翻转。

所以还要继续让 root.leftroot.right 做同样的事情。

递归过程可以理解成:

  1. 当前节点为空

    • 没有东西可以交换。
    • 返回 null
  2. 当前节点不为空

    • 交换左右子树。
    • 继续翻转左子树。
    • 继续翻转右子树。
    • 返回当前 root

最后返回 root 的意思是:

text 复制代码
以当前节点为根的这棵子树已经翻转完成。

2. 代码实现

java 复制代码
public TreeNode invertTree(TreeNode root) {
    if (root == null) {
        return null;
    }

    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;

    invertTree(root.left);
    invertTree(root.right);

    return root;
}

3. 容易错点

交换的是左右子树,不是左右孩子的值。

还有一点,交换完成后继续递归 root.leftroot.right 是没有问题的,因为它们只是位置变了,子树内部仍然需要继续翻转。

四、判断一棵二叉树是否是平衡二叉树(LeetCode 110)

题目链接:110. 平衡二叉树

核心:让每棵子树向上返回高度,如果已经不平衡,就返回 -1

1. 这题怎么想

平衡二叉树要求每个节点的左右子树高度差都不超过 1。

注意,是每个节点,不是只判断根节点。

如果从上往下判断,每到一个节点就重新计算左右高度,会重复计算很多次。

更自然的做法是从下往上判断。

当前站在节点 root 上,它需要先知道:

text 复制代码
左子树高度是多少?
右子树高度是多少?
左右高度差是否超过 1?

但是这里还要考虑一种情况:

如果左子树或者右子树本身已经不平衡,那么当前节点也没必要继续算高度了。

所以可以让 getHeight 返回两类信息:

返回值 含义
0、1、2... 当前子树的正常高度
-1 当前子树已经不平衡

当前处理 root 时:

  1. root == null

    • 空树高度为 0。
    • 返回 0
  2. 先看左子树

    • 如果左子树返回 -1,说明左边已经不平衡。
    • 当前节点直接返回 -1
  3. 再看右子树

    • 如果右子树返回 -1,说明右边已经不平衡。
    • 当前节点也直接返回 -1
  4. 左右子树都正常

    • 比较左右高度差。
    • 如果超过 1,返回 -1
    • 否则返回当前子树高度。

主函数只需要判断最终结果是不是 -1

2. 代码实现

java 复制代码
public boolean isBalanced(TreeNode root) {
    return getHeight(root) != -1;
}

private int getHeight(TreeNode root) {
    if (root == null) {
        return 0;
    }

    int leftHeight = getHeight(root.left);
    if (leftHeight == -1) {
        return -1;
    }

    int rightHeight = getHeight(root.right);
    if (rightHeight == -1) {
        return -1;
    }

    if (Math.abs(leftHeight - rightHeight) > 1) {
        return -1;
    }

    return Math.max(leftHeight, rightHeight) + 1;
}

3. 容易错点

getHeight 不是单纯求高度。

它一边求高度,一边判断平衡。

-1 是一个状态标记,表示"下面已经不平衡了",一旦出现就要直接往上传。

五、判断一棵树是否为对称二叉树(LeetCode 101)

题目链接:101. 对称二叉树

核心:对称不是"左子树等于右子树",而是左右子树互为镜像。

1. 这题怎么想

对称二叉树看的不是左右两棵树是否完全相同,而是它们是否互为镜像。

镜像关系是:

text 复制代码
左子树的左边  对应  右子树的右边
左子树的右边  对应  右子树的左边

所以这道题需要一个函数同时接收两个节点,比如 leftright

当前站在这两个节点上:

  1. 两个都为空

    • 两边同时走到底。
    • 结构仍然对称。
    • 返回 true
  2. 只有一个为空

    • 一边有节点,一边没有节点。
    • 结构不对称。
    • 返回 false
  3. 两个都不为空,但值不同

    • 内容不对称。
    • 返回 false
  4. 两个值相同

    • 当前层通过。
    • 继续比较外侧和内侧。

外侧和内侧分别是:

text 复制代码
left.left   和 right.right
left.right  和 right.left

这两组都对称,当前这两棵子树才算镜像。

2. 代码实现

java 复制代码
public boolean isSymmetric(TreeNode root) {
    if (root == null) {
        return true;
    }

    return isMirror(root.left, root.right);
}

private boolean isMirror(TreeNode left, TreeNode right) {
    if (left == null && right == null) {
        return true;
    }

    if (left == null || right == null) {
        return false;
    }

    if (left.val != right.val) {
        return false;
    }

    return isMirror(left.left, right.right) && isMirror(left.right, right.left);
}

3. 容易错点

最容易写错的是递归方向。

如果写成:

java 复制代码
isMirror(left.left, right.left)

那就变成比较两棵树是否相同了。

真正要比较的是外侧和外侧、内侧和内侧。

六、判断一棵树是否是完全二叉树

核心:层序遍历时,第一个空位置后面不能再出现非空节点。

1. 这题怎么想

完全二叉树要求节点从上到下、从左到右连续排列。

换句话说:

text 复制代码
在层序遍历顺序中,一旦出现空位置,后面就不应该再出现真实节点。

所以这道题适合用队列。

但它和普通层序遍历有一个区别:

普通层序遍历一般不会让 null 入队。

这道题必须让 null 入队。

因为 null 代表树结构里的空洞。

当前按层序顺序处理节点:

  1. 根节点为空

    • 空树可以认为是完全二叉树。
    • 返回 true
  2. 根节点不为空

    • 根节点入队。
    • 不断从队列中取节点。
  3. 取出的节点不是 null

    • 把它的左孩子入队。
    • 把它的右孩子入队。
    • 注意:左右孩子即使是 null 也要入队。
  4. 第一次取出 null

    • 说明层序结构中已经出现空位置。
    • 后面如果再出现非空节点,就不是完全二叉树。

2. 代码实现

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

public boolean isCompleteBinaryTree(TreeNode root) {
    if (root == null) {
        return true;
    }

    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);

    while (!queue.isEmpty()) {
        TreeNode cur = queue.poll();

        if (cur == null) {
            break;
        }

        queue.offer(cur.left);
        queue.offer(cur.right);
    }

    while (!queue.isEmpty()) {
        TreeNode cur = queue.poll();

        if (cur != null) {
            return false;
        }
    }

    return true;
}

3. 容易错点

这道题最关键的就是 null 也要入队。

如果不保存空位置,就无法判断"空洞后面是否还有节点"。

另外如果使用 ArrayDeque,它不允许存放 null,这里可以使用 LinkedList

七、二叉树构建及遍历

题目链接:TSINGK110 二叉树遍历

核心:输入如果是前序形式,构建时也要按"根、左、右"的顺序消耗字符。

1. 这题怎么想

这类题一般会给一串字符,用特殊字符表示空节点。

常见输入方式是前序:

text 复制代码
根 左子树 右子树

其中 # 表示当前位置为空。

所以构建树的时候,也应该按照前序顺序读取字符串。

当前正在构建一棵子树:

  1. 读取当前字符。

  2. 如果字符是 #

    • 当前这个位置没有节点。
    • 返回 null
    • 这个 null 会挂回上一层的 leftright
  3. 如果字符不是 #

    • 创建当前根节点。
    • 继续构建左子树。
    • 再继续构建右子树。
    • 返回当前根节点。

这里需要一个 index 记录字符串读到哪里。

因为左子树构建时会消耗一部分字符,左子树结束以后,右子树必须从新的位置继续读。

2. 代码实现

java 复制代码
public class Main {
    private static int index = 0;

    static class TreeNode {
        char val;
        TreeNode left;
        TreeNode right;

        TreeNode(char val) {
            this.val = val;
        }
    }

    public static TreeNode buildTree(String str) {
        if (index >= str.length()) {
            return null;
        }

        char ch = str.charAt(index++);

        if (ch == '#') {
            return null;
        }

        TreeNode root = new TreeNode(ch);
        root.left = buildTree(str);
        root.right = buildTree(str);

        return root;
    }

    public static void inorder(TreeNode root) {
        if (root == null) {
            return;
        }

        inorder(root.left);
        System.out.print(root.val + " ");
        inorder(root.right);
    }
}

3. 容易错点

# 不能直接忽略。

它虽然不是节点,但它表示一个空位置。

如果不处理 #,左右子树结构会乱掉。

八、最近公共祖先(LeetCode 236)

题目链接:236. 二叉树的最近公共祖先

核心:让左右子树向上汇报有没有找到 pq

1. 这题怎么想

最近公共祖先不一定要把两条路径都存下来。

更自然的方式是让递归从下往上返回信息。

当前站在节点 root 上,它只关心三件事:

text 复制代码
左子树有没有找到目标?
右子树有没有找到目标?
当前节点自己是不是目标?

返回值的含义非常重要:

返回值 含义
null 当前子树没有找到 pq
pq 当前子树找到了其中一个目标
某个祖先节点 最近公共祖先已经在下面确定

当前递归过程可以这样看:

  1. root == null

    • 当前路径没有目标。
    • 返回 null
  2. root == p || root == q

    • 当前节点就是目标之一。
    • 返回 root
  3. 当前节点不是目标

    • 去左子树找,结果记为 leftTree
    • 去右子树找,结果记为 rightTree
  4. 左右两边都不为空

    • 说明 pq 分别在当前节点两侧。
    • 当前节点就是最近公共祖先。
    • 返回 root
  5. 只有一边不为空

    • 说明目标或者答案在那一边。
    • 继续把那一边的结果向上传。

2. 代码实现

java 复制代码
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null) {
        return null;
    }

    if (root == p || root == q) {
        return root;
    }

    TreeNode leftTree = lowestCommonAncestor(root.left, p, q);
    TreeNode rightTree = lowestCommonAncestor(root.right, p, q);

    if (leftTree != null && rightTree != null) {
        return root;
    }

    if (leftTree != null) {
        return leftTree;
    }

    if (rightTree != null) {
        return rightTree;
    }

    return null;
}

3. 容易错点

当左右两边都不为空时,当前节点必须返回自己。

因为这说明两个目标节点已经在当前节点这里汇合。

如果只有一边不为空,不要强行返回当前节点,而是把那一边的结果继续向上传。

九、前序和中序构建二叉树(LeetCode 105)

题目链接:105. 从前序与中序遍历序列构造二叉树

核心:前序确定根,中序划分左右子树范围。

1. 这题怎么想

先看两个遍历顺序:

text 复制代码
前序:根 左 右
中序:左 根 右

前序数组中当前第一个还没使用的元素,一定是当前子树的根节点。

中序数组中,根节点左边是左子树,根节点右边是右子树。

所以这道题每一层都做同一件事:

text 复制代码
用前序拿根节点,用中序切左右范围。

假设当前正在构建中序范围 [inBegin, inEnd] 对应的子树:

  1. inBegin > inEnd

    • 当前范围没有节点。
    • 返回 null
  2. 从前序数组中取根节点

    • 使用 preIndex 从前往后取。
    • 创建当前根节点。
  3. 在中序数组中找到根节点位置 rootIndex

    • inBegin ~ rootIndex - 1 是左子树。
    • rootIndex + 1 ~ inEnd 是右子树。
  4. 先构建左子树,再构建右子树

    • 因为前序数组中根节点后面先出现左子树节点。

2. 代码实现

java 复制代码
public class Solution {
    private int preIndex = 0;

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder, inorder, 0, inorder.length - 1);
    }

    private TreeNode build(int[] preorder, int[] inorder, int inBegin, int inEnd) {
        if (inBegin > inEnd) {
            return null;
        }

        int rootVal = preorder[preIndex++];
        TreeNode root = new TreeNode(rootVal);

        int rootIndex = findVal(inorder, inBegin, inEnd, rootVal);

        root.left = build(preorder, inorder, inBegin, rootIndex - 1);
        root.right = build(preorder, inorder, rootIndex + 1, inEnd);

        return root;
    }

    private int findVal(int[] inorder, int begin, int end, int val) {
        for (int i = begin; i <= end; i++) {
            if (inorder[i] == val) {
                return i;
            }
        }

        return -1;
    }
}

3. 容易错点

这道题一定要维护中序范围。

每一层递归构建的是某一段中序范围对应的子树,不是一直对整棵树操作。

另外,前序加中序构建时,要先构建左子树,再构建右子树。

十、中序和后序构建二叉树(LeetCode 106)

题目链接:106. 从中序与后序遍历序列构造二叉树

核心:后序从后往前取根,中序继续划分范围。

1. 这题怎么想

先看遍历顺序:

text 复制代码
中序:左 根 右
后序:左 右 根

后序数组的最后一个元素,一定是当前子树的根节点。

拿到根节点以后,仍然可以去中序数组中找到它的位置,然后划分左右子树范围。

这道题和前序加中序很像,真正不同的是取根节点的方向。

前序是从前往后取根。

后序是从后往前取根。

当前构建中序范围 [inBegin, inEnd]

  1. 范围为空,返回 null
  2. 从后序数组中取当前根节点。
  3. 在中序数组中找到根节点位置。
  4. 根据中序位置划分左右子树。

这里最关键的是:

text 复制代码
后序从后往前读,顺序是:根 右 左

所以构建时必须先构建右子树,再构建左子树。

2. 代码实现

java 复制代码
public class Solution {
    private int postIndex;

    public TreeNode buildTree(int[] inorder, int[] postorder) {
        postIndex = postorder.length - 1;
        return build(inorder, postorder, 0, inorder.length - 1);
    }

    private TreeNode build(int[] inorder, int[] postorder, int inBegin, int inEnd) {
        if (inBegin > inEnd) {
            return null;
        }

        int rootVal = postorder[postIndex--];
        TreeNode root = new TreeNode(rootVal);

        int rootIndex = findVal(inorder, inBegin, inEnd, rootVal);

        root.right = build(inorder, postorder, rootIndex + 1, inEnd);
        root.left = build(inorder, postorder, inBegin, rootIndex - 1);

        return root;
    }

    private int findVal(int[] inorder, int begin, int end, int val) {
        for (int i = begin; i <= end; i++) {
            if (inorder[i] == val) {
                return i;
            }
        }

        return -1;
    }
}

3. 容易错点

中序加后序构建时,最容易错的是先构建左子树。

因为从后序数组尾部往前读,根节点后面先遇到的是右子树节点。

顺序写反,代码可能不报错,但构建出来的树是错的。

十一、二叉树前序遍历实现(LeetCode 144)

题目链接:144. 二叉树的前序遍历

核心:前序遍历的访问顺序是根、左、右。

1. 递归写法

前序遍历是:

text 复制代码
根 -> 左 -> 右

所以当前节点只要不为空,就先把当前节点加入结果,再去处理左子树和右子树。

java 复制代码
import java.util.LinkedList;
import java.util.List;

public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    if (root == null) {
        return ret;
    }

    ret.add(root.val);
    ret.addAll(preorderTraversal(root.left));
    ret.addAll(preorderTraversal(root.right));

    return ret;
}

2. 非递归写法

递归里,系统调用栈会帮我们保存回退路线。

非递归就需要自己使用栈。

前序要求根、左、右,但栈是后进先出。

所以想让左孩子先被处理,就要先压右孩子,再压左孩子。

java 复制代码
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

public List<Integer> preorderTraversalNor(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    if (root == null) {
        return ret;
    }

    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);

    while (!stack.isEmpty()) {
        TreeNode cur = stack.pop();
        ret.add(cur.val);

        if (cur.right != null) {
            stack.push(cur.right);
        }

        if (cur.left != null) {
            stack.push(cur.left);
        }
    }

    return ret;
}

3. 容易错点

非递归前序里,压栈顺序和访问顺序要反着想。

想要左先访问,就要让左后入栈。

十二、二叉树中序遍历实现(LeetCode 94)

题目链接:94. 二叉树的中序遍历

核心:中序遍历的访问顺序是左、根、右。

1. 递归写法

中序遍历是:

text 复制代码
左 -> 根 -> 右

所以当前节点不能一开始就访问。

必须先处理左子树,左子树结束以后再访问当前节点,最后处理右子树。

java 复制代码
import java.util.LinkedList;
import java.util.List;

public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    if (root == null) {
        return ret;
    }

    ret.addAll(inorderTraversal(root.left));
    ret.add(root.val);
    ret.addAll(inorderTraversal(root.right));

    return ret;
}

2. 非递归写法

中序非递归比前序绕一点。

因为遇到节点时不能马上访问,它的左子树还没处理。

正确过程是:

  1. 一路向左走。
  2. 沿途节点全部压栈。
  3. 左边走到底以后,从栈中弹出节点。
  4. 弹出的节点此时可以访问。
  5. 访问完后转向右子树。
java 复制代码
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

public List<Integer> inorderTraversalNor(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;

    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }

        TreeNode top = stack.pop();
        ret.add(top.val);
        cur = top.right;
    }

    return ret;
}

3. 容易错点

节点入栈不等于访问节点。

中序遍历中,只有节点从栈里弹出时,才说明它的左子树已经处理完,可以加入结果。

十三、二叉树后序遍历实现(LeetCode 145)

题目链接:145. 二叉树的后序遍历

核心:后序遍历的访问顺序是左、右、根。

1. 递归写法

后序遍历是:

text 复制代码
左 -> 右 -> 根

所以当前节点必须最后访问。

递归写法中,系统调用栈会自然帮我们等左子树和右子树都处理完,再回到当前节点。

java 复制代码
import java.util.LinkedList;
import java.util.List;

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    if (root == null) {
        return ret;
    }

    ret.addAll(postorderTraversal(root.left));
    ret.addAll(postorderTraversal(root.right));
    ret.add(root.val);

    return ret;
}

2. 非递归写法

后序非递归是三种非递归遍历里最容易写错的。

原因是当前节点不能在第一次遇到时访问。

它必须等左子树和右子树都处理完以后,才能加入结果。

递归中,系统调用栈会自动帮我们知道右子树是否已经处理完成。

非递归中,需要自己记录这个状态。

这里用 prev 表示上一个已经访问过的节点。

当前过程是:

  1. 先一路向左走,并把沿途节点压栈。

  2. 左边走到底后,查看栈顶节点 top

    • 注意是 peek,不是直接 pop
  3. 判断 top 能不能访问:

    • top.right == null:没有右子树,可以访问。
    • top.right == prev:右子树刚刚访问过,也可以访问。
  4. 如果可以访问:

    • top.val 加入结果。
    • 更新 prev = top
    • 弹出栈顶节点。
  5. 如果不能访问:

    • 说明右子树还没处理。
    • cur = top.right,进入右子树。
java 复制代码
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

public List<Integer> postorderTraversalNor(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    Stack<TreeNode> stack = new Stack<>();
    TreeNode cur = root;
    TreeNode prev = null;

    while (cur != null || !stack.isEmpty()) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }

        TreeNode top = stack.peek();

        if (top.right == null || top.right == prev) {
            ret.add(top.val);
            prev = top;
            stack.pop();
        } else {
            cur = top.right;
        }
    }

    return ret;
}

3. 容易错点

最关键的判断是:

java 复制代码
top.right == null || top.right == prev

它表示当前节点的右子树已经不需要再处理。

另外不能一开始就 pop,因为右子树没处理完时,当前节点还需要留在栈里等回退。

十四、二叉树的层序遍历(LeetCode 102)

题目链接:102. 二叉树的层序遍历

核心:队列负责从上到下访问,count 负责隔开每一层。

1. 这题怎么想

层序遍历要求从上到下、从左到右访问节点。

队列天然适合这个顺序。

但是这道题要求按层返回结果,所以还要知道当前层有多少个节点。

每一层开始之前,先记录:

java 复制代码
int count = queue.size();

此刻队列中的节点,刚好就是当前层的所有节点。

接下来只处理 count 个节点。

每取出一个节点,就把它加入当前层列表,同时把它的左右孩子放入队列。

这些新加入的孩子属于下一层。

因为当前层只处理固定数量的节点,所以不会和下一层混在一起。

2. 代码实现

java 复制代码
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ret = new LinkedList<>();

    if (root == null) {
        return ret;
    }

    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);

    while (!queue.isEmpty()) {
        int count = queue.size();
        List<Integer> list = new LinkedList<>();

        while (count != 0) {
            TreeNode cur = queue.poll();
            count--;

            list.add(cur.val);

            if (cur.left != null) {
                queue.offer(cur.left);
            }

            if (cur.right != null) {
                queue.offer(cur.right);
            }
        }

        ret.add(list);
    }

    return ret;
}

3. 容易错点

queue.size() 必须在每一层开始之前记录。

如果处理过程中一直用队列当前大小,下一层节点会不断入队,当前层和下一层就容易混在一起。

十五、根据二叉树创建字符串(LeetCode 606)

题目链接:606. 根据二叉树创建字符串

核心:前序遍历写节点值,同时用括号保留必要的左右结构。

1. 这题怎么想

这道题本质上是前序遍历。

因为字符串要先写当前节点,再写左子树,最后写右子树。

麻烦的地方不在遍历,而在括号什么时候可以省略。

当前站在节点 root 上:

  1. root == null

    • 当前没有节点可以写。
    • 直接返回。
  2. root != null

    • 先写 root.val
  3. 左子树存在

    • 用一对括号包住左子树结果。
  4. 左子树不存在,右子树也不存在

    • 当前是叶子节点。
    • 不需要继续写括号。
  5. 左子树不存在,但右子树存在

    • 必须写 ()
    • 这个空括号表示左子树为空。
  6. 右子树存在

    • 用一对括号包住右子树结果。

为什么左子树为空但右子树存在时必须补 ()

看这个结构:

text 复制代码
    1
     \
      2

如果写成:

text 复制代码
1(2)

那就无法区分 2 是左孩子还是右孩子。

所以必须写成:

text 复制代码
1()(2)

2. 代码实现

java 复制代码
public String tree2str(TreeNode root) {
    StringBuilder sb = new StringBuilder();
    build(root, sb);
    return sb.toString();
}

private void build(TreeNode root, StringBuilder sb) {
    if (root == null) {
        return;
    }

    sb.append(root.val);

    if (root.left != null) {
        sb.append("(");
        build(root.left, sb);
        sb.append(")");
    } else {
        if (root.right == null) {
            return;
        } else {
            sb.append("()");
        }
    }

    if (root.right != null) {
        sb.append("(");
        build(root.right, sb);
        sb.append(")");
    }
}

3. 容易错点

唯一特别需要注意的就是:

text 复制代码
没有左孩子,但有右孩子时,必须补空括号。

其他空括号能省略就省略,但这个位置不能省。

十六、二叉搜索树与双向链表(NowCoder BM30)

题目链接:BM30 二叉搜索树与双向链表

核心:利用二叉搜索树中序遍历有序的性质,把访问顺序变成链表顺序。

1. 这题怎么想

二叉搜索树有一个非常重要的性质:

text 复制代码
中序遍历会按照从小到大的顺序访问每个节点。

所以如果在中序遍历过程中,把当前节点和上一个访问过的节点连接起来,就可以得到一条有序双向链表。

这里最关键的变量是 prev

prev 表示上一个刚刚访问过的节点。

当前中序遍历来到节点 root

  1. 先处理左子树

    • 左子树中的节点都比当前节点小。
    • 它们应该排在链表前面。
  2. 左子树处理完后,连接当前节点

    • 此时 prev 就是当前节点前面的那个节点。
  3. 建立双向关系

    java 复制代码
    root.left = prev;

    如果 prev != null,再执行:

    java 复制代码
    prev.right = root;
  4. 更新 prev

    java 复制代码
    prev = root;

    这样下一个访问到的节点,就会接在当前节点后面。

  5. 最后处理右子树

    • 右子树中的节点比当前节点大。
    • 应该继续接到链表后面。

整棵树连接完后,原来的根节点不一定是链表头。

链表头应该是最小的节点,所以最后还要从原根节点一路向左,找到头节点返回。

2. 代码实现

java 复制代码
public class Solution {
    private TreeNode prev;

    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null) {
            return null;
        }

        inorderConnect(pRootOfTree);

        TreeNode head = pRootOfTree;
        while (head.left != null) {
            head = head.left;
        }

        return head;
    }

    private void inorderConnect(TreeNode root) {
        if (root == null) {
            return;
        }

        inorderConnect(root.left);

        root.left = prev;
        if (prev != null) {
            prev.right = root;
        }

        prev = root;

        inorderConnect(root.right);
    }
}

3. 容易错点

prev 不能定义在递归函数内部。

因为每一层递归都要共享"上一个访问过的节点"这个状态。

另外,这段代码转换出来的是普通双向链表,不是循环双向链表。

如果题目要求循环双向链表,还需要额外把头节点和尾节点连起来。

end*

相关推荐
微风欲寻竹影1 小时前
Java数据结构——二叉树(Binary Tree)详解
java·数据结构·算法
奋斗的小方1 小时前
Java进阶篇1-2:泛型
java·开发语言·windows
码语智行1 小时前
Codex 新手安装教程(完全小白版)
java·人工智能
z落落1 小时前
C# 多接口实现、重名成员、显式实现、接口继承+抽象类和接口区别
java·开发语言·c#
悠仁さん1 小时前
数据结构 排序
数据结构·算法·排序算法
C137的本贾尼1 小时前
【实战】分析一张真实业务表的 InnoDB 存储结构
java·大数据·数据库
超梦dasgg1 小时前
亿级数据 不停服务平滑迁移(生产环境实战方案)
java·数据库
Zella折耳根1 小时前
Java 正则表达式实战:IP 地址匹配与替换全解析
java·tcp/ip·正则表达式
摇滚侠1 小时前
JavaWeb 全套教程 Filter 107-111
java·开发语言·servlet