← Назад к вопросам

Как устроено распределение листьев в красно-чёрном дереве?

1.0 Junior🔥 81 комментариев
#Коллекции

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как устроено распределение листьев в красно-чёрном дереве

Красно-чёрное дерево (Red-Black Tree) — это самобалансирующееся бинарное дерево поиска, которое гарантирует логарифмическую высоту O(log n). В Java используется в реализациях TreeMap и TreeSet. Расскажу, как именно устроены листья и почему это важно для баланса.

1. Основные свойства красно-чёрного дерева

Каждый узел имеет цвет: красный или чёрный.

Свойства (инварианты):

  1. Каждый узел либо красный, либо чёрный
  2. Корень всегда чёрный
  3. Листья (NIL узлы) считаются чёрными
  4. Если узел красный, оба его потомка чёрные (нет двух красных подряд)
  5. Для любого узла все пути от него до листьев содержат одинаковое количество чёрных узлов (черная высота)

2. Листья в красно-чёрном дереве

Это критическая деталь, которую часто упускают:

NIL узлы (sentinel nodes) — листья дерева:

  • Это не реальные узлы с данными
  • Это плейсхолдеры (заполнители) для отсутствующих детей
  • Все NIL узлы считаются чёрными
  • Они не содержат значений
// Java реализация (Java TreeMap использует чуть другой подход, но концепция та же)
public class RedBlackTree {
    
    private static class Node {
        int value;
        Node left, right, parent;
        Color color;  // RED или BLACK
        
        Node(int value) {
            this.value = value;
            this.color = Color.RED;
            this.left = NIL;    // Изначально указывают на NIL
            this.right = NIL;
        }
    }
    
    // Sentinel NIL узел (лист)
    private static final Node NIL = new Node(-1);
    static {
        NIL.color = Color.BLACK;
        NIL.left = NIL;
        NIL.right = NIL;
        NIL.parent = NIL;
    }
    
    private Node root = NIL;
}

3. Визуальное представление дерева с листьями

Обычное представление (T — чёрные узлы, R — красные):

         (T) 10
        /      \
      (R) 5   (R) 15
      /   \    /   \
    (T)3 (T)7 (T)12 (T)18
    / \ / \ / \ / \
  (B)(B)(B)(B)(B)(B)(B)(B)

Где (B) = NIL листья (все чёрные)

Видишь, каждый реальный узел имеет двух потомков — либо настоящие узлы, либо NIL.

4. Черная высота и листья

Чёрная высота — это критическое свойство, определяемое листьями:

Черная высота = количество чёрных узлов от узла до листа (не считая самого узла, считая листья NIL)

Пример:

         (T-BH:2) 10
        /         \
     (R-BH:1) 5  (R-BH:1) 15
     /   \      /   \
   (T-BH:1)3 (T-BH:1)7 ...
   / \       / \
 (B)(B)   (B)(B)   <- Листья NIL (BH:0)

Все пути от корня до листьев имеют одинаковое количество чёрных узлов:

  • Путь 1: 10 (ч) → 5 (к) → 3 (ч) → NIL (ч) = 3 чёрных
  • Путь 2: 10 (ч) → 5 (к) → 7 (ч) → NIL (ч) = 3 чёрных
  • Путь 3: 10 (ч) → 15 (к) → ... = 3 чёрных

Это гарантирует баланс!

5. Почему листья именно чёрные?

Это не просто конвенция — это ключ к сбалансированности дерева.

Причина 1: Гарантирует минимальную глубину

Если листья чёрные:

  • Минимальная чёрная высота = 1 (сам корень, если он единственный)
  • Максимальная чёрная высота = log2(n)

Поэтому высота дерева O(log n).

Причина 2: Запрещает много красных узлов подряд

Правило: если узел красный, его дети чёрные.

Поскольку листья (NIL) чёрные, никакой красный узел не может иметь другого красного узла как потомка.

Причина 3: Упрощает инвариант

Вместо того, чтобы говорить "листья могут быть разного цвета", просто объявляем их все чёрными.

6. Вставка в красно-чёрное дерево

Листья важны при вставке:

private void insert(int value) {
    Node newNode = new Node(value);
    
    if (root == NIL) {
        root = newNode;
        newNode.color = Color.BLACK;  // Корень всегда чёрный
        return;
    }
    
    // Вставляем как в обычное BST, но вместо null используем NIL
    Node current = root;
    while (current != NIL) {
        if (value < current.value) {
            if (current.left == NIL) {
                current.left = newNode;
                newNode.parent = current;
                break;
            }
            current = current.left;
        } else {
            if (current.right == NIL) {
                current.right = newNode;
                newNode.parent = current;
                break;
            }
            current = current.right;
        }
    }
    
    newNode.color = Color.RED;  // Новый узел всегда красный
    fixInsert(newNode);  // Восстанавливаем инварианты
}

Когда вставляем узел, он всегда заменяет NIL лист!

7. Балансировка и листья

При ротациях листья переставляются:

До ротации:

      (B) 10
      /   \
    (R)5 (B)15
    / \   / \
  (B)(B)(B)(B)  <- Листья NIL

После левой ротации:

      (B) 15
      /   \
    (B)10 (B)
    /  \   
  (B)(B) (NIL)  <- Листья NIL

Листья остаются на месте логически, но физически перестраиваются.

8. Удаление из красно-чёрного дерева

Удаление сложнее, потому что нужно восстановить черную высоту.

Случай 1: Узел без детей (листья NIL)

if (node.left == NIL && node.right == NIL) {
    // Это лист, просто удаляем
    replaceNode(node, NIL);
}

Случай 2: Узел с одним ребёнком

if (node.left == NIL) {
    replaceNode(node, node.right);
} else if (node.right == NIL) {
    replaceNode(node, node.left);
}

Случай 3: Узел с двумя детьми

Находим successor (минимум в правом поддереве), заменяем и удаляем successor.

9. Реальный пример из Java TreeMap

Java TreeMap использует чуть другую реализацию (null вместо NIL), но концепция та же:

private static final class Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left, right, parent;
    boolean color = BLACK;
    
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
        this.left = null;   // null = NIL лист
        this.right = null;
    }
}

10. Сложность операций

Благодаря листьям и черной высоте гарантируется:

  • Search: O(log n)
  • Insert: O(log n)
  • Delete: O(log n)
  • Min/Max: O(log n)

Высота дерева никогда не превысит 2 * log2(n), где n — количество узлов.

Ключевые выводы про листья

  1. Листья это NIL узлы — специальные плейсхолдеры, не содержащие данных
  2. Листья всегда чёрные — это гарантирует баланс дерева
  3. Каждый реальный узел указывает на NIL или другие узлы — структура полностью определена
  4. Черная высота до листа — вот что определяет баланс
  5. При вставке новый узел заменяет NIL лист — это очень важно для понимания алгоритма

Визуально распределение листьев подчиняется правилу: все пути от корня до листа NIL содержат одинаковое количество чёрных узлов. Это единственное правило, которое гарантирует, что дерево остаётся хорошо сбалансированным, и высота никогда не превысит O(log n).

Как устроено распределение листьев в красно-чёрном дереве? | PrepBro