左哥算法 - 二叉树(二)

175 阅读11分钟

红黑树基本知识

1. 什么是红黑树?

想象一个纪律严明的军队方阵:

graph TD
    7B((7黑)) --> 3B((3黑))
    7B --> 10B((10黑))
    3B --> 1R((1红))
    3B --> 5R((5红))
    10B --> 9R((9红))
    10B --> 12R((12红))
    
    style 7B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 10B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 1R fill:#f66,stroke:#333,stroke-width:2px
    style 5R fill:#f66,stroke:#333,stroke-width:2px
    style 9R fill:#f66,stroke:#333,stroke-width:2px
    style 12R fill:#f66,stroke:#333,stroke-width:2px

红黑树的基本结构

class RBTree<K extends Comparable<K>, V> {
    private static final boolean RED = true;
    private static final boolean BLACK = false;
    
    class Node {
        K key;
        V value;
        Node left, right, parent;
        boolean color;  // true表示红色,false表示黑色
        
        Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.color = RED;  // 新节点默认为红色
        }
    }
    
    private Node root;
}

2.为什么红黑树查询比较快?

让我用图解方式解释为什么红黑树的查询效率高。

1. 平衡性保证
graph TD
    subgraph 平衡的红黑树
        7B((7黑)) --> 3B((3黑))
        7B --> 11B((11黑))
        3B --> 1R((1红))
        3B --> 5R((5红))
        11B --> 9R((9红))
        11B --> 13R((13红))
    end

特点:

1. 从根到任何叶子的最长路径不会超过最短路径的2倍
2. 黑节点的分布保证了基本的平衡性
2. 对比普通二叉搜索树
graph TD
    subgraph 不平衡的二叉搜索树
        1B((1)) --> 2B((2))
        2B --> 3B((3))
        3B --> 4B((4))
        4B --> 5B((5))
        5B --> 6B((6))
    end

查找6的过程:

  • 普通二叉树:需要6步
  • 红黑树:最多3步
3. 时间复杂度分析

对于n个节点的树:

普通二叉搜索树:
- 最好情况:O(logn)
- 最坏情况:O(n)
- 平均情况:O(logn)

红黑树:
- 最好情况:O(logn)
- 最坏情况:O(logn)
- 平均情况:O(logn)
4. 具体查找过程示例

假设查找值为9:

graph TD
    subgraph 查找步骤
        7B((7黑<br>步骤1)) --> 3B((3黑))
        7B --> 11B((11黑<br>步骤2))
        3B --> 1R((1红))
        3B --> 5R((5红))
        11B --> 9R((9红<br>步骤3))
        11B --> 13R((13红))
    end
    
    style 7B fill:#f96,stroke:#333,stroke-width:2px
    style 11B fill:#f96,stroke:#333,stroke-width:2px
    style 9R fill:#f96,stroke:#333,stroke-width:2px

查找步骤:

  1. 比较7,向右
  2. 比较11,向左
  3. 找到9
5. 为什么快?
1. 保证的平衡性
graph TD
    subgraph 高度保证
        A[树高度] --> B[最多2logn]
        B --> C[查找次数有上限]
    end
2. 自动平衡
graph TD
    subgraph 自平衡机制
        A[插入/删除] --> B[旋转和变色]
        B --> C[保持平衡]
        C --> D[防止退化]
    end
6. 实际应用中的性能
// TreeMap的查找操作
public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

