树的完全解读

124 阅读25分钟

第1章:树的基础知识

1.1 树的定义与特点

1.1.1 什么是树?

定义

树(Tree)是一种非线性的数据结构,它由n(n≥0)个有限节点组成一个具有层次关系的集合。

特点:

  • 每个节点有零个或多个子节点
  • 没有父节点的节点称为根节点
  • 每一个非根节点有且只有一个父节点
  • 除了根节点外,每个子节点可以分为多个不相交的子树
生活中的例子

文件系统:

根目录/
├── 文件夹A/
│   ├── 文件1.txt
│   └── 文件2.txt
├── 文件夹B/
│   └── 文件3.txt
└── 文件4.txt

组织架构:

CEO
├── 技术总监
│   ├── 开发经理
│   └── 测试经理
├── 市场总监
│   └── 销售经理
└── 财务总监

1.1.2 树的特点

核心特性

1. 层次性

  • 树具有明显的层次结构
  • 从根节点到叶子节点形成路径

2. 唯一性

  • 每个节点只有一个父节点(根节点除外)
  • 从根节点到任意节点有且仅有一条路径

3. 递归性

  • 树是递归定义的
  • 每个子树本身也是一棵树
与线性结构的对比
特性线性结构(数组、链表)树结构
数据关系一对一一对多
存储方式顺序或链式层次结构
查找效率O(n)O(log n)(平衡树)
应用场景简单数据存储层次关系、搜索

1.2 树的术语

1.2.1 基本术语

节点相关

节点(Node): 树中的基本单位,包含数据和指向子节点的指针

根节点(Root): 树的最顶层节点,没有父节点

叶子节点(Leaf): 没有子节点的节点,也称为终端节点

内部节点(Internal Node): 有子节点的节点

关系相关

父节点(Parent): 一个节点的直接上级节点

子节点(Child): 一个节点的直接下级节点

兄弟节点(Sibling): 具有相同父节点的节点

祖先节点(Ancestor): 从根节点到该节点的路径上的所有节点

后代节点(Descendant): 以该节点为根的子树中的所有节点

路径相关

路径(Path): 从根节点到某个节点的节点序列

路径长度(Path Length): 路径上边的数量

深度(Depth): 从根节点到该节点的路径长度

高度(Height): 从该节点到最远叶子节点的路径长度

树的高度: 根节点的高度

1.2.2 术语示例

示例树
        A (根节点)
       / \
      B   C
     / \   \
    D   E   F

术语说明:

  • 根节点: A
  • 叶子节点: D、E、F
  • 内部节点: A、B、C
  • 父节点: A是B和C的父节点
  • 子节点: B和C是A的子节点
  • 兄弟节点: B和C是兄弟节点
  • 深度: A的深度为0,B的深度为1,D的深度为2
  • 高度: D的高度为0,B的高度为1,A的高度为2

1.3 树的分类

1.3.1 按子节点数量分类

一般树

特点:

  • 每个节点可以有任意数量的子节点
  • 没有限制

示例:

        A
    /   |   \
   B    C    D
  / \   |   / \
 E   F  G  H   I
二叉树

特点:

  • 每个节点最多有两个子节点
  • 左子节点和右子节点

示例:

        A
       / \
      B   C
     / \   \
    D   E   F

1.3.2 按结构分类

完全二叉树

特点:

  • 除了最后一层,其他层都是满的
  • 最后一层从左到右填充

示例:

        A
       / \
      B   C
     / \ / \
    D  E F  G
满二叉树

特点:

  • 所有层都是满的
  • 每个节点要么是叶子节点,要么有两个子节点

示例:

        A
       / \
      B   C
     / \ / \
    D  E F  G
平衡二叉树

特点:

  • 任意节点的左右子树高度差不超过1
  • 保证查找性能

示例:

        A
       / \
      B   C
     /     \
    D       E

1.3.3 按应用分类

二叉搜索树(BST)

特点:

  • 左子树所有节点值 < 根节点值
  • 右子树所有节点值 > 根节点值
  • 中序遍历是有序的
红黑树

特点:

  • 自平衡二叉搜索树
  • 保证最坏情况下的性能
AVL树

特点:

  • 严格平衡的二叉搜索树
  • 任意节点左右子树高度差不超过1

