二叉树
二叉树是这么一种树状结构:每个节点最多有两个孩子,左孩子和右孩子
重要的二叉树结构
- 完全二叉树(complete binary tree)是一种二叉树结构,除最后一层以外,每一层都必须填满,填充时要遵从先左后右
- 平衡二叉树(balance binary tree)是一种二叉树结构,其中每个节点的左右子树高度相差不超过 1
存储
存储方式分为两种
- 定义树节点与左、右孩子引用(TreeNode)
- 使用数组,前面讲堆时用过,若以 0 作为树的根,索引可以通过如下方式计算
- 父 = floor((子 - 1) / 2)
- 左孩子 = 父 * 2 + 1
- 右孩子 = 父 * 2 + 2
遍历
遍历也分为两种
- 广度优先遍历(Breadth-first order):尽可能先访问距离根最近的节点,也称为层序遍历
- 深度优先遍历(Depth-first order):对于二叉树,可以进一步分成三种(要深入到叶子节点)
- pre-order 前序遍历,对于每一棵子树,先访问该节点,然后是左子树,最后是右子树
- in-order 中序遍历,对于每一棵子树,先访问左子树,然后是该节点,最后是右子树
- post-order 后序遍历,对于每一棵子树,先访问左子树,然后是右子树,最后是该节点
广度优先

本轮开始时队列 |
本轮访问节点 |
[1] |
1 |
[2, 3] |
2 |
[3, 4] |
3 |
[4, 5, 6] |
4 |
[5, 6] |
5 |
[6, 7, 8] |
6 |
[7, 8] |
7 |
[8] |
8 |
[] |
|
- 初始化,将根节点加入队列
- 循环处理队列中每个节点,直至队列为空
- 每次循环内处理节点后,将它的孩子节点(即下一层的节点)加入队列
注意
深度优先

栈暂存 |
已处理 |
前序遍历 |
中序遍历 |
[1] |
1 ✔️ 左💤 右💤 |
1 |
|
[1, 2] |
2✔️ 左💤 右💤 1✔️ 左💤 右💤 |
2 |
|
[1, 2, 4] |
4✔️ 左✔️ 右✔️ 2✔️ 左💤 右💤 1✔️ 左💤 右💤 |
4 |
4 |
[1, 2] |
2✔️ 左✔️ 右✔️ 1✔️ 左💤 右💤 |
|
2 |
[1] |
1✔️ 左✔️ 右💤 |
|
1 |
[1, 3] |
3✔️ 左💤 右💤 1✔️ 左✔️ 右💤 |
3 |
|
[1, 3, 5] |
5✔️ 左✔️ 右✔️ 3✔️ 左💤 右💤 1✔️ 左✔️ 右💤 |
5 |
5 |
[1, 3] |
3✔️ 左✔️ 右💤 1✔️ 左✔️ 右💤 |
|
3 |
[1, 3, 6] |
6✔️ 左✔️ 右✔️ 3✔️ 左✔️ 右💤 1✔️ 左✔️ 右💤 |
6 |
6 |
[1, 3] |
3✔️ 左✔️ 右✔️ 1✔️ 左✔️ 右💤 |
|
|
[1] |
1✔️ 左✔️ 右✔️ |
|
|
[] |
|
|
|
递归实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
static void preOrder(TreeNode node) { if (node == null) { return; } System.out.print(node.val + "\t"); preOrder(node.left); preOrder(node.right); }
static void inOrder(TreeNode node) { if (node == null) { return; } inOrder(node.left); System.out.print(node.val + "\t"); inOrder(node.right); }
static void postOrder(TreeNode node) { if (node == null) { return; } postOrder(node.left); postOrder(node.right); System.out.print(node.val + "\t"); }
|
非递归实现
前序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| LinkedListStack<TreeNode> stack = new LinkedListStack<>(); TreeNode curr = root;
while (!stack.isEmpty() || curr != null) { if (curr != null) { stack.push(curr); System.out.println(curr); curr = curr.left; } else { TreeNode pop = stack.pop(); curr = pop.right; }
}
|
中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13
| LinkedListStack<TreeNode> stack = new LinkedListStack<>(); TreeNode curr = root;
while (!stack.isEmpty() || curr != null) { if (curr != null) { stack.push(curr); curr = curr.left; } else { TreeNode pop = stack.pop(); System.out.println(pop); curr = pop.right; } }
|
后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| LinkedListStack<TreeNode> stack = new LinkedListStack<>(); TreeNode curr = root; TreeNode pop = null;
while (!stack.isEmpty() || curr != null) { if (curr != null) { stack.push(curr); curr = curr.left; } else { TreeNode peek = stack.peek(); if (peek.right == null || peek.right == pop) { pop = stack.pop(); System.out.println(pop); } else { curr = peek.right; } } }
|
对于后序遍历,向回走时,需要处理完右子树才能 pop 出栈。如何知道右子树处理完成呢?
对于前、中两种遍历,实际以上代码从右子树向回走时,并未走完全程(stack 提前出栈了)后序遍历以上代码是走完全程了
统一写法
下面是一种统一的写法,依据后序遍历修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode curr = root; TreeNode pop = null; while (curr != null || !stack.isEmpty()) { if (curr != null) { colorPrintln("前: " + curr.val, 31); stack.push(curr); curr = curr.left; } else { TreeNode peek = stack.peek(); if (peek.right == null) { colorPrintln("中: " + peek.val, 36); pop = stack.pop(); colorPrintln("后: " + pop.val, 34); } else if (peek.right == pop) { pop = stack.pop(); colorPrintln("后: " + pop.val, 34); } else { colorPrintln("中: " + peek.val, 36); curr = peek.right; } } }
public static void colorPrintln(String origin, int color) { System.out.printf("\033[%dm%s\033[0m%n", color, origin); }
|
一张图演示三种遍历

