哈喽各位同学!作为刚啃完数据结构"树"这一章节的我,深知这部分知识点看似抽象、实则逻辑极强,尤其是二叉树的性质和操作,既是考试重点也是后续学习的基础。今天就结合我的个人总结,把树与二叉树的核心内容梳理+拓展一下,帮大家少走弯路,快速吃透这部分知识~
一、树的核心概念与属性
首先得明确:树是一种非线性数据结构,它以分层的方式存储数据,像自然界的树一样有"根"有"枝"有"叶",核心特点是任意两个节点之间有且仅有一条路径,不存在环路(这也是它和图结构的核心区别)。下面这些基础属性必须牢记,是理解后续内容的前提:
-
根节点:树的最顶层节点,没有父节点,是整棵树的起点。比如家族树中的"祖先",是所有节点的根源。
-
父节点与子节点:若一个节点含有子结构,则该节点为父节点,其子结构的根节点为子节点。注意:一个父节点可以有多个子节点,但一个子节点只能有一个父节点。
-
叶子节点:没有子节点的节点,也叫终端节点。就像树的叶子,是分层结构的最末端。
-
度:分为节点的度和树的度。节点的度是该节点拥有的子节点个数;树的度是整棵树中所有节点度的最大值。比如一棵节点最多有3个子节点的树,其度为3。
-
树的深度(高度):有两种常见定义,需注意题干约定(考试常考!):一种是从根节点开始计数,根节点深度为1,最底层叶子节点的深度即为树的深度;另一种是根节点深度为0,具体以教材或题目说明为准,建议默认记忆"根为1"的场景,做题时再灵活调整。
小拓展:树的应用场景很广,比如操作系统的文件系统(文件夹嵌套就是典型的树结构)、数据库的索引结构、XML/HTML文档的解析等,理解树的本质能帮我们看懂很多实际场景的底层逻辑。
二、二叉树(重中之重)
二叉树是树结构中最常用的类型,核心定义:每个节点最多有两个子节点,分别称为左子节点和右子节点(允许只有左子树、只有右子树,或都没有)。下面分模块拆解重点内容。
(一)满二叉树与完全二叉树
这两种是二叉树的特殊形态,考试常考判断和性质应用,一定要分清二者的区别:
-
满二叉树:除了叶子节点,每个节点都有两个子节点,且所有叶子节点都在同一层。简单说就是"层层长满",没有空缺。比如深度为3的满二叉树,总节点数为7(1+2+4),符合"深度为k的二叉树最大节点数"的规律。
-
完全二叉树:按从上至下、从左至右的顺序依次填充节点,中间不允许出现空缺(最后一层的叶子节点只能集中在左侧,右侧可以空缺)。注意:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。比如深度为3的完全二叉树,总节点数可以是5、6、7,若为5,则最后一层只有左起两个叶子节点。
小拓展:完全二叉树的特性让它适合用数组存储(后续存储部分会讲),这也是它在实际应用中更常见的原因,比如堆排序中的"堆",本质就是一棵完全二叉树。
(二)二叉树的五大核心性质(必背+会用)
这部分是考试的"高频考点",不仅要记住公式,更要理解推导逻辑,避免死记硬背出错。下面结合推导和例子帮大家梳理:
-
第i层最多节点数:若根节点层数为1,则第i层最多有2^(i-1)个节点(i>0)。推导:第1层(根)1个,第2层最多2个,第3层最多4个,每层节点数是上一层的2倍,符合等比数列规律。
-
深度为k的最大节点数:若根节点深度为1,则总节点数最多为2^k - 1(k≥1)。推导:各层节点数之和为等比数列求和,1+2+4+...+2^(k-1) = 2^k - 1,对应满二叉树的节点总数。
-
叶子节点与度为2节点的关系:对任意一棵二叉树,叶子节点数n0 = 度为2的非叶子节点数n2 + 1。推导:设总节点数为n,度为1的节点数为n1,则n = n0 + n1 + n2;同时,所有节点的边数之和为n-1(树的边数=节点数-1),而边数也等于n1* 1 + n2* 2(度为1的节点贡献1条边,度为2的贡献2条),联立可得n0 = n2 + 1。这个性质常用来解题,比如已知n2求n0,或已知n0和n2求总节点数。
-
完全二叉树的深度:具有n个节点的完全二叉树,深度k为⌈log₂(n+1)⌉(上取整),也可表示为floor(log₂n)) + 1。例子:n=5时,log₂5≈2.32,floor后为2,加1得深度3;n=7时,log₂(7+1)=3,深度为3(满二叉树)。
-
完全二叉树的节点编号规则:按从上至下、从左至右给节点从0开始编号,对序号为i的节点:① 若i>0,双亲序号为(i-1)/2(整数除法,舍去小数);② 左孩子序号为2i+1(若2i+1<n,否则无左孩子);③ 右孩子序号为2i+2(若2i+2<n,否则无右孩子)。这个规则是数组存储完全二叉树的核心,也是后续堆操作、节点查找的基础。例子:i=2(0开始),双亲为(2-1)/2=0;左孩子为5,右孩子为6(若n>6)。
三、二叉树的存储方式
二叉树的存储主要有两种方式,分别适用于不同场景,按需选择即可:
(一)顺序存储(数组存储)
核心逻辑:利用完全二叉树的节点编号规则,将节点存入数组对应下标位置。优点:存储效率高,通过下标可快速查找双亲、孩子节点,无需额外存储指针;缺点:只适合完全二叉树,若为普通二叉树(存在大量空缺节点),会浪费大量数组空间。比如一棵深度为4的普通二叉树,若只有左子树,数组中大部分下标会为空。
小拓展:实际应用中,堆(大根堆、小根堆)就是用顺序存储实现的,充分利用了完全二叉树的特性和数组的高效访问。
(二)链式存储(二叉链表)
核心逻辑:每个节点设计一个数据域和两个指针域(左指针lchild、右指针rchild),分别指向左子节点和右子节点,根节点单独存储,通过指针串联起整棵树。优点:适合所有类型的二叉树,无空间浪费,插入、删除节点时只需调整指针;缺点:查找双亲节点时不够高效(需从头遍历),若需频繁查找双亲,可优化为"三叉链表"(增加一个parent指针指向双亲节点)。
小拓展:二叉链表是最常用的二叉树存储方式,后续二叉树的遍历、创建等操作,大多基于二叉链表实现,Java中自定义二叉树时,通常会定义这样的节点类。
四、二叉树的基本操作(Java视角)
二叉树的操作是实战重点,掌握创建、遍历和常用函数,才能应对编程题和实际应用,下面分模块讲解:
(一)树的创建与实例化应用
首先需定义二叉树节点类(Java),核心包含数据域和左右指针:
class TreeNode {
int val; // 数据域
TreeNode left; // 左子节点指针
TreeNode right; // 右子节点指针
// 构造方法
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
创建二叉树的本质的是手动或通过输入数据,为每个节点分配左、右子节点,构建链式结构。例子:创建一棵简单的二叉树(根为1,左孩子2,右孩子3,2的左孩子4):
public class BinaryTree {
// 根节点
private TreeNode root;
// 构造方法
public BinaryTree() {
this.root = null;
}
// 手动创建二叉树(示例树:根1,左2,右3,2的左4)
public void createTree() {
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); // 新增节点,丰富示例树
root = node1;
node1.left = node2;
node1.right = node3;
node2.left = node4;
node3.left = node5;
}
// -------------------------- 递归遍历实现 --------------------------
// 前序遍历(根→左→右)
public void preOrderRecursion(TreeNode node) {
if (node == null) {
return;
}
System.out.print(node.val + " "); // 访问根节点
preOrderRecursion(node.left); // 递归左子树
preOrderRecursion(node.right); // 递归右子树
}
// 中序遍历(左→根→右)
public void inOrderRecursion(TreeNode node) {
if (node == null) {
return;
}
inOrderRecursion(node.left); // 递归左子树
System.out.print(node.val + " "); // 访问根节点
inOrderRecursion(node.right); // 递归右子树
}
// 后序遍历(左→右→根)
public void postOrderRecursion(TreeNode node) {
if (node == null) {
return;
}
postOrderRecursion(node.left); // 递归左子树
postOrderRecursion(node.right); // 递归右子树
System.out.print(node.val + " "); // 访问根节点
}
// -------------------------- 迭代遍历实现 --------------------------
// 前序遍历(栈实现)
public void preOrderIteration() {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.print(node.val + " "); // 访问根节点
// 右子节点先入栈,左子节点后入栈(栈先进后出,保证左先遍历)
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}
// 中序遍历(栈实现)
public void inOrderIteration() {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
// 遍历至左子树最底层
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
// 访问节点,切换至右子树
curr = stack.pop();
System.out.print(curr.val + " ");
curr = curr.right;
}
}
// 后序遍历(栈实现,标记法)
public void postOrderIteration() {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode prev = null; // 记录上一个访问过的节点
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
// 遍历至左子树最底层
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
curr = stack.peek();
// 右子树为空或已访问,再访问当前节点
if (curr.right == null || curr.right == prev) {
System.out.print(curr.val + " ");
stack.pop();
prev = curr;
curr = null; // 避免重复遍历左子树
} else {
curr = curr.right; // 切换至右子树
}
}
}
// 层序遍历(队列实现,广度优先)
public void levelOrder() {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
System.out.print(node.val + " "); // 访问当前节点
// 左子节点先入队,右子节点后入队(队列先进先出,保证层序)
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
// -------------------------- 常用工具函数实现 --------------------------
// size():统计节点总数(递归)
public int size(TreeNode node) {
if (node == null) {
return 0;
}
return 1 + size(node.left) + size(node.right);
}
// getHeight():计算树的深度(递归)
public int getHeight(TreeNode node) {
if (node == null) {
return 0;
}
int leftHeight = getHeight(node.left);
int rightHeight = getHeight(node.right);
return Math.max(leftHeight, rightHeight) + 1;
}
// findNode():查找指定值节点(递归,返回节点对象)
public TreeNode findNode(TreeNode node, int val) {
if (node == null) {
return null; // 未找到
}
if (node.val == val) {
return node; // 找到目标节点
}
// 先查左子树,左子树未找到再查右子树
TreeNode leftResult = findNode(node.left, val);
if (leftResult != null) {
return leftResult;
}
return findNode(node.right, val);
}
// isBalanced():判断是否为平衡二叉树(递归)
public boolean isBalanced(TreeNode node) {
if (node == null) {
return true; // 空树视为平衡
}
// 计算左右子树深度
int leftHeight = getHeight(node.left);
int rightHeight = getHeight(node.right);
// 当前节点平衡,且左右子树均平衡
return Math.abs(leftHeight - rightHeight) <= 1
&& isBalanced(node.left)
&& isBalanced(node.right);
}
// 测试入口
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
tree.createTree(); // 构建示例树
System.out.println("=== 递归遍历 ===");
System.out.print("前序遍历:");
tree.preOrderRecursion(tree.root); // 输出:1 2 4 3 5
System.out.print("\n中序遍历:");
tree.inOrderRecursion(tree.root); // 输出:4 2 1 5 3
System.out.print("\n后序遍历:");
tree.postOrderRecursion(tree.root); // 输出:4 2 5 3 1
System.out.println("\n\n=== 迭代遍历 ===");
System.out.print("前序遍历:");
tree.preOrderIteration(); // 输出:1 2 4 3 5
System.out.print("\n中序遍历:");
tree.inOrderIteration(); // 输出:4 2 1 5 3
System.out.print("\n后序遍历:");
tree.postOrderIteration(); // 输出:4 2 5 3 1
System.out.print("\n层序遍历:");
tree.levelOrder(); // 输出:1 2 3 4 5
System.out.println("\n\n=== 常用函数测试 ===");
System.out.println("节点总数:" + tree.size(tree.root)); // 输出:5
System.out.println("树的深度:" + tree.getHeight(tree.root)); // 输出:3
TreeNode findNode = tree.findNode(tree.root, 3);
System.out.println("是否找到值为3的节点:" + (findNode != null ? "是" : "否")); // 输出:是
System.out.println("是否为平衡二叉树:" + (tree.isBalanced(tree.root) ? "是" : "否")); // 输出:是
}
}
// 二叉树节点类(需与BinaryTree类同级或在其内部)
class TreeNode {
int val; // 数据域
TreeNode left; // 左子节点指针
TreeNode right; // 右子节点指针
// 构造方法
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
补充说明:上述代码整合了二叉树的创建、四种遍历(递归+迭代双实现)及核心工具函数,每个方法都添加了注释便于理解,main方法中包含测试用例,可直接复制到IDE运行验证结果。其中迭代遍历采用栈(前/中/后序)和队列(层序)实现,覆盖面试常考场景;工具函数均为实际开发和考题中的高频需求,同时优化了示例树结构,让遍历结果更具代表性。
小拓展:实际中常通过前序、中序遍历结果反向构建二叉树(面试高频题),核心是利用前序找根、中序分左右子树的逻辑,递归构建整棵树。
(二)二叉树的遍历(核心操作)
遍历是指按一定顺序访问二叉树的所有节点,是后续统计节点数、查找节点、删除节点等操作的基础。二叉树有四种常用遍历方式,重点掌握递归实现(迭代实现可作为拓展,应对面试)。
-
前序遍历(根→左→右):先访问根节点,再递归遍历左子树,最后递归遍历右子树。例子:上述创建的二叉树,前序遍历结果为1→2→4→3。
-
中序遍历(左→根→右):先递归遍历左子树,再访问根节点,最后递归遍历右子树。例子:上述二叉树,中序遍历结果为4→2→1→3。注意:中序遍历是二叉搜索树(BST)的核心遍历方式,遍历结果为有序序列。
-
后序遍历(左→右→根):先递归遍历左子树,再递归遍历右子树,最后访问根节点。例子:上述二叉树,后序遍历结果为4→2→3→1。
-
层序遍历(广度优先遍历):按从上至下、从左至右的顺序,逐层访问节点。需借助队列实现(入队根节点,出队时入队其左右子节点,循环至队空)。例子:上述二叉树,层序遍历结果为1→2→3→4。
小拓展:递归遍历代码简洁易懂,但可能存在栈溢出风险(树深度过大时);迭代遍历通过栈(前、中、后序)或队列(层序)模拟递归过程,更适合底层开发场景,建议两种方式都掌握。
(三)Java封装的二叉树常用函数
自定义二叉树时,常封装以下函数,实现核心功能:
-
size():统计节点总数:递归实现,根节点为1,加上左子树节点数和右子树节点数,即size(root) = 1 + size(root.left) + size(root.right)(空节点返回0)。
-
getHeight():计算树的深度:递归实现,空节点深度为0,非空节点深度为1 + max(左子树深度, 右子树深度)。
-
findNode(int val):查找指定值节点:递归或迭代遍历树,找到值为val的节点返回,未找到返回null。
-
isBalanced():判断是否为平衡二叉树:平衡二叉树是指左右子树深度差不超过1的二叉树,需结合getHeight()函数,递归判断每个节点的左右子树深度差。
(四)相关面试题提示
二叉树是面试的"常客",建议大家在掌握基础后,针对性练习以下类型的题目:① 遍历相关(迭代实现前/中/后序、层序遍历变种,如之字形遍历);② 构建二叉树(前序+中序、中序+后序构建);③ 二叉树的性质应用(节点数计算、深度计算、平衡判断);④ 二叉搜索树的操作(插入、删除、查找、验证);⑤ 路径问题(二叉树的所有路径、求和路径)。推荐在LeetCode上刷二叉树专题,从简单题入手,逐步提升。
总结
树与二叉树的核心在于"分层逻辑"和"递归思维",二叉树的性质、遍历和存储是基础,后续的二叉搜索树、平衡二叉树、堆等都是基于二叉树的扩展。建议大家先吃透基础概念和性质,再通过编程实践巩固操作,遇到递归问题多画图分析,慢慢就能掌握其中的规律啦~ 祝大家都能搞定数据结构中的这棵"大树"!