// 二分查找的过程
final Entry<K,V> getEntry(Object key) {
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = compare(key, p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}
7. 与其他数据结构比较

查找效率对比:

数组:
- 无序:O(n)
- 有序:O(logn)(二分查找)

哈希表:
- 平均:O(1)
- 最坏:O(n)

红黑树:
- 稳定O(logn)
8. 使用场景

适合的场景:

1. 需要保持数据有序
2. 需要稳定的查找性能
3. 数据量较大且经常变动

例如:

TreeMap<Integer, String> map = new TreeMap<>();
// 1. 自动保持键的顺序
// 2. 查找性能稳定
// 3. 适合范围查询

所以红黑树查询快的原因是:

  1. 保证了树的平衡性
  2. 限制了最大高度
  3. 提供了稳定的性能保证
  4. 自动维护平衡,防止性能退化

这就是为什么Java的TreeMap和TreeSet都选择使用红黑树作为底层实现!

2. 红黑树的五大规则

规则1:每个节点要么是红色,要么是黑色

就像军队中:

- 黑色:军官(指挥官)
- 红色:士兵(执行者)
// 检查节点颜色
private boolean isRed(Node node) {
    if (node == null) return BLACK;  // 空节点是黑色
    return node.color == RED;
}

// 设置节点颜色
private void setColor(Node node, boolean color) {
    if (node != null) {
        node.color = color;
    }
}
规则2:根节点必须是黑色
graph TD
    7B((总指挥官)) --> 3B((指挥官))
    7B --> 10B((指挥官))
    
    style 7B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 10B fill:#333,stroke:#333,stroke-width:2px,color:#fff
// 插入后检查并修正根节点颜色
private void insert(K key, V value) {
    root = insert(root, key, value);
    root.color = BLACK;  // 确保根节点为黑色
}
规则3:所有叶子节点(NIL)都是黑色
就像每个队伍的末端都由军官坐镇

// 在实现中,null表示NIL节点,默认为黑色
private boolean isNil(Node node) {
    return node == null;  // null节点就是黑色的NIL节点
}
规则4:如果一个节点是红色,则它的子节点必须是黑色
graph TD
    3B((指挥官)) --> 1R((士兵))
    3B --> 5R((士兵))
    1R --> NB1((NIL))
    1R --> NB2((NIL))
    5R --> NB3((NIL))
    5R --> NB4((NIL))
    
    style 3B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 1R fill:#f66,stroke:#333,stroke-width:2px
    style 5R fill:#f66,stroke:#333,stroke-width:2px
    style NB1 fill:#ddd,stroke:#333,stroke-width:2px
    style NB2 fill:#ddd,stroke:#333,stroke-width:2px
    style NB3 fill:#ddd,stroke:#333,stroke-width:2px
    style NB4 fill:#ddd,stroke:#333,stroke-width:2px

就像:

士兵(红色)不能直接管理其他士兵
必须由军官(黑色)来管理士兵
// 检查是否违反红色节点规则
private boolean isRedViolation(Node node) {
    if (node == null) return false;
    
    // 如果当前节点是红色,检查其子节点
    if (isRed(node)) {
        if (isRed(node.left) || isRed(node.right)) {
            return true;  // 违反规则
        }
    }
    return false;
}

// 修正红色冲突
private void fixRedConflict(Node node) {
    // 如果父节点和叔叔节点都是红色
    if (isRed(node.parent) && isRed(getUncle(node))) {
        // 变色操作
        node.parent.color = BLACK;
        getUncle(node).color = BLACK;
        node.parent.parent.color = RED;
    } else {
        // 需要旋转操作
        handleRotation(node);
    }
}
规则5:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
graph TD
    7B((7黑)) --> 3B((3黑))
    7B --> 10B((10黑))
    
    3B --> 1R((1红))
    3B --> 5R((5红))
    
    1R --> NB1((NIL))
    1R --> NB2((NIL))
    5R --> NB3((NIL))
    5R --> NB4((NIL))
    
    style 7B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 10B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 1R fill:#f66,stroke:#333,stroke-width:2px
    style 5R fill:#f66,stroke:#333,stroke-width:2px
    style NB1 fill:#ddd,stroke:#333,stroke-width:2px
    style NB2 fill:#ddd,stroke:#333,stroke-width:2px
    style NB3 fill:#ddd,stroke:#333,stroke-width:2px
    style NB4 fill:#ddd,stroke:#333,stroke-width:2px

就像:

从总指挥到每个末端队伍
必须经过相同数量的军官
保证命令传达的层级一致
// 验证黑色节点数量是否平衡
private int validateBlackHeight(Node node) {
    if (node == null) return 1;  // NIL节点算作1个黑色节点
    
    int leftHeight = validateBlackHeight(node.left);
    int rightHeight = validateBlackHeight(node.right);
    
    // 左右子树的黑色高度必须相同
    if (leftHeight != rightHeight) {
        throw new RuntimeException("Invalid Red-Black tree");
    }
    
    // 返回当前路径的黑色节点数
    return leftHeight + (isRed(node) ? 0 : 1);
}

3. 红黑树的操作

插入新节点
graph TD
    subgraph 插入前
        7B1((7黑)) --> 3B1((3黑))
        7B1 --> 10B1((10黑))
    end
    
    subgraph 插入后
        7B2((7黑)) --> 3B2((3黑))
        7B2 --> 10B2((10黑))
        3B2 --> 4R((4红))
    end
    
    style 7B1 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3B1 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 10B1 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    
    style 7B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 10B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 4R fill:#f66,stroke:#333,stroke-width:2px

就像:

新人加入时先作为士兵(红色)
然后通过调整确保军队结构稳定
public void put(K key, V value) {
    // 1. 标准BST插入
    root = put(root, key, value);
    // 2. 确保根节点为黑色
    root.color = BLACK;
}

private Node put(Node node, K key, V value) {
    // 1. 标准BST插入
    if (node == null) {
        return new Node(key, value);  // 新节点默认为红色
    }
    
    int cmp = key.compareTo(node.key);
    if (cmp < 0) {
        node.left = put(node.left, key, value);
    } else if (cmp > 0) {
        node.right = put(node.right, key, value);
    } else {
        node.value = value;
        return node;
    }
    
    // 2. 修正红黑树性质
    // 处理右边是红色,左边是黑色的情况
    if (isRed(node.right) && !isRed(node.left)) {
        node = rotateLeft(node);
    }
    // 处理连续的红色节点
    if (isRed(node.left) && isRed(node.left.left)) {
        node = rotateRight(node);
    }
    // 处理两个子节点都是红色的情况
    if (isRed(node.left) && isRed(node.right)) {
        flipColors(node);
    }
    
    return node;
}
旋转操作
左旋操作详解
1. 初始状态
graph TD
    3B((3黑)) --> 1R((1红))
    3B --> 5R((5红))
    5R --> 4R((4红))
    5R --> 7R((7红))
    
    style 3B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 1R fill:#f66,stroke:#333,stroke-width:2px
    style 5R fill:#f66,stroke:#333,stroke-width:2px
    style 4R fill:#f66,stroke:#333,stroke-width:2px
    style 7R fill:#f66,stroke:#333,stroke-width:2px
2. 左旋过程

就像跳舞时的转圈:

graph TD
    subgraph 步骤1-初始
        3B1((3黑)) --> 1R1((1红))
        3B1 --> 5R1((5红))
        5R1 --> 4R1((4红))
        5R1 --> 7R1((7红))
    end
    
    subgraph 步骤2-旋转中
        5R2((5红)) --> 3B2((3黑))
        5R2 --> 7R2((7红))
        3B2 --> 1R2((1红))
        3B2 --> 4R2((4红))
    end
    
    style 3B1 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 1R1 fill:#f66,stroke:#333,stroke-width:2px
    style 5R1 fill:#f66,stroke:#333,stroke-width:2px
    style 4R1 fill:#f66,stroke:#333,stroke-width:2px
    style 7R1 fill:#f66,stroke:#333,stroke-width:2px
    
    style 5R2 fill:#f66,stroke:#333,stroke-width:2px
    style 3B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 7R2 fill:#f66,stroke:#333,stroke-width:2px
    style 1R2 fill:#f66,stroke:#333,stroke-width:2px
    style 4R2 fill:#f66,stroke:#333,stroke-width:2px
// 左旋操作
private Node rotateLeft(Node h) {
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    x.color = h.color;
    h.color = RED;
    return x;
}

// 右旋操作
private Node rotateRight(Node h) {
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    x.color = h.color;
    h.color = RED;
    return x;
}

// 颜色翻转
private void flipColors(Node h) {
    h.color = RED;
    h.left.color = BLACK;
    h.right.color = BLACK;
}
左旋的具体步骤
  1. 节点位置变化
- 原来的3(黑)是根节点
- 5(红)是3的右子节点
- 左旋后5变成新的根节点
- 3变成5的左子节点
  1. 子树的处理
- 4从5的左子树变成3的右子树
- 7保持为5的右子树
- 1保持为3的左子树
形象比喻

想象一个跳芭蕾舞的动作:

1. 3和5像是两个舞者
2. 3向左转
3. 5升到上面
4. 4跟着3走

或者像玩跷跷板:

- 3往下沉
- 5往上升
- 中间的4跟着3走
旋转操作规则
1. 旋转的触发条件
graph TD
    subgraph 情况1-连续的红节点
        7B1((7黑)) --> 5R1((5红))
        5R1 --> 3R1((3红))
    end
    
    subgraph 情况2-不平衡的黑高度
        7B2((7黑)) --> 5B2((5黑))
        7B2 --> 9R2((9红))
        5B2 --> 3R2((3红))
        5B2 --> 6R2((6红))
    end
    
    style 7B1 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5R1 fill:#f66,stroke:#333,stroke-width:2px
    style 3R1 fill:#f66,stroke:#333,stroke-width:2px
    
    style 7B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 9R2 fill:#f66,stroke:#333,stroke-width:2px
    style 3R2 fill:#f66,stroke:#333,stroke-width:2px
    style 6R2 fill:#f66,stroke:#333,stroke-width:2px

主要在以下情况需要旋转:

  1. 插入后出现连续的红节点
  2. 删除后破坏黑色平衡
2. 旋转的基本规则
左旋规则:
graph TD
    subgraph 左旋前
        P((P)) --> A((A))
        P --> R((R))
        R --> B((B))
        R --> C((C))
    end
    
    subgraph 左旋后
        R2((R)) --> P2((P))
        R2 --> C2((C))
        P2 --> A2((A))
        P2 --> B2((B))
    end

步骤:

  1. R成为新的根节点
  2. P成为R的左子节点
  3. B从R的左子树变成P的右子树
右旋规则:
graph TD
    subgraph 右旋前
        P((P)) --> L((L))
        P --> C((C))
        L --> A((A))
        L --> B((B))
    end
    
    subgraph 右旋后
        L2((L)) --> A2((A))
        L2 --> P2((P))
        P2 --> B2((B))
        P2 --> C2((C))
    end

步骤:

  1. L成为新的根节点
  2. P成为L的右子节点
  3. B从L的右子树变成P的左子树
3. 具体场景和处理方法
场景1:插入节点后的调整
graph TD
    subgraph 插入前
        10B((10黑)) --> 5B((5黑))
        10B --> 15B((15黑))
    end
    
    subgraph 插入后
        10B2((10黑)) --> 5B2((5黑))
        10B2 --> 15B2((15黑))
        5B2 --> 3R((3红))
    end
    
    style 10B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 15B fill:#333,stroke:#333,stroke-width:2px,color:#fff
    
    style 10B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 15B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3R fill:#f66,stroke:#333,stroke-width:2px

处理步骤:

  1. 新节点总是插入为红色
  2. 检查是否违反红黑树规则
  3. 根据情况选择旋转或变色
4. 旋转的判断口诀
1. 左左情况:右旋一次
2. 右右情况:左旋一次
3. 左右情况:先左旋后右旋
4. 右左情况:先右旋后左旋
5. 代码实现模板
private void leftRotate(Node node) {
    Node right = node.right;
    node.right = right.left;
    
    if (right.left != null)
        right.left.parent = node;
        
    right.parent = node.parent;
    
    if (node.parent == null)
        root = right;
    else if (node == node.parent.left)
        node.parent.left = right;
    else
        node.parent.right = right;
        
    right.left = node;
    node.parent = right;
}
5. 实际运行示例

插入序列 [10, 5, 15, 3, 7]:

graph TD
    subgraph 步骤1
        10B1((10黑))
    end
    
    subgraph 步骤2
        10B2((10黑)) --> 5R2((5红))
    end
    
    subgraph 步骤3
        10B3((10黑)) --> 5R3((5红))
        10B3 --> 15R3((15红))
    end
    
    subgraph 步骤4
        10B4((10黑)) --> 5B4((5黑))
        10B4 --> 15B4((15黑))
        5B4 --> 3R4((3红))
    end
    
    style 10B1 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 10B2 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5R2 fill:#f66,stroke:#333,stroke-width:2px
    style 10B3 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5R3 fill:#f66,stroke:#333,stroke-width:2px
    style 15R3 fill:#f66,stroke:#333,stroke-width:2px
    style 10B4 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 5B4 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 15B4 fill:#333,stroke:#333,stroke-width:2px,color:#fff
    style 3R4 fill:#f66,stroke:#333,stroke-width:2px
6. 实际应用记忆方法

想象跳舞的动作:

左旋:向左转圈
右旋:向右转圈
双旋:转两次圈

或者像魔方的旋转:

左旋:左面向上转
右旋:右面向上转

记住这些规则后,就能更容易理解和实现红黑树的平衡操作了!

4. 红黑树的应用

  1. Java中的TreeMap和TreeSet
TreeMap<Integer, String> map = new TreeMap<>();
// 自动保持有序,并保持平衡
  1. Linux内核中的进程调度
使用红黑树管理进程
保证快速查找和调度

5. 为什么要用红黑树?

优点:

1. 查找速度快(类似二分查找)
2. 插入删除后自动平衡
3. 最坏情况性能有保证

就像一支纪律严明的军队:

- 层级分明(黑红交替)
- 命令传达快速(查找效率高)
- 调整灵活(自动平衡)

这就是红黑树,一个看似复杂但实际非常实用的数据结构。它就像一支训练有素的军队,通过严格的规则保持高效和平衡!

记忆相关内容

1. 红黑树的五大特性:

想象一个规范的公司组织:
1. 每个员工要么穿红色工作服,要么穿黑色工作服(节点非红即黑)
2. 老板必须穿黑色西装(根节点必须是黑色)
3. 新员工(叶子NIL节点)必须穿黑色工作服
4. 穿红色工作服的员工的直接上级必须穿黑色(红节点的父节点必须是黑色)
5. 从老板到任何一个普通员工,路径上的黑衣人数量必须相同(每条路径上黑节点数量相同)

2. 为什么要有这些规则?

平衡的关键:
- 就像公司管理:
  * 不能两个红色员工直接上下级(避免连续红节点)
  * 黑色数量平衡(确保树的高度平衡)
  * 最长路径不会超过最短路径的2倍

3. 插入新节点的处理:

新员工入职规则:
1. 新人先穿红色工作服(新节点默认红色)
2. 如果发现问题,有三种处理方式:
   
   a. 换色:
      直接上级和同级都换色(父节点和叔叔节点变黑,爷爷变红)
   
   b. 左旋:
      类似公司调整:
      爸爸 -> 我 -> 孙子
      变成:
      我 -> 爸爸 -> 孙子
   
   c. 右旋:
      反方向的调整:
      爸爸 -> 我 -> 孙子
      变成:
      孙子 -> 我 -> 爸爸

4. 应用场景:

1. Java的TreeMap和TreeSet的底层实现
2. Linux的进程调度
3. 数据库的索引结构

5. 性能特点:

时间复杂度:
- 查找:O(logN)
- 插入:O(logN)
- 删除:O(logN)

就像在一个组织良好的公司:
- 查找任何员工都很快(树的高度被限制)
- 新员工入职流程规范(插入操作)
- 员工离职处理有序(删除操作)

6. 面试回答技巧:

1. 先说应用场景(TreeMap等)
2. 再说五大特性(用公司组织类比)
3. 然后说优势(平衡性好,性能稳定)
4. 最后说操作(插入、删除的处理方式)

记住关键词:
- 红黑两色
- 根黑叶黑
- 红父必黑
- 黑色平衡
- 左旋右旋

7. 相比AVL树的优势:

就像公司管理:
- AVL树太严格(要求左右子树高度差不超过1)
  像是要求每个部门人数必须完全平衡
  
- 红黑树更灵活
  像是允许部门间有一定的人数差异
  但保证不会差距过大

记住这个比喻:

红黑树就像一个组织良好的公司:
- 有明确的着装规定(颜色规则)
- 有严格的等级制度(树的结构)
- 有灵活的调整机制(旋转和变色)
- 保持整体平衡(性能稳定)

这样回答:

  1. 简单易懂
  2. 容易记忆
  3. 突出重点
  4. 展示理解深度

相关题目

1. TreeMap的应用题

【题目732】:我的日程安排表 III

class MyCalendarThree {
    // 使用TreeMap记录时间点的预订次数变化
    TreeMap<Integer, Integer> delta;
    
    public MyCalendarThree() {
        delta = new TreeMap<>();
    }
    
    public int book(int start, int end) {
        // 记录时间点的预订次数变化
        delta.put(start, delta.getOrDefault(start, 0) + 1);
        delta.put(end, delta.getOrDefault(end, 0) - 1);
        
        int active = 0, ans = 0;
        // 统计最大重叠预订数
        for (int d : delta.values()) {
            active += d;
            ans = Math.max(ans, active);
        }
        return ans;
    }
}

图解过程:

graph LR
    A[时间轴] --> B[10:00 +1]
    B --> C[20:00 -1]
    C --> D[50:00 +1]
    D --> E[60:00 -1]
    
    style A fill:#f96,stroke:#333,stroke-width:2px

2. TreeSet的应用题

【题目220】:存在重复元素 III

public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
    TreeSet<Long> set = new TreeSet<>();
    
    for (int i = 0; i < nums.length; i++) {
        // 查找大于等于当前值-t的最小值
        Long ceiling = set.ceiling((long) nums[i] - t);
        
        // 检查是否找到符合条件的值
        if (ceiling != null && ceiling <= (long) nums[i] + t)
            return true;
            
        // 将当前值加入集合
        set.add((long) nums[i]);
        
        // 维护滑动窗口大小为k
        if (i >= k)
            set.remove((long) nums[i - k]);
    }
    return false;
}