📊 本章总结

核心要点:

  1. 树是一种非线性数据结构,具有层次性
  2. 树的基本术语:节点、根、叶子、深度、高度等
  3. 树的分类:二叉树、完全二叉树、平衡二叉树等
  4. 树结构适合表示层次关系和高效搜索

第2章:二叉树深度剖析

2.1 二叉树基础

2.1.1 二叉树的定义

定义

**二叉树(Binary Tree)**是每个节点最多有两个子节点的树结构。

特点:

  • 每个节点最多有两个子节点
  • 左子节点和右子节点
  • 可以为空(空树)
数学性质

1. 第i层最多有2^(i-1)个节点

2. 深度为k的二叉树最多有2^k - 1个节点

3. 对于任意二叉树,叶子节点数 = 度为2的节点数 + 1

2.1.2 二叉树的存储

顺序存储

使用数组存储:

// 完全二叉树可以用数组存储
// 索引关系:
// 父节点索引:i
// 左子节点索引:2*i + 1
// 右子节点索引:2*i + 2

示例:

树结构:
        A(0)
       / \
    B(1)  C(2)
    / \   /
  D(3) E(4) F(5)

数组:[A, B, C, D, E, F]
索引: 0  1  2  3  4  5

优势:

  • 内存连续,CPU缓存友好
  • 索引计算快速

劣势:

  • 不适合非完全二叉树(浪费空间)
链式存储

使用节点存储:

class TreeNode {
    int val;
    TreeNode left;   // 左子节点
    TreeNode right;  // 右子节点
    
    TreeNode(int val) {
        this.val = val;
    }
}

优势:

  • 灵活,适合任意二叉树
  • 不浪费空间

劣势:

  • 内存不连续
  • 需要额外指针空间

2.2 二叉树的遍历

2.2.1 深度优先遍历(DFS)

前序遍历(Preorder)

顺序: 根 → 左 → 右

void preorder(TreeNode root) {
    if (root == null) return;
    System.out.println(root.val);  // 访问根节点
    preorder(root.left);           // 遍历左子树
    preorder(root.right);          // 遍历右子树
}

示例:

树:     1
       / \
      2   3
     / \
    4   5

前序遍历:1 → 2 → 4 → 5 → 3
中序遍历(Inorder)

顺序: 左 → 根 → 右

void inorder(TreeNode root) {
    if (root == null) return;
    inorder(root.left);            // 遍历左子树
    System.out.println(root.val);  // 访问根节点
    inorder(root.right);           // 遍历右子树
}

示例:

树:     1
       / \
      2   3
     / \
    4   5

中序遍历:4 → 2 → 5 → 1 → 3
后序遍历(Postorder)

顺序: 左 → 右 → 根

void postorder(TreeNode root) {
    if (root == null) return;
    postorder(root.left);          // 遍历左子树
    postorder(root.right);         // 遍历右子树
    System.out.println(root.val);   // 访问根节点
}

示例:

树:     1
       / \
      2   3
     / \
    4   5

后序遍历:4 → 5 → 2 → 3 → 1

2.2.2 广度优先遍历(BFS)

层序遍历(Level Order)

顺序: 从上到下,从左到右

void levelOrder(TreeNode root) {
    if (root == null) return;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        System.out.println(node.val);
        
        if (node.left != null) queue.offer(node.left);
        if (node.right != null) queue.offer(node.right);
    }
}

示例:

树:     1
       / \
      2   3
     / \
    4   5

层序遍历:1 → 2 → 3 → 4 → 5

2.3 二叉搜索树

2.3.1 定义与性质

定义

**二叉搜索树(Binary Search Tree,BST)**是一种特殊的二叉树:

性质:

  1. 左子树所有节点的值 < 根节点的值
  2. 右子树所有节点的值 > 根节点的值
  3. 左右子树都是二叉搜索树
示例
        8
       / \
      3   10
     / \    \
    1   6    14
       / \   /
      4   7 13

验证:

  • 左子树(3, 1, 6, 4, 7)都 < 8
  • 右子树(10, 14, 13)都 > 8
  • 每个子树也满足BST性质

2.3.2 查找操作

