数据结构之树与二叉树:重点梳理与拓展

哈喽各位同学!作为刚啃完数据结构"树"这一章节的我,深知这部分知识点看似抽象、实则逻辑极强,尤其是二叉树的性质和操作,既是考试重点也是后续学习的基础。今天就结合我的个人总结,把树与二叉树的核心内容梳理+拓展一下,帮大家少走弯路,快速吃透这部分知识~

一、树的核心概念与属性

首先得明确:树是一种非线性数据结构,它以分层的方式存储数据,像自然界的树一样有"根"有"枝"有"叶",核心特点是任意两个节点之间有且仅有一条路径,不存在环路(这也是它和图结构的核心区别)。下面这些基础属性必须牢记,是理解后续内容的前提:

  • 根节点:树的最顶层节点,没有父节点,是整棵树的起点。比如家族树中的"祖先",是所有节点的根源。

  • 父节点与子节点:若一个节点含有子结构,则该节点为父节点,其子结构的根节点为子节点。注意:一个父节点可以有多个子节点,但一个子节点只能有一个父节点。

  • 叶子节点:没有子节点的节点,也叫终端节点。就像树的叶子,是分层结构的最末端。

  • 度:分为节点的度和树的度。节点的度是该节点拥有的子节点个数;树的度是整棵树中所有节点度的最大值。比如一棵节点最多有3个子节点的树,其度为3。

  • 树的深度(高度):有两种常见定义,需注意题干约定(考试常考!):一种是从根节点开始计数,根节点深度为1,最底层叶子节点的深度即为树的深度;另一种是根节点深度为0,具体以教材或题目说明为准,建议默认记忆"根为1"的场景,做题时再灵活调整。

小拓展:树的应用场景很广,比如操作系统的文件系统(文件夹嵌套就是典型的树结构)、数据库的索引结构、XML/HTML文档的解析等,理解树的本质能帮我们看懂很多实际场景的底层逻辑。

二、二叉树(重中之重)

二叉树是树结构中最常用的类型,核心定义:每个节点最多有两个子节点,分别称为左子节点和右子节点(允许只有左子树、只有右子树,或都没有)。下面分模块拆解重点内容。

(一)满二叉树与完全二叉树

这两种是二叉树的特殊形态,考试常考判断和性质应用,一定要分清二者的区别:

  • 满二叉树:除了叶子节点,每个节点都有两个子节点,且所有叶子节点都在同一层。简单说就是"层层长满",没有空缺。比如深度为3的满二叉树,总节点数为7(1+2+4),符合"深度为k的二叉树最大节点数"的规律。

  • 完全二叉树:按从上至下、从左至右的顺序依次填充节点,中间不允许出现空缺(最后一层的叶子节点只能集中在左侧,右侧可以空缺)。注意:满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。比如深度为3的完全二叉树,总节点数可以是5、6、7,若为5,则最后一层只有左起两个叶子节点。

小拓展:完全二叉树的特性让它适合用数组存储(后续存储部分会讲),这也是它在实际应用中更常见的原因,比如堆排序中的"堆",本质就是一棵完全二叉树。

(二)二叉树的五大核心性质(必背+会用)

这部分是考试的"高频考点",不仅要记住公式,更要理解推导逻辑,避免死记硬背出错。下面结合推导和例子帮大家梳理:

  1. 第i层最多节点数:若根节点层数为1,则第i层最多有2^(i-1)个节点(i>0)。推导:第1层(根)1个,第2层最多2个,第3层最多4个,每层节点数是上一层的2倍,符合等比数列规律。

  2. 深度为k的最大节点数:若根节点深度为1,则总节点数最多为2^k - 1(k≥1)。推导:各层节点数之和为等比数列求和,1+2+4+...+2^(k-1) = 2^k - 1,对应满二叉树的节点总数。

  3. 叶子节点与度为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求总节点数。

  4. 完全二叉树的深度:具有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(满二叉树)。

  5. 完全二叉树的节点编号规则:按从上至下、从左至右给节点从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&gt; 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&gt; 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&gt; 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上刷二叉树专题,从简单题入手,逐步提升。

总结

树与二叉树的核心在于"分层逻辑"和"递归思维",二叉树的性质、遍历和存储是基础,后续的二叉搜索树、平衡二叉树、堆等都是基于二叉树的扩展。建议大家先吃透基础概念和性质,再通过编程实践巩固操作,遇到递归问题多画图分析,慢慢就能掌握其中的规律啦~ 祝大家都能搞定数据结构中的这棵"大树"!

相关推荐
毕设源码-钟学长1 天前
【开题答辩全过程】以 助学贷款管理系统为例,包含答辩的问题和答案
java
亓才孓1 天前
任意大小的整数和任意精度的小数的API方法
java
2501_941875281 天前
从资源隔离到多租户安全的互联网工程语法构建与多语言实践分享
java·开发语言
xiaolyuh1231 天前
ThreadLocalMap 中弱引用被 GC 后的行为机制解析
java·jvm·redis
不知疲倦的仄仄1 天前
第一天:从 ByteBuffer 内存模型到网络粘包处理实战
java·网络·nio
Tinachen881 天前
YonBIP旗舰版本地开发环境搭建教程
java·开发语言·oracle·eclipse·前端框架
星火开发设计1 天前
堆排序原理与C++实现详解
java·数据结构·c++·学习·算法·排序算法
七七powerful1 天前
docker28.1.1和docker-compose v.2.35.1安装
java·docker·eureka
CoderIsArt1 天前
常用SCSI数据结构的详细注释和用法
数据结构