数据结构之树(Java实现)

一.树的概念

在计算机科学(Computer Science,SC)中,树(Tree)是一种重要的非线性数据结构,用于表示具有层次关系的数据。它由节点(Node)组成,其中一个节点被指定为根节点(Root),其余节点被划分成若干个互不相交的子集,每个子集本身也是一颗树,称为子树(Subtree)

二.树的特点

树是一种递归定义的非线性结构,由节点组成

  • 有且仅有一个根节点
  • 其余节点被划分成若干个互不相交的子树(子树之间不能相交,否则会形成环路)
  • 节点之间通过边(Edge)连接
  • 没有环(Cycle):树中不存在环路(即不能从一个节点出发沿环回到自身)
  • 节点个数n=0时为空树

三.树的基本术语

1.根(Root)

根为树的顶层节点,没有父节点

复制代码
    A
   / \
  B   C

如上图,A就是根节点

2.父节点(Parent)

父节点表示一个节点的直接上层节点

复制代码
    A
   /
  B

如上例子,A是B的父节点

3.子节点(Child)

表示一个节点的直接下层节点

复制代码
    A
   /
  B

B是A的子节点

4.叶子(Leaf)

没有子节点的节点

复制代码
    A
   / \
  B   C
     / \
    D   E

B、D、E都是叶子节点

5.兄弟(Silbing)

具有相同父节点的节点

复制代码
    A
   / \
  B   C

B和C是兄弟节点

6.深度(Depth)

表示从根到改节点的边数。

根深度通常为0或1,依定义而定

复制代码
    A (depth 0)
   / \
  B   C (depth 1)
     / \
    D   E (depth 2)

7.高度(Height)

表示从该节点到最远叶子的最长路径边数

整棵树的高数等于根的高度

复制代码
    A (height 2)
   / \
  B   C (height 1)
     / \
    D   E (height 0)

整棵树的高度是2,因为从根节点A到最远叶子节点(如D或E),需要经过两条边

8.层(Level)

层是从根节点开始自上而下对树中节点进行的水平划分

  • 根节点位于第0层(虽然有些教程定义为第1层,但在CS中,通常从0开始计数)

  • 层与深度的关系:一个节点的"层号"等于它的深度(前提是根节点的深度等于0)

    复制代码
          A                ← 层 0(深度 0)
         / \
        B   C              ← 层 1(深度 1)
       /   / \
      D   E   F            ← 层 2(深度 2)
             /
            G               ← 层 3(深度 3)
节点 所在层(Level) 深度(Depth)
A 0 0
B, C 1 1
D, E, F 2 2
G 3 3

为什么需要"层"这个概念:

1.层序遍历:

按从上到下,从左到右访问节点,也叫广度优先遍历(BFS)

如上图进行BFS,顺序为:A → B → C → D → E → F → G

2.计算树的宽度(Width)

某一层中节点最多的数量,就是树的"最大宽度"

3.打印树的结构

很多可视化工具按层输出,便于理解层次关系

9.度(Degree)

节点的度:该节点拥有的子节点的数量

树的度:指整棵树所有节点的度的最大值

换句话说,一个节点有几个"孩子",它的度就是几

java 复制代码
        A
      / | \
     B  C  D
    / \
   E   F

节点A有3个字节点(B、C、D) → 节点A的度为3

节点B有2个子节点(E、F) → 节点B的度为2

节点C、D、E、F都没有子节点 → 度都为0

整棵树的度为max(3,2,0,0,0,0) = 3

由此得出叶子节点的特性:度为0的节点为叶子节点,叶子节点的度为0

10.度为m的树和m叉树

相同点:树里面节点的度最多为m

不同点:

  • 度为m的树至少要有一个节点的度等于m
  • m叉树所有节点的度都可以小于m,甚至可以是空树(0节点,自然节点的度为0,树的度也为0)
  • " 度为m的树 "这种说法是描述树的某一刻的状态
  • m叉树是事先对树结构的一种约束、、

11.有序树和无序树

各节点从左到右有次序,不能互换,称该树为有序树;否则称为无序树

java 复制代码
        A
      / | \
     B  C  D
    / \
   E   F

比如,如上图,假如规定所有子树上的节点(字母)要从小到大排列,比如B、C、D,那就是有序树

如果这些子树可以随意交换位置

如:

java 复制代码
        A
      / | \
     D  B  C
       / \
      E   F

那就是无序树

12.路径(Path)

路径是指从树的一个节点到另一个节点所经过的连续边的序列

路径由一系列相邻的节点组成,且不能重复经过同一个节点(因为树中无边)

路径的特点:

  • 唯一性:在树中,任意两个节点有且仅有一条路径(这是树"无环" + "联通"的直接结果)
  • 方向无关:路径可以自上而下(根 → 叶),也可以自下而上(叶 → 根),或横跨子树(如左子树某节点 → 右子树某节点)
  • 长度:路径的长度等于路中边的数量(不是路中的节点数!!) 如A → B → C,长度为2 。 单节点(如A → A)路径的长度为0
java 复制代码
        A
       / \
      B   C
     /   / \
    D   E   F
           /
          G
