文章目录
这是《Java 数据结构》第 8 讲的课堂笔记整理。课件讲得很细,我把它的顺序、例子、图都保留了下来,代码补全并且都跑通了。看的时候建议对着图看——二叉树这东西,画一遍比背十遍有用。
本节目标
- 掌握树的基本概念
- 掌握二叉树概念及特性
- 掌握二叉树的基本操作
- 完成二叉树相关的面试题练习
一、树型结构(了解)
1.1 概念
树是一种非线性的数据结构,它是由 n(n >= 0)个有限结点组成一个具有层次关系的集合。
把它叫做树,是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。

▲ 一棵"树":上面是树冠,下面是树根——数据结构里的树正好倒过来
它具有以下的特点:
- 有一个特殊的结点,称为根结点,根结点没有前驱结点;
- 除根结点外,其余结点被分成 M(M > 0) 个互不相交的集合 T1、T2、……、Tm,其中每一个集合 Ti (1 <= i <= m) 又是一棵与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有 0 个或多个后继;
- 树是递归定义的。

▲ 一棵树的结点图:A 是根,往下层层分叉
注意:树形结构中,子树之间不能有交集,否则就不是树形结构。课件里专门用"树与非树"做了对比:

▲ 三个图里哪些是树?答案都在打问号的地方
判断标准就三条:
- 子树是不相交的(左边第一张图里 C 和 D 被连起来了,所以不是树);
- 除了根结点外,每个结点有且仅有一个父结点(中间那张图 E、F 的父结点说不清,不是树);
- 一棵 N 个结点的树有 N-1 条边(右边那张图 D 有三个父结点,不是树)。
1.2 概念(重要)
这一节是"行话"集中营,后面的性质、代码、面试题全靠它。先看图:

▲ 术语全靠这张图,下面每条都对着它说
结点的度:一个结点含有子树的个数称为该结点的度;如上图,A 的度为 6。
树的度:一棵树中,所有结点度的最大值称为树的度;如上图,树的度为 6。
叶子结点或终端结点:度为 0 的结点称为叶结点;如上图,B、C、H、I…… 等结点为叶结点。
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;如上图,A 是 B 的父结点。
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;如上图,B 是 A 的孩子结点。
根结点:一棵树中,没有双亲结点的结点;如上图,A。
再往下还有几个,考试爱抠字眼,但不用背,知道意思就行:
| 术语 | 一句话解释 |
|---|---|
| 结点的层次 | 从根开始定义,根为第 1 层,根的子结点为第 2 层,以此类推 |
| 树的高度或深度 | 树中结点的最大层次 |
| 非终端结点 / 分支结点 | 度不为 0 的结点(D、E、F、G……) |
| 兄弟结点 | 具有相同父结点的结点(B、C 是兄弟) |
| 堂兄弟结点 | 双亲在同一层的结点互为堂兄弟(H、I) |
| 结点的祖先 | 从根到该结点所经分支上的所有结点(A 是所有结点的祖先) |
| 子孙 | 以某结点为根的子树中任一结点(所有结点都是 A 的子孙) |
| 森林 | 由 m(m >= 0)棵互不相交的树组成的集合 |
记法:度是往下连了几个,层次是从上数第几排,别混。
1.3 树的表示形式(了解)
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦。实际中树有很多种表示方式,如:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法等。
课件里重点讲了最常用的孩子兄弟表示法:
class Node {
int value; // 树中存储的数据
Node firstChild; // 第一个孩子引用
Node nextBrother; // 下一个兄弟引用
}

▲ 孩子兄弟表示法:每个结点只管"第一个孩子"和"下一个兄弟"
这张图很值得多看两眼——它把一棵多叉树拍平成了二叉的形状:左指针存第一个孩子,右指针存下一个兄弟。所以后面"树转二叉树"的口诀就是"左孩子右兄弟"。
1.4 树的应用
文件系统管理(目录和文件),就是树最典型的落地场景:

▲ Linux 的文件系统结构:一个根 / 底下挂着所有目录
除了文件系统,组织架构图、HTML 的 DOM 树、路由表、决策树,本质上都是树。
二、二叉树(重点)
2.1 概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空;
- 或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。

▲ 左边是整棵二叉树,右边把它拆成"根结点 + 左子树 + 右子树"
从上图可以看出:
- 二叉树不存在度大于 2 的结点;
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。
注意:对于任意的二叉树,都是由以下几种情况复合而成的:

▲ 二叉树的 5 种基本形态:空树、只有根、只有左子树、只有右子树、左右都在
这 5 种形态看着简单,其实是二叉树所有递归代码的"分类讨论表"——写递归时你只要想清楚"空"和"根 + 左 + 右"两种情况,剩下的交给递归就行。
另外,课件里有两张"大自然的奇观",挺有意思——大自然自己长出来的就是二叉树:

▲ 分枝树

▲ 这棵更夸张,主干上直接摊开成一整个伞状树冠
2.2 两种特殊的二叉树
1. 满二叉树:一棵二叉树,如果每层的结点数都达到最大值,则这棵二叉树就是满二叉树。也就是说,如果一棵二叉树的层数为 K,且结点总数是 2^K - 1,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,它是由满二叉树引出来的。对于深度为 K 的、有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 K 的满二叉树中编号从 0 至 n-1 的结点一一对应时,称之为完全二叉树。
要注意的是:满二叉树是一种特殊的完全二叉树。

▲ 左边满二叉树:每层都塞满;右边完全二叉树:只允许最后一层右边缺,中间不许空
为什么完全二叉树"效率很高"?因为它的结点可以用数组连续存,一个空位都不浪费,PriorityQueue(堆)就是靠这个性质活着的。
2.3 二叉树的性质
下面 5 条,前 4 条笔试必考,第 5 条写堆的时候天天用。
性质 1:若规定根结点的层数为 1,则一棵非空二叉树的第 i 层上最多有 2^(i-1) 个结点(i > 0)。
性质 2:若规定只有根结点的二叉树深度为 1,则深度为 K 的二叉树的最大结点数是 2^K - 1(K >= 1)。
性质 3:对任何一棵二叉树,如果其叶结点个数为 n0,度为 2 的非叶结点个数为 n2,则 n0 = n2 + 1。
性质 4:具有 n 个结点的完全二叉树的深度 k 为 ⌊log2 n⌋ + 1(等价于 ⌈log2(n+1)⌉)。
性质 5:对于具有 n 个结点的完全二叉树,如果按照从上至下、从左至右的顺序对所有结点从 0 开始编号,则对于序号为 i 的结点有:
- 若 i > 0,双亲序号:
(i-1)/2;i = 0 时 i 为根结点编号,无双亲结点; - 若
2i+1 < n,左孩子序号:2i+1,否则无左孩子; - 若
2i+2 < n,右孩子序号:2i+2,否则无右孩子。
性质 3 的证明,课上推过一次,自己再推一遍就忘不掉了:
设结点数 n、边数 e、度为 1 的结点数 n1。结点数
n = n0 + n1 + n2;边数一方面e = n - 1,另一方面每个度为 1 的结点贡献 1 条边、度为 2 的贡献 2 条边,e = n1 + 2n2。
联立:n0 + n1 + n2 - 1 = n1 + 2n2,得 n0 = n2 + 1。
来做 4 道题(答案是 1.B 2.A 3.B 4.B,先自己算再看解析):
题 1:某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树 B 200 C 198 D 199
由性质 3:n0 = n2 + 1 = 200,选 B。
再看它存不存在:n = n0 + n1 + n2 = 200 + n1 + 199 = 399,得 n1 = 0。结点总数是奇数时 n1 恰好为 0,合法。
题 2:在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n B n+1 C n-1 D n/2
完全二叉树里 n1 只能是 0 或 1。总结点数是 2n(偶数),所以 n1 = 1。
2n = n0 + 1 + (n0 - 1),得n0 = n,选 A。
题 3:一个具有 767 个结点的完全二叉树,其叶子结点个数为( )
A 383 B 384 C 385 D 386
767 是奇数,n1 = 0。由
n0 = n2 + 1和n0 + n2 = 767,解得 n2 = 383、n0 = 384,选 B。
题 4:一棵完全二叉树的结点数为 531 个,那么这棵树的高度为( )
A 11 B 10 C 8 D 12
性质 4 代入:
⌊log2 531⌋ + 1 = 9 + 1 = 10,选 B。
2.4 二叉树的存储
二叉树的存储结构分为:顺序存储和类似于链表的链式存储。顺序存储下一节(堆)再讲,这里重点看链式存储。
链式存储是通过一个一个的结点引用起来的,常见的表示方式有二叉和三叉两种:
// 孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用
Node right; // 右孩子的引用
Node parent; // 当前节点的根节点
}
孩子双亲表示法后面在平衡树的位置会用到,本文统一采用孩子表示法来构建二叉树。
2.5 二叉树的基本操作
2.5.1 前置说明
在学习基本操作前,得先有一棵二叉树。为了降低学习成本,这里先手动快速搭一棵简单的树,等结构熟悉了,再回头看真正的建树方式。
public class BinaryTree {
static class BTNode {
BTNode left;
BTNode right;
int value;
BTNode(int value) {
this.value = value;
}
}
private BTNode root;
public void createBinaryTree() {
BTNode node1 = new BTNode(1);
BTNode node2 = new BTNode(2);
BTNode node3 = new BTNode(3);
BTNode node4 = new BTNode(4);
BTNode node5 = new BTNode(5);
BTNode node6 = new BTNode(6);
root = node1;
node1.left = node2;
node2.left = node3;
node1.right = node4;
node4.left = node5;
node4.right = node6;
}
}
搭出来就是 2.1 里那棵树:
1
/ \
2 4
/ / \
3 5 6
注意:上述代码并不是创建二叉树的"正规"方式,真正的建树后序会重点讲。另外课件里这段代码 6 行都写成了
BTNode node1 = ...,是排版笔误,变量名重复编译不过,我这里改成了 node1~node6。
再看基本操作前,回顾一下二叉树的概念,二叉树是:
- 空树;
- 非空:根节点、根节点的左子树、根节点的右子树组成的。
看出来了吧——二叉树的定义是递归式的,因此后续基本操作基本都是按照这个概念实现的。
2.5.2 二叉树的遍历
学习二叉树结构,最简单的方式就是遍历。
所谓遍历(Traversal),是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。访问结点所做的操作依赖于具体的应用问题(比如打印结点内容、结点内容加 1)。遍历是二叉树上最重要的操作之一,是二叉树上进行其它运算之基础。