滑动窗口示意图:

graph LR
    A((1)) --> B((4))
    B --> C((9))
    C --> D((13))
    
    style A fill:#f96,stroke:#333,stroke-width:2px
    style B fill:#f96,stroke:#333,stroke-width:2px
    style C fill:#f96,stroke:#333,stroke-width:2px

3. 平衡树特性题

【题目1382】:将二叉搜索树变平衡

public TreeNode balanceBST(TreeNode root) {
    // 中序遍历获取有序数组
    List<Integer> values = new ArrayList<>();
    inorder(root, values);
    
    // 将有序数组转换为平衡二叉树
    return buildTree(values, 0, values.size() - 1);
}

private void inorder(TreeNode node, List<Integer> values) {
    if (node == null) return;
    inorder(node.left, values);
    values.add(node.val);
    inorder(node.right, values);
}

private TreeNode buildTree(List<Integer> values, int start, int end) {
    if (start > end) return null;
    
    int mid = (start + end) / 2;
    TreeNode node = new TreeNode(values.get(mid));
    
    node.left = buildTree(values, start, mid - 1);
    node.right = buildTree(values, mid + 1, end);
    
    return node;
}

转换过程示意图:

graph TD
    subgraph 转换前
        4A((4)) --> 2A((2))
        4A --> 6A((6))
        2A --> 1A((1))
        2A --> 3A((3))
        6A --> 5A((5))
        6A --> 7A((7))
    end
    
    subgraph 转换后
        4B((4)) --> 2B((2))
        4B --> 6B((6))
        2B --> 1B((1))
        2B --> 3B((3))
        6B --> 5B((5))
        6B --> 7B((7))
    end

4. 实际应用建议

  1. 掌握TreeMap/TreeSet的使用
TreeMap<Integer, Integer> map = new TreeMap<>();
// 常用方法
map.floorKey(key);    // 小于等于key的最大键
map.ceilingKey(key);  // 大于等于key的最小键
map.firstKey();       // 最小键
map.lastKey();        // 最大键
  1. 理解平衡树的特性
- 查找/插入/删除的时间复杂度都是O(logN)
- 中序遍历可以得到有序序列
- 平衡因子的概念
  1. 实际面试中的重点
- 理解红黑树的应用场景
- 会使用Java中的TreeMap/TreeSet
- 理解平衡树的基本特性
- 不需要手写红黑树的实现

这些题目主要考察对红黑树特性的理解和应用,而不是具体实现。在面试中,重点掌握TreeMap和TreeSet的使用,以及平衡树的基本特性就足够了。