实现原理
TreeNode search(TreeNode root, int key) {
    if (root == null || root.val == key) {
        return root;
    }
    
    if (key < root.val) {
        return search(root.left, key);   // 在左子树查找
    } else {
        return search(root.right, key);   // 在右子树查找
    }
}

时间复杂度:

  • 最好情况:O(log n) - 平衡树
  • 最坏情况:O(n) - 退化为链表
  • 平均情况:O(log n)

2.3.3 插入操作

实现原理
TreeNode insert(TreeNode root, int key) {
    if (root == null) {
        return new TreeNode(key);  // 创建新节点
    }
    
    if (key < root.val) {
        root.left = insert(root.left, key);   // 插入左子树
    } else if (key > root.val) {
        root.right = insert(root.right, key); // 插入右子树
    }
    
    return root;
}

时间复杂度: O(log n)平均,O(n)最坏

2.3.4 删除操作

三种情况

1. 删除叶子节点: 直接删除

2. 删除只有一个子节点的节点: 用子节点替换

3. 删除有两个子节点的节点: 用右子树的最小值替换

TreeNode delete(TreeNode root, int key) {
    if (root == null) return null;
    
    if (key < root.val) {
        root.left = delete(root.left, key);
    } else if (key > root.val) {
        root.right = delete(root.right, key);
    } else {
        // 找到要删除的节点
        if (root.left == null) return root.right;
        if (root.right == null) return root.left;
        
        // 有两个子节点:用右子树的最小值替换
        TreeNode min = findMin(root.right);
        root.val = min.val;
        root.right = delete(root.right, min.val);
    }
    
    return root;
}

2.4 二叉树的退化问题

2.4.1 退化场景

问题描述

当二叉搜索树插入的元素有序时,会退化为链表

示例:

插入顺序:1, 2, 3, 4, 5

退化后的树:
1
 \
  2
   \
    3
     \
      4
       \
        5

问题:

  • 查找性能从O(log n)退化为O(n)
  • 失去了二叉搜索树的优势

2.4.2 解决方案

自平衡二叉搜索树

解决方案: 使用自平衡二叉搜索树

常见实现:

  1. AVL树: 严格平衡,高度差不超过1
  2. 红黑树: 近似平衡,性能优秀
  3. B树/B+树: 多路平衡树

目标:

  • 保证树的高度为O(log n)
  • 保证查找、插入、删除的性能

📊 本章总结

核心要点:

  1. 二叉树是每个节点最多有两个子节点的树
  2. 二叉搜索树保证左小右大的性质
  3. 二叉搜索树在极端情况下会退化为链表
  4. 需要使用自平衡树来解决退化问题

第3章:平衡二叉树

3.1 平衡二叉树基础

3.1.1 定义

什么是平衡二叉树?

**平衡二叉树(Balanced Binary Tree)**是一种特殊的二叉搜索树,它保证树的高度平衡。

特点:

  • 任意节点的左右子树高度差不超过1
  • 保证查找性能为O(log n)
  • 通过旋转操作保持平衡

3.1.2 平衡因子

定义

平衡因子(Balance Factor) = 左子树高度 - 右子树高度

平衡条件:

  • 平衡因子的绝对值 <= 1
  • 即:|左子树高度 - 右子树高度| <= 1

示例:

节点A:
左子树高度 = 2
右子树高度 = 1
平衡因子 = 2 - 1 = 1  ✓(平衡)

节点B:
左子树高度 = 3
右子树高度 = 1
平衡因子 = 3 - 1 = 2  ✗(不平衡)

3.2 AVL树

3.2.1 AVL树定义

特点

AVL树是最早发明的自平衡二叉搜索树:

性质:

  • 是二叉搜索树
  • 任意节点的左右子树高度差不超过1
  • 通过旋转操作保持平衡

命名: 以发明者Adelson-Velsky和Landis的名字命名

3.2.2 AVL树的优势

严格平衡

优势:

  • 严格平衡,查找性能稳定
  • 最坏情况下也是O(log n)
  • 适合查找频繁的场景

劣势:

  • 插入和删除需要频繁旋转
  • 维护成本高

3.3 旋转操作

3.3.1 左旋(Left Rotation)

操作过程

场景: 右子树过高,需要左旋