▲ 遍历就是沿着红色虚线把每个结点"走一遍",Φ 表示空
在遍历二叉树时,如果没有进行某种约定,每个人都按照自己的方式遍历,结果就比较混乱;如果按照某种规则进行约定,则每个人对于同一棵树的遍历结果肯定是相同的。
如果 N 代表根节点,L 代表根节点的左子树,R 代表根节点的右子树,则根据遍历根节点的先后次序有以下遍历方式:
- NLR:前序遍历(Preorder Traversal,亦称先序遍历)——访问根结点 → 根的左子树 → 根的右子树;
- LNR:中序遍历(Inorder Traversal)——根的左子树 → 根节点 → 根的右子树;
- LRN:后序遍历(Postorder Traversal)——根的左子树 → 根的右子树 → 根节点。
// 前序遍历
void preOrder(Node root);
// 中序遍历
void inOrder(Node root);
// 后序遍历
void postOrder(Node root);
课件下面主要分析了前序递归遍历,中序与后序图解类似。递归到底是怎么"进去又出来"的,看这张图最清楚:

▲ 红色箭头是"递归"往下走,绿色箭头是"回溯"往回返,NULL 处返回
三段代码几乎一模一样,唯一区别就是那行访问语句放在第几行:
/** 前序:根 → 左 → 右 */
public void preOrder(BTNode root) {
if (root == null) return;
System.out.print(root.value + " "); // ① 访问根
preOrder(root.left); // ② 左子树
preOrder(root.right); // ③ 右子树
}
/** 中序:左 → 根 → 右 */
public void inOrder(BTNode root) {
if (root == null) return;
inOrder(root.left);
System.out.print(root.value + " ");
inOrder(root.right);
}
/** 后序:左 → 右 → 根 */
public void postOrder(BTNode root) {
if (root == null) return;
postOrder(root.left);
postOrder(root.right);
System.out.print(root.value + " ");
}
对上面那棵 1(2(3), 4(5,6)) 的树,结果是:
前序遍历结果:1 2 3 4 5 6
中序遍历结果:3 2 1 5 4 6
后序遍历结果:3 2 5 6 4 1
说个面试常被追问的点:递归写起来爽,但递归深度受栈限制。同一个遍历用非递归怎么写?
前序非递归:根先入栈,弹出即访问,然后先压右孩子、再压左孩子(保证左孩子先出栈)。
public void preOrderNoRec(BTNode root) {
if (root == null) return;
Deque<BTNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
BTNode cur = stack.pop();
System.out.print(cur.value + " ");
if (cur.right != null) stack.push(cur.right); // 先右后左
if (cur.left != null) stack.push(cur.left);
}
}
中序非递归:一路向左压栈,压到底就弹出一个访问,再转向它的右子树。
public void inOrderNoRec(BTNode root) {
Deque<BTNode> stack = new ArrayDeque<>();
BTNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) { // 一路向左
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
System.out.print(cur.value + " ");
cur = cur.right; // 转向右子树
}
}
后序非递归稍微绕一点,要多记一个"上一个访问过的结点",用来判断右子树是不是已经处理完了。
public void postOrderNoRec(BTNode root) {
Deque<BTNode> stack = new ArrayDeque<>();
BTNode cur = root, last = null;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
BTNode peek = stack.peek();
if (peek.right == null || peek.right == last) {
System.out.print(peek.value + " ");
last = stack.pop();
} else {
cur = peek.right;
}
}
}
2. 层序遍历
除了前序、中序、后序,还可以对二叉树进行层序遍历。
设二叉树的根节点所在层数为 1,层序遍历就是从根节点出发,首先访问第一层的根节点,然后从左到右访问第 2 层的节点,接着是第三层…… 自上而下、自左至右逐层访问,这就是层序遍历。