起点 → 终点 路径(节点序列) 路径长度(边数)
A → D A → B → D 2
D → G D → B → A → C → F → G 5
C → E C → E 1
F → F F 0
B → C B → A → C 2

注意:D到G必须经过它们的共同祖先A,因为树种没有其他连接方式

概念 含义 是否包含多个节点?
路径(Path) 两个节点之间的连接路线 ✅ 是(至少1个节点,通常≥2)
深度(Depth) 从根到某节点的路径长度 ❌ 是一种特殊路径(根→某节点)
高度(Height) 从某节点到最远叶子的最长路径长度 ❌ 是路径长度的最大值
祖先/后代 描述节点间的上下级关系 路径上的节点互为祖先/后代

13.直径(Diameter)

树中任意两个节点的最长路径称为直径

java 复制代码
        A
       / \
      B   C
     /     \
    D       F
             \
              G

最长路径可能是:D → B → A → C → F → G,长度 = 5(6个节点,5条边)

14.森林(Forest)

森林是若干棵互不相交的树的集合

森林 = 多棵树

空森林(0棵树)也是合法的森林

这是一棵树:

java 复制代码
    A
   / \
  B   C
     / \
    D   E

这是一个森林(多棵树,互不相连):

java 复制代码
    A        F        X
   / \                \
  B   C                Y
     / \
    D   E

四.树的性质

1.节点数 = 边数 + 1 = 所有节点度数之和 + 1

例题:

在一棵度为4的树T中,若有20个度为4的节点,10个度为3的节点,1个度为2的节点,10个度为1的节点,则树T的叶节点个数是( )。

解析:

节点数n = 度为4的节点个数 + 度为3的节点个数 + 度为2的节点个数 + 度为1的节点个数 + 度为0的节点个数(叶子节点)

n = 20 + 10 + 1 + 10 +

n = 41 +

而由公式:节点数 = 所有节点度数之和 + 1

即n = 20*4 + 10*3 + 1*2 + 10 * 1 + *0 + 1

n = 123

所以123 = 41 +

所以 = 82

所以叶子节点数为82

五.二叉树(Binary Tree)

1.二叉树的概念

每个节点最多有2个分支(最多有2个孩子节点),即节点的度只可能为0、1、2的树

二叉树是一种特殊的树结构,其中每个节点最多有两个子节点,并且这两个子节点有明确的顺序区分:

左子节点(Left Child)

右子节点(Right Child)

注意:"左" 不等于"右",即使某个节点只有一个子节点,也必须说明它是左子节点还是右子节点

合法的二叉树示例:

java 复制代码
       1
      / \
     2   3
    /   / \
   4   5   6

关键:有序 + 最多两个孩子

2.二叉树的常见类型

类型 特点
满二叉树(Full Binary Tree) 每个节点要么有 0 个子节点,要么有 2 个(不能只有 1 个)
完全二叉树(Complete Binary Tree) 除了最后一层,其他层全满;最后一层节点靠左排列(堆常用)
完美二叉树(Perfect Binary Tree) 所有叶子在同一层,且每个非叶节点都有 2 个孩子
二叉搜索树(BST) 左子树所有值 < 根 < 右子树所有值(用于高效查找)
平衡二叉树(如 AVL) 左右子树高度差 ≤ 1,保证 O(log n) 性能

完全二叉树是满二叉树的一种特殊情况,完全二叉树最后一层可以不满,但是必须完全往左一次排列

如:

java 复制代码
       1
      / \
     2   3
    / \  / 
   4   5 6

而像下面这种,就不是完全二叉树

java 复制代码
       1
      / \
     2   3
    /   / \
   4   5   6

如果完全二叉树最后一层全满了,此时就是满二叉树了

如:

java 复制代码
       1
     /   \
    2     3
   / \   / \
  4   5 6   7

3.二叉树的共有性质

1.叶子节点树 = 双分支节点数 + 1

由树的性质:节点数 = 边数 + 1 = 所有节点度数之和 + 1

先理解这个性质:每个节点(除了根节点)都有唯一的向上的边,所以边数会比节点数少1

而度为x的节点机会向下延伸出x条边(指向x个子节点),所以边数和度数是相等的

n = 2* + 1* + 0 * + 1

= + +

=> 2* + + 1 = + +

=> + 1 =

4.完全二叉树的性质

从上到下,从左到右给每个节点编号,编号从1开始

复制代码
        1
      /   \
     2     3
    / \   / \
   4   5 6   7
  / \
 8   9

这样可以利用编号之间的规律,轻松地找到某个节点的孩子或父亲

一个节点,用它的编号乘以2就可以找到它的左孩子,编号乘以2再加1就能找到它的右孩子(前提是这个节点有孩子节点)

对于一个节点,编号除以2下取整就能找到它的父亲节点

对于一棵节点数为n的完全二叉树,它的最后一个分支节点的编号是n除以2下取整,即 ,而这个最后的分支节点前面的节点都是分支节点,后面的都是叶子节点

例题:一棵完全二叉树有768个节点,则该二叉树的叶子节点个数是( )。

解析: 768 除以 2 下取整为 384 ,则384(不包括384)后面的节点都是叶子节点