步骤:

  1. 将右子节点提升为新的根节点
  2. 原根节点成为新根节点的左子节点
  3. 新根节点原来的左子节点成为原根节点的右子节点

示意图:

左旋前:
    A
     \
      B
     / \
    C   D

左旋后:
      B
     / \
    A   D
     \
      C
代码实现
TreeNode leftRotate(TreeNode root) {
    TreeNode newRoot = root.right;
    root.right = newRoot.left;
    newRoot.left = root;
    return newRoot;
}

3.3.2 右旋(Right Rotation)

操作过程

场景: 左子树过高,需要右旋

步骤:

  1. 将左子节点提升为新的根节点
  2. 原根节点成为新根节点的右子节点
  3. 新根节点原来的右子节点成为原根节点的左子节点

示意图:

右旋前:
      A
     /
    B
   / \
  C   D

右旋后:
    B
   / \
  C   A
     /
    D
代码实现
TreeNode rightRotate(TreeNode root) {
    TreeNode newRoot = root.left;
    root.left = newRoot.right;
    newRoot.right = root;
    return newRoot;
}

3.3.3 左右旋和右左旋

左右旋(Left-Right Rotation)

场景: 左子树的右子树过高

步骤:

  1. 先对左子节点左旋
  2. 再对根节点右旋
右左旋(Right-Left Rotation)

场景: 右子树的左子树过高

步骤:

  1. 先对右子节点右旋
  2. 再对根节点左旋

📊 本章总结

核心要点:

  1. 平衡二叉树保证树的高度平衡
  2. AVL树是严格平衡的二叉搜索树
  3. 通过旋转操作保持平衡
  4. 旋转操作包括左旋、右旋、左右旋、右左旋

第4章:红黑树深度剖析

4.1 为什么使用红黑树?

4.1.1 二叉搜索树的退化问题

问题描述

二叉搜索树在极端情况下会退化为链表:

示例:

插入顺序:1, 2, 3, 4, 5, 6, 7

退化后的树:
1
 \
  2
   \
    3
     \
      4
       \
        5
         \
          6
           \
            7

问题:

  • 查找性能从O(log n)退化为O(n)
  • 失去了二叉搜索树的优势

4.1.2 解决方案

自平衡二叉搜索树

解决方案: 使用自平衡二叉搜索树

常见实现:

  1. AVL树: 严格平衡,但维护成本高
  2. 红黑树: 近似平衡,性能优秀(推荐)
  3. B树/B+树: 多路平衡树

红黑树的优势:

  • 近似平衡,性能优秀
  • 插入和删除的维护成本低于AVL树
  • 适合频繁插入删除的场景

4.1.3 在HashMap中的应用

JDK8的优化

JDK7: HashMap使用数组+链表

JDK8: HashMap使用数组+链表/红黑树

优化原因:

  • 当链表长度超过8时,转为红黑树
  • 避免哈希冲突严重时性能退化
  • 保证最坏情况下也是O(log n)

触发条件:

  • 链表长度 >= 8
  • 数组长度 >= 64

4.2 红黑树五大性质

4.2.1 性质详解

性质1:节点是红色或黑色

定义: 每个节点要么是红色,要么是黑色

作用: 通过颜色标记来维护平衡

性质2:根节点是黑色

定义: 根节点必须是黑色

作用: 保证树的稳定性

性质3:叶子节点(NIL)是黑色

定义: 所有叶子节点(NIL节点,空节点)都是黑色

注意: 红黑树中的叶子节点是指NIL节点,不是实际存储数据的节点

性质4:红节点的子节点必须是黑色

定义: 如果一个节点是红色,那么它的两个子节点都是黑色

作用: 保证不会出现连续的红节点

推论: 从根节点到任意叶子节点的路径上,红节点数量 <= 黑节点数量

性质5:从任一节点到叶子节点的黑节点数相同

定义: 从任意节点到其每个叶子节点的所有路径上,黑色节点的数量相同

作用: 这是红黑树平衡的关键性质

示例:

从根节点到叶子节点的路径:
路径1:黑 → 红 → 黑 → 黑(3个黑节点)
路径2:黑 → 黑 → 红 → 黑(3个黑节点)
路径3:黑 → 黑 → 黑(3个黑节点)

4.2.2 性质保证的平衡性

平衡性分析

