Java基础快速入门:TreeSet底层数据结构——树

本文纲要

  1. 引言
  2. 树的基本结构:节点与二叉树
    2.1 节点的构成
    2.2 什么是二叉树
    2.3 树的基本术语
  3. 二叉查找树(二叉排序树)
    3.1 定义与特点
    3.2 添加节点过程
    3.3 代码演示:节点类与插入
  4. 平衡二叉树
    4.1 为什么需要平衡
    4.2 定义与判断
  5. 平衡二叉树的旋转机制
    5.1 左旋(Left Rotation)
    5.2 右旋(Right Rotation)
    5.3 左左、左右、右右、右左四种情况
  6. 总结

引言

在 Java 集合框架中,TreeSet 是一种有序的 Set 实现。它的底层数据结构是红黑树(一种自平衡的二叉查找树)。要深入理解 TreeSet 的工作原理,首先要掌握树结构的基础知识。

本文将从最基础的二叉树讲起,逐步过渡到二叉查找树、平衡二叉树,并详细讲解平衡二叉树维持平衡的核心操作——左旋与右旋。这些内容是理解 TreeMapTreeSet 底层实现的关键。

树的基本结构:节点与二叉树

1 ) 节点的构成

在树结构中,每一个元素称为一个节点(Node)。每一个节点通常包含四部分信息:
父节点的地址
自身存储的值
左子节点的地址
右子节点的地址

可以用如下 Java 类来表示:

public class TreeNode {
   // 父节点 
   TreeNode parent;
   // 存储的值 
   int value;
   // 左子节点 
   TreeNode left;
   // 右子节点 
   TreeNode right;

   public TreeNode(int value) {
       this.value = value;
   }
}

一个节点如果没有父节点(即为根节点),则 parentnull;如果没有左子节点或右子节点,则相应的 leftrightnull

以下图为例:

节点 A
value:?
parent:null
left: B
right: C

节点 B
value:?
parent: A
left: null
right: null

节点 C
value:?
parent: A
left: null
right: null

A 的左子节点记录了 B 的地址,右子节点记录了 C 的地址;B 和 C 的父节点记录了 A 的地址。B 和 C 没有子节点,所以它们的左、右子节点均为 null。

2 ) 什么是二叉树

二叉树是指任意一个节点的子节点数量不超过 2 的树。子节点的数量在树结构中有一个专业术语叫做度(degree)。二叉树中每个节点的度 ≤ 2。

一个简单的二叉树示例:

2

4

5

7

10

11

12

null

null

3 ) 树的基本术语

根节点(Root Node):树的最顶层节点,没有父节点。
子节点(Child Node):某个节点的直接后代。
父节点(Parent Node):某个节点的直接前驱。
叶子节点(Leaf Node):度为零的节点,没有子节点。
树的高度(Height):树中节点的最大层数。
左子树 / 右子树:以根节点的左子节点为根的子树称为左子树,以右子节点为根的称为右子树。

以高度为 4 的二叉树为例:

2

4

5

7

10

11

12

null

null

null

null

null

null

null

null

  • 第 1 层:节点 7
  • 第 2 层:节点 4、10
  • 第 3 层:节点 2、5、11
  • 第 4 层:节点 12

树的高度 = 层数 = 4。

  • 7 是根节点。
  • 4 是 7 的左子节点,10 是 7 的右子节点。
  • 蓝色虚线框住的部分(以 4 为根的子树)是根节点的左子树,高度为 3。
  • 绿色虚线框住的部分(以 10 为根的子树)是根节点的右子树,高度为 3。

二叉查找树(二叉排序树)

1 ) 定义与特点

普通的二叉树在存储数据时没有任何规律,查找某个节点需要遍历整棵树,效率很低。
为了提高查找效率,引入了二叉查找树(Binary Search Tree,BST),也称为二叉排序树或二叉搜索树。

二叉查找树必须满足:

  1. 首先是一棵二叉树(每个节点最多两个子节点)。
  2. 对于任意节点,其左子节点的值 < 该节点的值
  3. 对于任意节点,其右子节点的值 > 该节点的值