则叶子节点的个数为 768 - 384 = 384

完全二叉树最多只有一个度为1的节点

如:

复制代码
        1
      /   \
     2     3
    / \   / \
   4   5 6   7
  / \
 8   9

这棵二叉树没有度为1的节点

复制代码
        1
      /   \
     2     3
    / \   / \
  4    5 6   7
 / \  /
 8 9  10

这棵二叉树有一个度为1的节点

结论:完全二叉树的节点个数为偶数则有唯一的一个度为1的节点

节点个数为奇数则没有度为1的节点

5.二叉树的存储

1.顺序存储

用数组存储完全二叉树的节点,数组索引与节点编号相对应,索引0不存节点,从索引1开始存

对于不是完全二叉树的树,也可以使用顺序存储,只不过要将树补成完全二叉树(或满二叉树)的形式,补充的节点是不存在的,如果节点值是正数那就用-1来填充不存在的节点值;如果节点值是字符串,那就用#号来填充。这样就可以利用完全二叉树的规律来索引节点了

需要注意的是,如果是接近右单支树或右单支树,采用顺序存储的方式来存储节点浪费的空间将达到指数级别

2.链式存储

六.二叉树的遍历

1.前序遍历

规则:先访问根节点,再访问左子树,最后访问右子树

记忆:根左右

复制代码
       1
      / \
     2   3
    /   / \
   4   5   6

如对上述二叉树进行前序遍历,遍历的结果是:124356

2.中序遍历

规则:左根右

复制代码
       1
      / \
     2   3
    /   / \
   4   5   6

中序遍历结果:421536

3.后序遍历

规则:左右根

复制代码
       1
      / \
     2   3
    /   / \
   4   5   6

后序遍历结果:425631

4.深度优先遍历(DFS)

Depth Frist Seach

从根开始,沿一条路径走到最深(叶子),再回溯尝试其他路径

前序遍历、中序遍历、后序遍历都属于深度优先遍历

复制代码
        A
       / \
      B   C
     /   / \
    D   E   F

以前序为例:A → B → D → C → E → F

5.广度优先遍历(BFS)

Breadth Frist Seach

尽可能先访问根最近的节点,也称为层序遍历

复制代码
        A
       / \
      B   C
     /   / \
    D   E   F

层序遍历:A → B → C → D → E → F

6.Java代码

1.递归实现DFS

复制代码
       1
      / \
     2   3
    /   / \
   4   5   6

TreeNode.java

java 复制代码
package algorithm.datastructure.tree;

public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    // 构造函数1:接受节点值
    public TreeNode(int val) {
        this.val = val;
    }

    // 构造函数2:接受节点值、左子树、右子树
    public TreeNode(TreeNode left, int val, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }

    // 重写toString方法
    // toString是Java中Object类的一个方法,所有类都默认继承Object
    // 因此每个类都具备toString方法
    // toString方法提供了对象的字符串表示形式,方便调试和日志输出
    // 默认情况下,toString方法返回的是类名 + @ + 对象的hashCode(例如TreeNode@1b6d3586),但这通常不具备可读性
    // 开发者可以通过重写toString方法来自定义对象的字符串表示,使其更直观地反应对象的状态
    @Override
    public String toString() {
        return String.valueOf(this.val);
    }
}

TreeTraversal.java

java 复制代码
package algorithm.datastructure.tree;

public class TreeTraversal {
    public static void main(String[] args) {
        TreeNode root = new TreeNode(
                new TreeNode(new TreeNode(4), 2, null),
                1,
                new TreeNode(new TreeNode(5), 3, new TreeNode(6))
        );
        //System.out.println(root); // 如果没有重新toString方法则输出algorithm.datastructure.tree.TreeNode@5b2133b1,重写了则输出1
        /*Java内部设计了两种调用toString()方法的方式
        * 显式调用:对象.toString();
        * 隐式调用:System.out.println(对象);*/
        preOrder(root); // 1 2 4 3 5 6
        System.out.println();
        inOrder(root); // 4 2 1 5 3 6
        System.out.println();
        postOrder(root); // 4 2 5 6 3 1
    }

    // 前序遍历
    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);
    }

    // 后序遍历
    // post除了有邮件、邮递、邮政、帖子、发布的意思,还有在...之后的意思
    static void postOrder(TreeNode node) {
        if (node == null) {
            return;
        }
        // 左
        postOrder(node.left);
        // 右
        postOrder(node.right);
        // 根
        System.out.print(node.val + "\t");
    }
}

2.非递归实现DFS

相关推荐
NAGNIP9 小时前
轻松搞懂全连接神经网络结构!
人工智能·算法·面试
NAGNIP10 小时前
一文搞懂激活函数!
算法·面试
董董灿是个攻城狮10 小时前
AI 视觉连载7:传统 CV 之高斯滤波实战
算法
日月云棠15 小时前
各版本JDK对比:JDK 25 特性详解
java
爱理财的程序媛16 小时前
openclaw 盯盘实践
算法
用户83071968408216 小时前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide16 小时前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
IT探险家17 小时前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
花花无缺17 小时前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java