从性质5可以推导:

  • 从根节点到最远叶子节点的路径长度 <= 从根节点到最近叶子节点的路径长度的2倍
  • 这保证了红黑树是近似平衡的

数学证明:

  • 最短路径:全黑节点,长度为h
  • 最长路径:黑红交替,长度为2h
  • 最长路径 <= 2 * 最短路径

结论: 红黑树的高度 <= 2 * log(n+1)


4.3 红黑树平衡操作

4.3.1 左旋和右旋

左旋(Left Rotation)

操作过程:

左旋前:
    A
     \
      B
     / \
    C   D

左旋后:
      B
     / \
    A   D
     \
      C

代码实现:

TreeNode leftRotate(TreeNode root) {
    TreeNode newRoot = root.right;
    root.right = newRoot.left;
    newRoot.left = root;
    
    // 更新颜色
    newRoot.color = root.color;
    root.color = RED;
    
    return newRoot;
}
右旋(Right Rotation)

操作过程:

右旋前:
      A
     /
    B
   / \
  C   D

右旋后:
    B
   / \
  C   A
     /
    D

代码实现:

TreeNode rightRotate(TreeNode root) {
    TreeNode newRoot = root.left;
    root.left = newRoot.right;
    newRoot.right = root;
    
    // 更新颜色
    newRoot.color = root.color;
    root.color = RED;
    
    return newRoot;
}

4.3.2 插入后的调整

插入策略

基本步骤:

  1. 按照二叉搜索树的方式插入新节点
  2. 将新节点标记为红色
  3. 如果违反红黑树性质,进行调整
调整情况

情况1:父节点是黑色

  • 不需要调整,直接插入

情况2:父节点是红色,叔叔节点是红色

  • 将父节点和叔叔节点变黑
  • 将祖父节点变红
  • 继续向上调整

情况3:父节点是红色,叔叔节点是黑色(或不存在)

  • 需要旋转调整
  • 根据插入位置选择左旋或右旋

4.3.3 删除后的调整

删除策略

基本步骤:

  1. 按照二叉搜索树的方式删除节点
  2. 如果删除的是红色节点,不需要调整
  3. 如果删除的是黑色节点,需要调整
调整情况

情况1:替代节点是红色

  • 将替代节点变黑,完成

情况2:替代节点是黑色,兄弟节点是红色

  • 旋转调整
  • 继续处理

情况3:替代节点是黑色,兄弟节点是黑色

  • 根据兄弟节点的子节点情况调整
  • 可能需要继续向上调整

4.4 红黑树 vs AVL树

4.4.1 对比分析

平衡性
特性AVL树红黑树
平衡程度严格平衡近似平衡
高度差<= 1<= 2倍
查找性能略好略差
维护成本
特性AVL树红黑树
插入旋转可能多次最多2次
删除旋转可能多次最多3次
维护成本
适用场景

AVL树:

  • 查找频繁,插入删除较少
  • 需要严格平衡

红黑树:

  • 插入删除频繁
  • 需要近似平衡即可
  • HashMap、TreeMap使用红黑树

4.4.2 为什么HashMap选择红黑树?

原因分析

1. 性能平衡:

  • 查找性能:O(log n),虽然略差于AVL树,但足够好
  • 插入删除:维护成本低,性能更好

2. 实际场景:

  • HashMap中插入删除频繁
  • 红黑树的综合性能更好

3. 实现复杂度:

  • 红黑树的实现相对简单
  • 维护成本低

结论: 红黑树在综合性能上更适合HashMap的场景


📊 本章总结

核心要点:

  1. 红黑树是自平衡二叉搜索树,解决BST退化问题
  2. 红黑树有五大性质,保证近似平衡
  3. 通过旋转操作保持平衡
  4. 红黑树在综合性能上优于AVL树,适合HashMap

第5章:红黑树在Java中的应用

5.1 TreeMap中的红黑树

5.1.1 数据结构

节点定义
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;    // 左子节点
    Entry<K,V> right;   // 右子节点
    Entry<K,V> parent;  // 父节点
    boolean color = BLACK;  // 节点颜色
}

特点:

  • 使用红黑树存储键值对
  • 节点包含颜色信息
  • 支持父节点指针,便于操作

