在数据结构的世界里,线性结构(如数组、链表)虽基础,但面对层级关系的数据(如文件系统、组织架构)时却力不从心。而树作为典型的非线性结构,恰好解决了这一痛点,其中二叉树更是因结构简洁、操作高效,成为树结构中的 “明星”—— 从算法面试到实际开发(如数据库索引、表达式解析),处处都有它的身影。
一、什么是树形结构?
在学二叉树前,我们得先明白 “树” 的本质 —— 它是由n(n≥0)个有限节点组成的层次关系集合,像一棵倒挂的树:根朝上,叶朝下。
1.1 树的 3 个核心特点
- 有且仅有一个根节点:根是树的 “起点”,没有前驱节点(比如文件系统的根目录
/)。 - 子树互不相交:任意两个子树之间没有公共节点,否则就不是合法的树(比如两个文件夹不能互相包含)。
- 递归定义:除根外,其余节点被分成
M(M>0)个互不相交的子树,每个子树也是一棵独立的树。
1.2 必须掌握的树结构术语
这些术语是后续学习的 “语言基础”,务必理解清楚(结合下图辅助记忆:根为 A,A 有 B/C/D/E/F/G6 个子节点,D 有 H 子节点,E 有 I 子节点):
| 术语 | 定义 | 例子 |
|---|---|---|
| 结点的度 | 一个节点拥有的子树个数 | A 的度为 6(有 6 个子节点) |
| 树的度 | 所有节点度的最大值 | 整棵树的度为 6(A 的度最大) |
| 叶子节点(终端节点) | 度为 0 的节点(没有子树) | B、C、H、I 等 |
| 父 / 子节点 | 若节点 A 有子树 B,则 A 是父节点,B 是子节点 | A 是 B 的父节点,B 是 A 的子节点 |
| 节点的层次 | 从根开始计数,根为第 1 层,子节点为第 2 层,以此类推 | A 在第 1 层,B 在第 2 层,H 在第 3 层 |
| 树的高度(深度) | 树中节点的最大层次 | 上图树的高度为 4(H/I 在第 3 层?不对,若 A1、B2、D3、H4,则高度为 4) |
| 森林 | m (m≥0) 棵互不相交的树的集合 | 若把 A 的 6 个子树单独拿出来,就是一个森林 |
1.3 树的表示方式:孩子兄弟表示法
树的存储比线性表复杂,常见的有双亲表示法、孩子表示法等,其中孩子兄弟表示法最灵活(能轻松将树转为二叉树),核心思路是:每个节点存储 “第一个孩子” 和 “下一个兄弟” 的引用。
用 Java 简单实现:
class TreeNode {
int val; //节点存储的数据
TreeNode firstChild; //指向第一个孩子
TreeNode nextBrother; //指向同一父节点的下一个兄弟
}
比如节点 A 的firstChild是 B,B 的nextBrother是 C,C 的nextBrother是 D,以此类推 —— 这样就串联起了 A 的所有子节点。
1.4 树的实际应用
最常见的就是文件系统管理:根目录是根节点,文件夹是分支节点(有子节点),文件是叶子节点(无子女),完美契合树的层次结构。
二、二叉树
二叉树是树的 “特殊版”,但正是这份 “特殊” 让它变得高效 —— 接下来我们从概念、特性到操作,逐一拆解。
2.1 二叉树的定义:度不超过 2 的有序树
一棵二叉树要么为空,要么由根节点 + 左子树 + 右子树组成,且满足两个关键规则:
- 所有节点的度≤2(最多有两个子节点:左孩子、右孩子);
- 子树有 “左右之分”,次序不能颠倒(比如左子树是 A、右子树是 B,和反过来是两棵不同的二叉树)。
二叉树的 4 种基本形态:
- 空树
- 只有根节点
- 根节点 + 左子树
- 根节点 + 右子树
- 根节点 + 左子树 + 右子树
2.2 两种特殊的二叉树
实际应用中,满二叉树和完全二叉树是高频考点,务必区分清楚。
(1)满二叉树:“装满” 的二叉树
如果二叉树的层数为k,且每层节点数都达到最大值(第i层有2^(i-1)个节点),则称为满二叉树。比如层数k=3的满二叉树,总节点数是2^3 - 1 = 7(第 1 层 1 个,第 2 层 2 个,第 3 层 4 个)。
(2)完全二叉树:“按顺序排满” 的二叉树
完全二叉树由满二叉树衍生而来,核心定义是:深度为 k、有 n 个节点的二叉树,其节点编号(从上到下、从左到右)与深度为 k 的满二叉树中 1~n 号节点完全对应。
简单理解:完全二叉树是 “先把左半部分排满,再排右半部分”,不允许出现 “左空右不空” 的情况。比如满二叉树是特殊的完全二叉树,但完全二叉树不一定是满二叉树(比如最后一层缺右边几个节点)。
2.3 二叉树的 5 个必背性质(面试高频)
这些性质是解题的 “公式”,必须牢记并会用!
性质 1:第 i 层最多有 2^(i-1) 个节点
- 解释:第 1 层(根)最多 1 个(2^0),第 2 层最多 2 个(2^1),第 3 层最多 4 个(2^2),以此类推。
性质 2:深度为 k 的二叉树最多有 2^k - 1 个节点
- 解释:每层节点数求和(等比数列):1 + 2 + 4 + ... + 2^(k-1) = 2^k - 1(满二叉树的总节点数)。
性质 3:叶子节点数 = 度为 2 的节点数 + 1(n0 = n2 + 1)
- 推导:设总节点数为
n,度为 0(叶子)、1、2 的节点数分别为n0、n1、n2,则:- 总节点数:
n = n0 + n1 + n2; - 总边数:每个节点除根外都有一条边指向父节点,所以边数 =
n - 1;同时,度为 1 的节点贡献 1 条边,度为 2 的贡献 2 条,边数 =n1 + 2n2; - 联立得:
n0 = n2 + 1。
- 总节点数:
- 例子:若有 10 个度为 2 的节点,叶子节点数就是 11。
性质 4:n 个节点的完全二叉树深度为 log₂(n+1)(上取整)
- 解释:比如 n=5,log₂(5+1)≈2.58,上取整为 3(深度 3);n=7(满二叉树),log₂(7+1)=3,深度 3。
性质 5:完全二叉树的节点编号规律(从 0 开始编号)
对编号为i的节点:
- 父节点编号:
(i-1)/2(i>0 时,比如 i=2,父节点是 0); - 左孩子编号:
2i + 1(若 2i+1 < n,否则无左孩子); - 右孩子编号:
2i + 2(若 2i+2 < n,否则无右孩子)。
比如 n=5(编号 0~4):
- 编号 2 的节点,左孩子是 5(>4,无),右孩子是 6(>4,无);
- 编号 1 的节点,左孩子是 3,右孩子是 4。
2.4 二叉树的存储方式
二叉树有两种存储方式,分别适用于不同场景。
(1)顺序存储:用数组存(适合完全二叉树)
利用完全二叉树的编号规律(性质 5),将节点按编号存入数组:
- 编号 i 的节点存在数组下标 i 的位置;
- 父、子节点通过公式计算(无需指针)。
缺点:非完全二叉树会浪费大量数组空间(比如右斜树,大部分位置为空)。
(2)链式存储:用链表存(通用)
最常用的是孩子表示法:每个节点存储数据、左孩子引用、右孩子引用,结构灵活,适合所有二叉树。
Java 实现:
//二叉树节点类
class TreeNode {
int val; //数据域
TreeNode left; //左孩子引用(指向左子树)
TreeNode right; //右孩子引用(指向右子树)
//构造方法
public TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
还有 “孩子双亲表示法”(多一个 parent 引用),后续在平衡树中会用到,这里先聚焦孩子表示法。
2.5 二叉树的基本操作(核心中的核心)
二叉树的操作大多基于递归(因为二叉树是递归定义的),我们从 “创建二叉树” 开始,逐步学习遍历、节点计数等操作。
2.5.1 临时创建二叉树(快速入门)
为了快速上手操作,先手动创建一棵简单的二叉树(后续会讲从数组 / 字符串构建二叉树的通用方法):
public class BinaryTree {
private TreeNode root; //根节点
//手动创建二叉树(结构:1为根,左2右4;2左3;4左5;5右6)
public void createTempTree() {
TreeNode node1 = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
root = node1; //根为1
node1.left = node2; //1的左孩子是2
node2.left = node3; //2的左孩子是3
node1.right = node4; //1的右孩子是4
node4.left = node5; //4的左孩子是5
node5.right = node6; //5的右孩子是6
}
}
这棵树的结构如下:

2.5.2 二叉树的遍历(重中之重)
遍历是 “访问树中所有节点,且每个节点仅访问一次”,是后续所有操作的基础。二叉树有 4 种核心遍历方式:前序、中序、后序(递归)和层序(迭代)。
(1)前序遍历(NLR:根 → 左 → 右)
遍历顺序:先访问根节点,再递归遍历左子树,最后递归遍历右子树。
递归实现:
//前序遍历(递归)
public void preOrder(TreeNode root) {
if (root == null) {
return; //递归终止条件:空树直接返回
}
System.out.print(root.val + " "); //访问根节点
preOrder(root.left); //遍历左子树
preOrder(root.right); //遍历右子树
}
对上面的临时树,前序遍历结果:1 2 3 4 5 6。
(2)中序遍历(LNR:左 → 根 → 右)
遍历顺序:先递归遍历左子树,再访问根节点,最后递归遍历右子树。
递归实现:
//中序遍历(递归)
public void inOrder(TreeNode root) {
if (root == null) {
return;
}
inOrder(root.left); //遍历左子树
System.out.print(root.val + " "); //访问根节点
inOrder(root.right); //遍历右子树
}
对临时树,中序遍历结果:3 2 1 5 6 4。
(3)后序遍历(LRN:左 → 右 → 根)
遍历顺序:先递归遍历左子树,再递归遍历右子树,最后访问根节点。
递归实现:
//后序遍历(递归)
public void postOrder(TreeNode root) {
if (root == null) {
return;
}
postOrder(root.left); //遍历左子树
postOrder(root.right); //遍历右子树
System.out.print(root.val + " "); //访问根节点
}
对临时树,后序遍历结果:3 2 6 5 4 1。
(4)层序遍历(自上而下,自左至右)
遍历顺序:从根节点开始,按层次依次访问每个节点(第 1 层→第 2 层→...→第 k 层),同一层从左到右。
实现思路:用队列(先进先出)辅助,步骤如下:
- 根节点入队;
- 队列非空时,出队当前节点并访问;
- 若当前节点有左孩子,左孩子入队;
- 若当前节点有右孩子,右孩子入队;
- 重复 2~4,直到队列为空。
代码实现:
import java.util.LinkedList;
import java.util.Queue;
//层序遍历
public void levelOrder(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); //根节点入队
while (!queue.isEmpty()) {
TreeNode cur = queue.poll(); //出队当前节点
System.out.print(cur.val + " "); //访问
//左孩子入队(先左后右,保证同一层顺序)
if (cur.left != null) {
queue.offer(cur.left);
}
//右孩子入队
if (cur.right != null) {
queue.offer(cur.right);
}
}
}
对临时树,层序遍历结果:1 2 4 3 5 6。
2.5.3 其他常用操作(递归实现)
这些操作的核心思路都是 “分解为子问题”—— 根节点的操作 + 左子树的操作 + 右子树的操作。
(1)获取树的总节点数
public int getNodeCount(TreeNode root) {
if (root == null) {
return 0; //空树节点数为0
}
//总节点数 = 1(根) + 左子树节点数 + 右子树节点数
return 1 + getNodeCount(root.left) + getNodeCount(root.right);
}
(2)获取叶子节点数
public int getLeafCount(TreeNode root) {
if (root == null) {
return 0;
}
//叶子节点:左右孩子都为空
if (root.left == null && root.right == null) {
return 1;
}
//叶子数 = 左子树叶子数 + 右子树叶子数
return getLeafCount(root.left) + getLeafCount(root.right);
}
(3)获取第 k 层的节点数
public int getKLevelCount(TreeNode root, int k) {
if (root == null || k < 1) {
return 0; //空树或k无效(层次从1开始)
}
if (k == 1) {
return 1; //第1层只有根节点
}
//第k层节点数 = 左子树第k-1层节点数 + 右子树第k-1层节点数
return getKLevelCount(root.left, k-1) + getKLevelCount(root.right, k-1);
}
(4)获取树的高度(深度)
public int getTreeHeight(TreeNode root) {
if (root == null) {
return 0; //空树高度为0
}
//树的高度 = 1(当前层) + max(左子树高度, 右子树高度)
int leftHeight = getTreeHeight(root.left);
int rightHeight = getTreeHeight(root.right);
return 1 + Math.max(leftHeight, rightHeight);
}
(5)查找值为 val 的节点
public TreeNode findNode(TreeNode root, int val) {
if (root == null) {
return null; //空树,未找到
}
if (root.val == val) {
return root; //找到,返回当前节点
}
//先在左子树找,找到则返回
TreeNode leftResult = findNode(root.left, val);
if (leftResult != null) {
return leftResult;
}
//左子树没找到,在右子树找
return findNode(root.right, val);
}
(6)判断是否为完全二叉树
思路:用层序遍历,遇到空节点后,后续不能再出现非空节点(否则不是完全二叉树)。
public boolean isCompleteTree(TreeNode root) {
if (root == null) {
return true; //空树是完全二叉树
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
boolean hasNull = false; //标记是否遇到过空节点
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
if (cur == null) {
hasNull = true; //遇到空节点,标记
} else {
if (hasNull) {
return false; //空节点后出现非空节点,不是完全二叉树
}
//不管孩子是否为空,都入队(后续判断)
queue.offer(cur.left);
queue.offer(cur.right);
}
}
return true;
}
三、实战提升:经典二叉树 OJ 题思路
掌握了基础操作后,我们来看看面试中常见的二叉树题目,如何用所学知识解决。
1. 检查两棵树是否相同(LeetCode 100)
题目:判断两棵二叉树的结构和节点值是否完全相同。思路:递归比较根节点 + 左子树 + 右子树:
- 都为空:相同;
- 一个空一个非空:不同;
- 根节点值不同:不同;
- 递归比较左子树和右子树。
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);
}
2. 翻转二叉树(LeetCode 226)
题目:将二叉树的左右子树互换(镜像翻转)。思路:递归交换每个节点的左右子树:
- 空树直接返回;
- 交换当前节点的左右孩子;
- 递归翻转左子树和右子树。
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
//交换左右孩子
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
//递归翻转左、右子树
invertTree(root.left);
invertTree(root.right);
return root;
}
3. 二叉树的最近公共祖先(LeetCode 236)
题目:找到二叉树中两个指定节点的最近公共祖先(LCA)。思路:递归查找,利用 “祖先的特性”:
- 若 root 是 p 或 q,root 就是 LCA;
- 递归在左子树找 p/q,在右子树找 p/q;
- 若左子树找到一个,右子树找到一个,root 是 LCA;
- 若只有左子树找到,返回左子树结果;反之返回右子树结果。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
//左子树查找
TreeNode left = lowestCommonAncestor(root.left, p, q);
//右子树查找
TreeNode right = lowestCommonAncestor(root.right, p, q);
//左右都找到,root是LCA
if (left != null && right != null) {
return root;
}
//只在左/右找到,返回对应结果
return left != null ? left : right;
}
四、总结
二叉树的核心是递归思维和遍历逻辑—— 从树的基础概念到二叉树的特性、操作,再到实战题目,所有知识点都围绕这两点展开。想要真正掌握二叉树,建议:
- 牢记 5 个性质,尤其是 n0=n2+1 和完全二叉树的编号规律;
- 手动模拟前中后序遍历(画递归栈),理解递归过程;
- 多做 OJ 题,比如非递归遍历、根据遍历序列构建二叉树等,强化应用能力。
二叉树是后续学习平衡树(AVL、红黑树)、B 树的基础,打好这个地基,后续复杂的数据结构学习会轻松很多!
2297

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