- 红色:前序遍历顺序
- 绿色:中序遍历顺序
- 蓝色:后续遍历顺序
习题
E01. 前序遍历二叉树-Leetcode 144
E02. 中序遍历二叉树-Leetcode 94
E03. 后序遍历二叉树-Leetcode 145
E04. 对称二叉树-Leetcode 101
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public boolean isSymmetric(TreeNode root) { return check(root.left, root.right); }
public boolean check(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 check(left.left, right.right) && check(left.right, right.left); }
|
类似题目:Leetcode 100 题 - 相同的树
E05. 二叉树最大深度-Leetcode 104
后序遍历求解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
public int maxDepth(TreeNode node) { if (node == null) { return 0; } int d1 = maxDepth(node.left); int d2 = maxDepth(node.right); return Integer.max(d1, d2) + 1; }
|
后序遍历求解-非递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
public int maxDepth(TreeNode root) { TreeNode curr = root; LinkedList<TreeNode> stack = new LinkedList<>(); int max = 0; TreeNode pop = null; while (curr != null || !stack.isEmpty()) { if (curr != null) { stack.push(curr); int size = stack.size(); if (size > max) { max = size; } curr = curr.left; } else { TreeNode peek = stack.peek(); if(peek.right == null || peek.right == pop) { pop = stack.pop(); } else { curr = peek.right; } } } return max; }
|
层序遍历求解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
public int maxDepth(TreeNode root) { if(root == null) { return 0; } Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); int level = 0; while (!queue.isEmpty()) { level++; int size = queue.size(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } } return level; }
|
E06. 二叉树最小深度-Leetcode 111
后序遍历求解
1 2 3 4 5 6 7 8 9 10 11
| public int minDepth(TreeNode node) { if (node == null) { return 0; } int d1 = minDepth(node.left); int d2 = minDepth(node.right); if (d1 == 0 || d2 == 0) { return d1 + d2 + 1; } return Integer.min(d1, d2) + 1; }
|
相较于求最大深度,应当考虑:
- 当右子树为 null,应当返回左子树深度加一
- 当左子树为 null,应当返回右子树深度加一
上面两种情况满足时,不应该再把为 null 子树的深度 0 参与最小值比较,例如这样
- 正确深度为 2,若把为 null 的右子树的深度 0 考虑进来,会得到错误结果 1
- 正确深度为 3,若把为 null 的左子树的深度 0 考虑进来,会得到错误结果 1
层序遍历求解
遇到的第一个叶子节点所在层就是最小深度
例如,下面的树遇到的第一个叶子节点 3 所在的层就是最小深度,其他 4,7 等叶子节点深度更深,也更晚遇到
1 2 3 4 5 6 7
| 1 / \ 2 3 / \ 4 5 / 7
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public int minDepth(TreeNode root) { if(root == null) { return 0; } Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); int level = 0; while (!queue.isEmpty()) { level++; int size = queue.size(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); if (node.left == null && node.right == null) { return level; } if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } } return level; }
|
效率会高于之前后序遍历解法,因为找到第一个叶子节点后,就无需后续的层序遍历了
E07. 翻转二叉树-Leetcode 226
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public TreeNode invertTree(TreeNode root) { fn(root); return root; }
private void fn(TreeNode node){ if (node == null) { return; } TreeNode t = node.left; node.left = node.right; node.right = t; fn(node.left); fn(node.right); }
|
先交换、再递归或是先递归、再交换都可以
E08. 后缀表达式转二叉树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| static class TreeNode { public String val; public TreeNode left; public TreeNode right;
public TreeNode(String val) { this.val = val; }
public TreeNode(TreeNode left, String val, TreeNode right) { this.left = left; this.val = val; this.right = right; }
@Override public String toString() { return this.val; } }
public TreeNode constructExpressionTree(String[] tokens) { LinkedList<TreeNode> stack = new LinkedList<>(); for (String t : tokens) { switch (t) { case "+", "-", "*", "/" -> { TreeNode right = stack.pop(); TreeNode left = stack.pop(); TreeNode parent = new TreeNode(t); parent.left = left; parent.right = right; stack.push(parent); } default -> { stack.push(new TreeNode(t)); } } } return stack.peek(); }
|
E09. 根据前序与中序遍历结果构造二叉树-Leetcode 105
- 先通过前序遍历结果定位根节点
- 再结合中序遍历结果切分左右子树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| public class E09Leetcode105 {
public TreeNode buildTree(int[] preOrder, int[] inOrder) { if (preOrder.length == 0) { return null; } int rootValue = preOrder[0]; TreeNode root = new TreeNode(rootValue); for (int i = 0; i < inOrder.length; i++) { if (inOrder[i] == rootValue) { int[] inLeft = Arrays.copyOfRange(inOrder, 0, i); int[] inRight = Arrays.copyOfRange(inOrder, i + 1, inOrder.length);
int[] preLeft = Arrays.copyOfRange(preOrder, 1, i + 1); int[] preRight = Arrays.copyOfRange(preOrder, i + 1, inOrder.length);
root.left = buildTree(preLeft, inLeft); root.right = buildTree(preRight, inRight); break; } } return root; }
}
|
E10. 根据中序与后序遍历结果构造二叉树-Leetcode 106
- 先通过后序遍历结果定位根节点
- 再结合中序遍历结果切分左右子树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public TreeNode buildTree(int[] inOrder, int[] postOrder) { if (inOrder.length == 0) { return null; } int rootValue = postOrder[postOrder.length - 1]; TreeNode root = new TreeNode(rootValue); for (int i = 0; i < inOrder.length; i++) { if (inOrder[i] == rootValue) { int[] inLeft = Arrays.copyOfRange(inOrder, 0, i); int[] inRight = Arrays.copyOfRange(inOrder, i + 1, inOrder.length);
int[] postLeft = Arrays.copyOfRange(postOrder, 0, i); int[] postRight = Arrays.copyOfRange(postOrder, i, postOrder.length - 1);
root.left = buildTree(inLeft, postLeft); root.right = buildTree(inRight, postRight); break; } } return root; }
|
11.基础数据结构-堆
基础算法