5.1.2 插入操作

实现流程

TreeMap的put()方法:

  1. 按照二叉搜索树的方式查找插入位置
  2. 创建新节点,标记为红色
  3. 插入节点
  4. 调用fixAfterInsertion()调整红黑树

fixAfterInsertion()方法:

  • 检查是否违反红黑树性质
  • 根据情况旋转和变色
  • 保证红黑树平衡

5.1.3 删除操作

实现流程

TreeMap的remove()方法:

  1. 按照二叉搜索树的方式查找节点
  2. 删除节点
  3. 调用fixAfterDeletion()调整红黑树

fixAfterDeletion()方法:

  • 检查是否违反红黑树性质
  • 根据情况旋转和变色
  • 保证红黑树平衡

5.2 HashMap中的红黑树

5.2.1 转换条件

链表转红黑树

触发条件:

  • 链表长度 >= 8
  • 数组长度 >= 64

代码:

if (binCount >= TREEIFY_THRESHOLD - 1) {
    treeifyBin(tab, hash);
}

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();  // 如果数组长度 < 64,先扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 转换为红黑树
        TreeNode<K,V> hd = null, tl = null;
        // ... 转换逻辑
    }
}

5.2.2 节点定义

TreeNode节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // 父节点
    TreeNode<K,V> left;     // 左子节点
    TreeNode<K,V> right;    // 右子节点
    TreeNode<K,V> prev;     // 前驱节点(用于链表)
    boolean red;            // 节点颜色
}

特点:

  • 继承LinkedHashMap.Entry
  • 包含红黑树需要的指针
  • 保留prev指针,支持链表操作

5.2.3 红黑树转链表

转换条件

触发条件:

  • 红黑树节点数 < 6

代码:

if (lc <= UNTREEIFY_THRESHOLD) {
    untreeify(map);  // 转换为链表
}

原因:

  • 当节点数较少时,链表的性能已经足够好
  • 减少维护红黑树的成本

5.3 性能分析

5.3.1 时间复杂度

各种操作
操作链表红黑树
查找O(n)O(log n)
插入O(1)O(log n)
删除O(n)O(log n)
实际性能

正常情况(链表长度 < 8):

  • 使用链表,性能O(1)平均

冲突严重(链表长度 >= 8):

  • 使用红黑树,性能O(log n)
  • 避免退化为O(n)

5.3.2 空间复杂度

空间占用

链表:

  • 每个节点:数据 + next指针
  • 空间复杂度:O(n)

红黑树:

  • 每个节点:数据 + left + right + parent + color
  • 空间复杂度:O(n),但常数更大

权衡:

  • 红黑树占用更多空间
  • 但保证了性能

📊 本章总结

核心要点:

  1. TreeMap完全基于红黑树实现
  2. HashMap在链表长度>=8时转为红黑树
  3. 红黑树保证最坏情况下也是O(log n)
  4. 在冲突严重时,红黑树性能明显优于链表

第6章:红黑树高频面试题精选

6.1 树与二叉树面试题

面试题1:什么是树?树和线性结构有什么区别?

答案:

树的定义:

  • 树是一种非线性数据结构
  • 由n个节点组成,具有层次关系
  • 每个节点有零个或多个子节点

与线性结构的区别:

特性线性结构树结构
数据关系一对一一对多
存储方式顺序或链式层次结构
查找效率O(n)O(log n)(平衡树)
应用场景简单数据存储层次关系、搜索
面试题2:什么是二叉树?二叉树有哪些性质?

答案:

二叉树定义:

  • 每个节点最多有两个子节点的树
  • 左子节点和右子节点

性质:

  1. 第i层最多有2^(i-1)个节点
  2. 深度为k的二叉树最多有2^k - 1个节点
  3. 对于任意二叉树,叶子节点数 = 度为2的节点数 + 1
面试题3:二叉树的遍历方式有哪些?它们有什么区别?

答案:

深度优先遍历(DFS):

  • 前序遍历:根 → 左 → 右
  • 中序遍历:左 → 根 → 右
  • 后序遍历:左 → 右 → 根

广度优先遍历(BFS):

  • 层序遍历:从上到下,从左到右

区别:

  • DFS:使用递归或栈,适合深度搜索
  • BFS:使用队列,适合层次遍历