简单理解:任意节点从左到右依次增大

示例:

2

4

5

7

9

10

11

  • 7 的左子节点 4 < 7,右子节点 10 > 7。
  • 4 的左子节点 2 < 4,右子节点 5 > 4。
  • 10 的左子节点 9 < 10,右子节点 11 > 10。

这是一棵标准的二叉查找树。

2 ) 添加节点过程

向二叉查找树中添加节点时,遵循小的存左边,大的存右边,相等的值不存的规则。
具体过程如下:

  1. 如果树为空,直接将新节点作为根节点。
  2. 如果树不为空,从根节点开始比较:
    • 若新节点的值 < 当前节点的值,且当前节点的左子节点为空,则成为左子节点;否则进入左子树继续比较。
    • 若新节点的值 > 当前节点的值,且当前节点的右子节点为空,则成为右子节点;否则进入右子树继续比较。
    • 若新节点的值 == 当前节点的值,不存储(或根据具体需求处理)。

添加实例:依次添加 7、4、10、5。

  1. 添加 7:树空,7 成为根节点。
  2. 添加 4:4 < 7,且 7 的左子节点为空,所以 4 成为 7 的左子节点。
  3. 添加 10:10 > 7,且 7 的右子节点为空,所以 10 成为 7 的右子节点。
  4. 添加 5:5 < 7,向左走;5 > 4,且 4 的右子节点为空,所以 5 成为 4 的右子节点。

结果:

4

5

7

10

如果继续添加 12、13,会出现“一边倒”的情况,这在后面会引出平衡二叉树的需求。

3 ) 代码演示:节点类与插入

下面使用 Java 代码演示二叉查找树的节点结构和插入过程。

节点类(包含父节点引用,以备平衡树使用):

class TreeNode {
    int value;
    TreeNode parent;
    TreeNode left;
    TreeNode right;
 
    public TreeNode(int value) {
        this.value = value;
    }
 
    // 插入节点(二叉查找树规则)
    public void insert(int newValue) {
        if (newValue < this.value) {
            if (left == null) {
                left = new TreeNode(newValue);
                left.parent = this;
            } else {
                left.insert(newValue);
            }
        } else if (newValue > this.value) {
            if (right == null) {
                right = new TreeNode(newValue);
                right.parent = this;
            } else {
                right.insert(newValue);
            }
        }
        // 相等则不处理 
    }
}

构建树并输出结构:

public class BSTDemo {
    public static void main(String[] args) {
        TreeNode root = new TreeNode(7);
        root.insert(4);
        root.insert(10);
        root.insert(5);
        // 输出树的结构(先序)
        printTree(root, "");
    }
 
    public static void printTree(TreeNode node, String prefix) {
        if (node == null) return;
        System.out.println(prefix + "├─ " + node.value);
        if (node.left != null || node.right != null) {
            if (node.left != null) {
                printTree(node.left, prefix + "│   ");
            } else {
                System.out.println(prefix + "│   ├─ null");
            }
            if (node.right != null) {
                printTree(node.right, prefix + "    ");
            } else {
                System.out.println(prefix + "    ├─ null");
            }
        }
    }
}

输出示例:

├─ 7 
│   ├─ 4 
│   │   ├─ null 
│   │   ├─ 5 
│   │       ├─ null 
│   │       ├─ null 
│   ├─ 10 
│       ├─ null 
│       ├─ null 

平衡二叉树

1 ) 为什么需要平衡

二叉查找树的插入顺序会影响树的形状。例如按 7、10、11、12、13 的顺序依次插入:

7

10

11

12

13

整棵树退化成了一条链表。此时查找节点 13 需要比较 5 次,效率降低。树的高度差越大,查询效率就越接近 O(n)。为了避免这种情况,需要让树的左右子树高度尽可能接近,即平衡

2 ) 定义与判断

平衡二叉树(AVL 树是最典型的一种)的定义:

  1. 二叉树中,左右子树的高度差不超过 1
  2. 任意节点的左右子树也是平衡二叉树