▲ 层序遍历的走法:A → B → C → D → E → F → G → H → I,一层走完再走下一层
思路很直白:用队列。
public void levelOrder(BTNode root) {
if (root == null) return;
Queue<BTNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
BTNode cur = queue.poll();
System.out.print(cur.value + " ");
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
}
如果要"按层分行"输出,就在循环里先用 queue.size() 把当前层一次性取完:
public List<List<Integer>> levelOrderByLevel(BTNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<BTNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> level = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
BTNode cur = queue.poll();
level.add(cur.value);
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
res.add(level);
}
return res;
}
一句话记住:深度优先用栈(递归),广度优先用队列。二叉树这章的题,八成都能往这两个模子上套。
练习 1:请根据以上三种遍历方式,写出下面这棵二叉树的三种遍历结果。

▲ 练习题用的二叉树
(按图中画法,H 是 E 的右孩子)
前序:A B D E H C F G
中序:D B E H A F C G
后序:D H E B F G C A
练习 2:四道选择题(参考答案:1.A 2.A 3.D 4.A)
1. 某完全二叉树按层次输出(同一层从左到右)的序列为 ABCDEFGH。该完全二叉树的前序序列为( )
A: ABDHECFG B: ABCDEFGH C: HDBEAFCG D: HDEBFGCA
层序正好对应完全二叉树的数组编号:A 是根,B、C 是第二层,D~G 是第三层,H 是第四层且是 E 的左孩子。
A / \ B C / \ / \ D E F G / H前序 A → B → D → H → E → C → F → G,选 A。
2. 二叉树的先序遍历和中序遍历如下:先序遍历:EFHIGJK;中序遍历:HFIEJKG。则二叉树根结点为( )
A: E B: F C: G D: H
先序的第一个结点一定是根,所以根是 E,选 A。
3. 设一棵二叉树的中序遍历序列:badce,后序遍历序列:bdeca,则二叉树前序遍历序列为( )
A: adbce B: decab C: debac D: abcde
后序的最后一个结点是根 → 根是
a。中序badce里a左边是b(左子树)、右边是dce(右子树)。右子树后序dec末位是c,所以c是右子树的根,d、e分别是它的左、右孩子。
树形为a(b, c(d, e)),前序a b c d e,选 D。
4. 某二叉树的后序遍历序列与中序遍历序列相同,均为 ABCDEF,则按层次输出(同一层从左到右)的序列为( )
A: FEDCBA B: CBAFED C: DEFCBA D: ABCDEF
后序最后一个是根 → 根是 F;中序里 F 也在最后,说明 A~E 全在 F 的左子树里。逐层递归下来,整棵树是一条只有左孩子的单链:F → E → D → C → B → A。
层序输出FEDCBA,选 A。顺带记两条规律:后序 = 中序 ⟹ 每个结点都没有右孩子;前序 = 中序 ⟹ 每个结点都没有左孩子。
2.5.3 二叉树的基本操作
这几个方法的签名,课件里都列出来了,基本就是"标准模板":
// 获取树中节点的个数
int size(Node root);
// 获取叶子节点的个数
int getLeafNodeCount(Node root);
// 子问题思路-求叶子结点个数
// 获取第K层节点的个数
int getKLevelNodeCount(Node root, int k);
// 获取二叉树的高度
int getHeight(Node root);
// 检测值为value的元素是否存在
Node find(Node root, int val);
// 层序遍历
void levelOrder(Node root);
// 判断一棵树是不是完全二叉树
boolean isCompleteTree(Node root);
实现全是"大问题 = 根的处理 + 左子树的答案 + 右子树的答案":
① 结点个数
public int size(BTNode root) {
if (root == null) return 0;
return 1 + size(root.left) + size(root.right);
}
② 叶子结点个数
public int getLeafNodeCount(BTNode root) {
if (root == null) return 0;
if (root.left == null && root.right == null) return 1; // 左右都空 → 叶子
return getLeafNodeCount(root.left) + getLeafNodeCount(root.right);
}
③ 第 K 层结点个数
public int getKLevelNodeCount(BTNode root, int k) {
if (root == null || k < 1) return 0;
if (k == 1) return 1; // 第 1 层只有根
return getKLevelNodeCount(root.left, k - 1)
+ getKLevelNodeCount(root.right, k - 1);
}
④ 树的高度
public int getHeight(BTNode root) {
if (root == null) return 0;
return Math.max(getHeight(root.left), getHeight(root.right)) + 1;
}
⑤ 查找值为 value 的结点
public BTNode find(BTNode root, int val) {
if (root == null) return null;
if (root.value == val) return root;
BTNode leftRes = find(root.left, val);
return leftRes != null ? leftRes : find(root.right, val); // 左边找到就不找右边
}
⑥ 判断一棵树是不是完全二叉树
思路:层序遍历时把空孩子也入队。一旦弹出空结点,说明"后面必须全是空结点";若之后又冒出非空结点,就不是完全二叉树。
public boolean isCompleteTree(BTNode root) {
if (root == null) return true;
Queue<BTNode> queue = new LinkedList<>();
queue.offer(root);
boolean mustBeLeaf = false;
while (!queue.isEmpty()) {
BTNode cur = queue.poll();
if (cur == null) {
mustBeLeaf = true;
continue;
}
if (mustBeLeaf) return false; // 空结点之后又出现了非空结点
queue.offer(cur.left);
queue.offer(cur.right);
}
return true;
}
这段代码的前提是
LinkedList允许offer(null)。换成ArrayDeque会直接抛NullPointerException——这是实际编码时最常踩的坑。
2.6 二叉树相关 OJ 题
课件最后留了 14 道题,基本覆盖了二叉树所有考点。我按"由易到难"重排了一下,每题给一句核心思路,链接可以在 优快云 题库/牛客搜同题名:
| # | 题目 | 核心思路 |
|---|---|---|
| 1 | 检查两棵树是否相同 | 同时递归,值不等或结构不等即 false |
| 2 | 另一棵树的子树 | 对每个结点做一次"是否相同"的判断 |
| 3 | 翻转二叉树 | 交换左右孩子再递归 |
| 4 | 判断是否是平衡二叉树 | 求高度时顺便判断,用 -1 剪枝避免 O(n²) |
| 5 | 对称二叉树 | 比较 left.left 与 right.right、left.right 与 right.left |
| 6 | 二叉树的构建及遍历 | 读入含空标记的前序串递归建树,再中序输出 |
| 7 | 二叉树的分层遍历 | 队列 + size() 分层 |
| 8 | 二叉树的最近公共祖先 | 后序递归:左右各找到一个就返回根 |
| 9 | 前序 + 中序构造二叉树 | 前序定根,中序切区间,HashMap 定位下标 |
| 10 | 中序 + 后序构造二叉树 | 后序定根,其余同上 |
| 11 | 二叉树创建字符串 | 左空右不空时必须补 () |
| 12 | 前序非递归遍历 | 栈,先压右再压左 |
| 13 | 中序非递归遍历 | 一路向左压栈,弹出后转右 |
| 14 | 后序非递归遍历 | 记录 last,或双栈反转 |
三、小结
把这一讲压缩成几句话:
- 树是递归定义的层次结构,子树之间不能有交集;
- 二叉树度 <= 2 且有序,
n0 = n2 + 1必须会推; - 满二叉树结点数
2^K - 1;完全二叉树深度⌊log2 n⌋ + 1,且n1只能是 0 或 1; - 四种遍历:前/中/后序靠递归或栈,层序靠队列;
- 基本操作全是"根 + 左子树答案 + 右子树答案",写不出递归时先问自己"这棵树的子问题是什么"。
下一讲是二叉树的顺序存储和堆——完全二叉树真正发光的地方。
附:示例代码跑出来的完整输出
前序:1 2 3 4 5 6
中序:3 2 1 5 4 6
后序:3 2 5 6 4 1
层序:1 2 4 3 5 6
结点数:6
叶子数:3
第 2 层结点数:2
高度:3
如果这篇笔记帮你把二叉树串起来了,点个赞再走;有讲错的地方欢迎在评论区指出来。
本文系统讲解二叉树的基本概念、性质(如n0=n2+1、完全二叉树深度公式)、两种特殊类型(满/完全二叉树),重点剖析前序/中序/后序/层序四种遍历方式的递归与非递归实现,涵盖结点计数、叶子统计、K层节点数、高度计算等基本操作,并结合典型OJ题强化算法思维。所有内容基于Java实现,紧扣面试与笔试高频考点。
379

被折叠的 条评论
为什么被折叠?