面试题4:什么是二叉搜索树?它有什么特点?

答案:

二叉搜索树(BST)定义:

  • 是特殊的二叉树
  • 左子树所有节点值 < 根节点值
  • 右子树所有节点值 > 根节点值
  • 左右子树都是BST

特点:

  • 中序遍历是有序的
  • 查找、插入、删除的时间复杂度:O(log n)平均,O(n)最坏
面试题5:二叉搜索树在什么情况下会退化?如何解决?

答案:

退化情况:

  • 插入有序数据时,会退化为链表
  • 例如:插入1, 2, 3, 4, 5

退化后的树:

1
 \
  2
   \
    3
     \
      4
       \
        5

解决方案:

  • 使用自平衡二叉搜索树
  • 常见实现:AVL树、红黑树、B树

6.2 平衡二叉树面试题

面试题6:什么是平衡二叉树?平衡因子是什么?

答案:

平衡二叉树定义:

  • 特殊的二叉搜索树
  • 任意节点的左右子树高度差不超过1

平衡因子:

  • 平衡因子 = 左子树高度 - 右子树高度
  • 平衡条件:|平衡因子| <= 1
面试题7:什么是AVL树?它有什么特点?

答案:

AVL树定义:

  • 最早发明的自平衡二叉搜索树
  • 任意节点的左右子树高度差不超过1

特点:

  • 严格平衡
  • 查找性能稳定O(log n)
  • 插入删除需要频繁旋转
  • 维护成本高
面试题8:了解红黑树的左旋和右旋操作吗?它们是为了解决什么问题?

答案:

左旋操作:

  • 场景:右子树过高
  • 将右子节点提升为根节点
  • 原根节点成为新根节点的左子节点

右旋操作:

  • 场景:左子树过高
  • 将左子节点提升为根节点
  • 原根节点成为新根节点的右子节点

解决的问题:

  • 调整树的结构,保持平衡
  • 在插入和删除后恢复红黑树性质

6.3 红黑树核心面试题

面试题9:HashMap为什么在JDK8中引入红黑树?

答案:

原因:

  1. 解决性能退化: 当哈希冲突严重时,链表会变长,查找性能从O(1)退化为O(n)
  2. 保证性能下限: 红黑树保证最坏情况下也是O(log n)
  3. 实际场景: 当链表长度>=8时,转为红黑树,避免性能问题

优化效果:

  • 正常情况:使用链表,O(1)平均
  • 冲突严重:使用红黑树,O(log n)
  • 避免退化为O(n)
面试题10:红黑树和AVL树(平衡二叉搜索树)有什么区别?为什么HashMap选择红黑树?

答案:

主要区别:

特性AVL树红黑树
平衡程度严格平衡近似平衡
高度差<= 1<= 2倍
查找性能略好略差
插入旋转可能多次最多2次
删除旋转可能多次最多3次
维护成本

为什么选择红黑树:

  1. 综合性能更好: 虽然查找略差,但插入删除性能更好
  2. 维护成本低: 旋转次数少,维护成本低
  3. 适合场景: HashMap中插入删除频繁,红黑树更适合
面试题11:二叉搜索树在极端情况下会退化成链表,红黑树如何避免?

答案:

红黑树的避免机制:

  1. 五大性质保证:

    • 性质5:从任一节点到叶子节点的黑节点数相同
    • 这保证了树的高度 <= 2 * log(n+1)
  2. 自平衡机制:

    • 插入和删除后自动调整
    • 通过旋转和变色保持平衡
  3. 近似平衡:

    • 虽然不是严格平衡,但保证了性能
    • 最长路径 <= 2 * 最短路径

效果:

  • 避免了退化为链表
  • 保证查找性能为O(log n)
面试题12:简述红黑树的五个基本性质。

答案:

性质1:节点是红色或黑色

  • 每个节点要么是红色,要么是黑色

性质2:根节点是黑色

  • 根节点必须是黑色

性质3:叶子节点(NIL)是黑色

  • 所有叶子节点(NIL节点)都是黑色

性质4:红节点的子节点必须是黑色

  • 如果一个节点是红色,那么它的两个子节点都是黑色
  • 保证不会出现连续的红节点