判断示例:

二叉树是否平衡原因
文本描述:根节点 7,左子树高度 3,右子树高度 4,但节点 10 的左子树高度 0,右子树高度 3,差超过 1节点 10 左右高度差为 3 − 0 = 3 > 1
文本描述:节点 4 左子树高度 2,右子树高度 0节点 4 左右高度差 2 − 0 = 2 > 1
文本描述:根节点 7,左子树高度 2,右子树高度 1;其他节点高度差均 ≤ 1所有节点高度差都 ≤ 1
文本描述:根节点 7 左右高度均 2;节点 4 左高度 1,右高度 0,差 1所有节点高度差 ≤ 1

平衡二叉树的查询效率始终保持在 O(log n) 级别,因为树的高度被控制在对数级别。

平衡二叉树的旋转机制

当向平衡二叉树中添加一个节点后,可能导致树不再平衡。此时,需要通过左旋右旋操作来恢复平衡。

1 ) 左旋(Left Rotation)

触发条件:右子树过高(右侧节点偏多),需要将右侧节点向左旋转。

左旋步骤

  1. 将原先的右子节点(设为 R)提升为新的根节点。
  2. 原先的根节点(设为 E)降级为 R 的左子节点。
  3. R 原先的左子节点移交给 E,作为 E 的右子节点。

简单左旋图解

旋转前:

E 根

左子节点

R

RL

RR

旋转后:

R 新根

E 左子节点

RR 右子节点

原左子节点

RL 原 R 的左子节点

复杂左旋图解(R 节点有一个左子节点):

旋转前:

4

7

9

10

11

12

  • 添加节点 12 后,右子树过高,触发左旋。
  • 先忽略 9(10 的左子节点),将 10 提升为新根,7 降级为 10 的左子节点。
  • 处理 9:原来 9 是 10 的左子节点,旋转后无法再作为 10 的左子节点(因为 7 已经是左子节点了),因此将 9 移交给降级后的 7,作为 7 的右子节点。

旋转后:

4

7

9

10

11

12

Java 代码演示左旋(假设节点包含父指针):

// 左旋操作,以当前节点为根进行左旋 
public TreeNode rotateLeft() {
    TreeNode newRoot = this.right;  // 新根是当前的右子节点 
    if (newRoot == null) return this;
 
    // 当前节点成为新根的左子节点 
    this.right = newRoot.left;
    if (newRoot.left != null) {
        newRoot.left.parent = this;
    }
 
    // 新根替换当前节点的位置 
    newRoot.parent = this.parent;
    if (this.parent != null) {
        if (this.parent.left == this) {
            this.parent.left = newRoot;
        } else {
            this.parent.right = newRoot;
        }
    }
 
    // 设置当前节点为新根的左子节点 
    newRoot.left = this;
    this.parent = newRoot;
 
    return newRoot; // 返回新的根节点 
}

2 ) 右旋(Right Rotation)

触发条件:左子树过高,需要将左侧节点向右旋转。

右旋步骤:

  1. 将原先的左子节点(设为 L)提升为新的根节点。
  2. 原先的根节点(设为 E)降级为 L 的右子节点。
  3. L 原先的右子节点移交给 E,作为 E 的左子节点。

简单右旋图解

旋转前:

E 根

L

右子节点

LL

LR

旋转后:

L 新根

LL

E 右子节点

LR

右子节点

复杂右旋图解(L 节点有一个右子节点 5):

旋转前:

1

2

4

5

7

10

  • 添加节点 1 后,左子树过高,触发右旋。
  • 先忽略 5(4 的右子节点),将 4 提升为新根,7 降级为 4 的右子节点。
  • 处理 5:原来 5 是 4 的右子节点,旋转后交给降级后的 7,作为 7 的左子节点。

旋转后:

1

2

4

5

7

10

Java 代码演示右旋

// 右旋操作,以当前节点为根进行右旋 
public TreeNode rotateRight() {
    TreeNode newRoot = this.left;   // 新根是当前的左子节点 
    if (newRoot == null) return this;
 
    // 当前节点成为新根的右子节点 
    this.left = newRoot.right;
    if (newRoot.right != null) {
        newRoot.right.parent = this;
    }
 
    // 新根替换当前节点的位置 
    newRoot.parent = this.parent;
    if (this.parent != null) {
        if (this.parent.left == this) {
            this.parent.left = newRoot;
        } else {
            this.parent.right = newRoot;
        }
    }
 
    // 设置当前节点为新根的右子节点 
    newRoot.right = this;
    this.parent = newRoot;
 
    return newRoot; // 返回新的根节点 
}

3 ) 左左、左右、右右、右左四种情况

当插入节点导致失衡时,根据插入位置的不同,分为四种情况,分别采用不同的旋转组合来恢复平衡。

情况插入位置失衡特征恢复操作
左左 (LL)根节点左子树的左子树左子树高度 − 右子树高度 > 1,且左子节点的左子树高一次右旋
左右 (LR)根节点左子树的右子树左子树高度 − 右子树高度 > 1,且左子节点的右子树高先局部左旋,再整体右旋
右右 (RR)根节点右子树的右子树右子树高度 − 左子树高度 > 1,且右子节点的右子树高一次左旋
右左 (RL)根节点右子树的左子树右子树高度 − 左子树高度 > 1,且右子节点的左子树高先局部右旋,再整体左旋

左左 (LL) 示例:

一次右旋后平衡

4

2

7

1

5

10

插入后失衡

7

4

10

2

5

1

插入后失衡

一次右旋后平衡

左右 (LR) 示例(在左子树的右子树插入 6):

失衡树:

2

4

5

6

7

10

  • 第一步:对 4 的右子树进行左旋(局部),5 上提,4 下移。
  • 第二步:得到类似“左左”的结构后,再对整棵树进行右旋。

旋转过程:

第二步整体右旋结果

5

4

7

2

6

10

局部左旋结果

7

5

10

4

6

2

第一步局部左旋

7

4

10

2

5

6

第一步局部左旋

局部左旋结果

第二步整体右旋结果

右右 (RR)、右左 (RL) 与左左、左右镜像对称,不再赘述。

判断与恢复平衡的伪代码:

// 插入节点后,从插入点向上回溯,检查每个节点的平衡因子 
void rebalance(TreeNode node) {
    while (node != null) {
        // 计算平衡因子 = 左子树高度 - 右子树高度 
        int balance = getHeight(node.left) - getHeight(node.right);
 
        if (balance > 1) {                 // 左子树过高 
            if (getHeight(node.left.left) >= getHeight(node.left.right)) {
                // 左左情况 → 一次右旋 
                node = node.rotateRight();
            } else {
                // 左右情况 → 先左旋左子节点,再右旋当前节点 
                node.left = node.left.rotateLeft();
                node = node.rotateRight();
            }
        } else if (balance < -1) {         // 右子树过高 
            if (getHeight(node.right.right) >= getHeight(node.right.left)) {
                // 右右情况 → 一次左旋 
                node = node.rotateLeft();
            } else {
                // 右左情况 → 先右旋右子节点,再左旋当前节点 
                node.right = node.right.rotateRight();
                node = node.rotateLeft();
            }
        } else {
            // 平衡,无需旋转 
        }
        // 向上回溯 
        node = node.parent;
    }
}

总结

本文从最基本的节点结构开始,循序渐进地介绍了二叉树、二叉查找树和平衡二叉树的概念与操作,并重点讲解了平衡二叉树通过左旋、右旋维持平衡的机制。

理解这些数据结构是学习 Java 中 TreeSetTreeMap 底层红黑树的基础。红黑树是一种更复杂的自平衡二叉查找树,它同样依赖旋转操作来保持近似平衡,从而保证查找、插入、删除的最坏时间复杂度为 O(log n)。

联系红黑树的结构和规则,进一步完善对 TreeSet 底层实现的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值