性质5:从任一节点到叶子节点的黑节点数相同

  • 从任意节点到其每个叶子节点的所有路径上,黑色节点的数量相同
  • 这是红黑树平衡的关键性质
面试题13:红黑树的插入和删除操作如何保持平衡?

答案:

插入操作:

  1. 按照BST方式插入,标记为红色
  2. 如果违反性质4(连续红节点),调整:
    • 情况1:父节点是黑色,不需要调整
    • 情况2:父节点和叔叔节点都是红色,变色
    • 情况3:父节点是红色,叔叔节点是黑色,旋转

删除操作:

  1. 按照BST方式删除
  2. 如果删除的是黑色节点,调整:
    • 情况1:替代节点是红色,变黑
    • 情况2:兄弟节点是红色,旋转
    • 情况3:兄弟节点是黑色,根据子节点调整

调整方式:

  • 旋转:左旋、右旋
  • 变色:改变节点颜色
面试题14:红黑树的时间复杂度是多少?

答案:

各种操作:

操作时间复杂度
查找O(log n)
插入O(log n)
删除O(log n)

原因:

  • 红黑树的高度 <= 2 * log(n+1)
  • 所有操作最多遍历树的高度
  • 因此时间复杂度为O(log n)
面试题15:红黑树和普通二叉搜索树相比有什么优势?

答案:

优势:

  1. 避免退化: 不会退化为链表,保证性能
  2. 性能稳定: 最坏情况下也是O(log n)
  3. 维护成本低: 相比AVL树,维护成本更低
  4. 综合性能好: 在查找、插入、删除之间取得平衡

普通BST的问题:

  • 可能退化为链表,性能O(n)
  • 性能不稳定

6.4 红黑树应用面试题

面试题16:TreeMap是如何使用红黑树的?

答案:

实现方式:

  • TreeMap完全基于红黑树实现
  • 所有键值对存储在红黑树中
  • 按键排序,支持范围查询

特点:

  • 插入、删除、查找都是O(log n)
  • 支持导航方法(ceilingKey、floorKey等)
  • 线程不安全
面试题17:HashMap在什么情况下会使用红黑树?

答案:

转换条件:

  • 链表长度 >= 8
  • 数组长度 >= 64

转换过程:

  1. 检查链表长度
  2. 如果 >= 8,检查数组长度
  3. 如果数组长度 >= 64,转换为红黑树
  4. 否则,先扩容

转换回链表:

  • 红黑树节点数 < 6时,转换回链表
面试题18:红黑树在HashMap中的性能提升有多大?

答案:

性能对比:

场景链表红黑树
正常情况O(1)平均O(log n)
冲突严重O(n)O(log n)

提升效果:

  • 正常情况:链表性能更好
  • 冲突严重:红黑树避免O(n)退化
  • 保证最坏情况下也是O(log n)

实际效果:

  • 在冲突严重时,性能提升明显
  • 避免了极端情况下的性能问题
面试题19:如何实现一个简单的红黑树?

答案:

基本结构:

class RBTree {
    private static final boolean RED = true;
    private static final boolean BLACK = false;
    
    class Node {
        int key;
        Node left, right, parent;
        boolean color;
    }
    
    // 左旋
    Node leftRotate(Node root) {
        Node newRoot = root.right;
        root.right = newRoot.left;
        newRoot.left = root;
        newRoot.color = root.color;
        root.color = RED;
        return newRoot;
    }
    
    // 右旋
    Node rightRotate(Node root) {
        Node newRoot = root.left;
        root.left = newRoot.right;
        newRoot.right = root;
        newRoot.color = root.color;
        root.color = RED;
        return newRoot;
    }
    
    // 插入
    Node insert(Node root, int key) {
        // 1. 按照BST方式插入
        // 2. 标记为红色
        // 3. 调整红黑树
        return root;
    }
}
面试题20:红黑树和B树、B+树有什么区别?

答案:

主要区别:

特性红黑树B树B+树
节点子节点数最多2个多个多个
应用场景内存数据结构磁盘存储数据库索引
查找方式二分查找多路查找多路查找
叶子节点存储数据存储数据只存储索引

选择建议:

  • 内存数据结构:红黑树
  • 磁盘存储:B树或B+树
  • 数据库索引:B+树