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

Минимальная глубина бинарного дерева

2.0 Middle🔥 111 комментариев
#Python Core#Архитектура и паттерны

Условие

Найдите минимальную глубину бинарного дерева.

Минимальная глубина — это кратчайший путь от корня до ближайшего листа.

Пример

    3
   / \
  9  20
    /  \
   15   7

Ответ: 2 (путь 3 → 9)

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

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

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

Минимальная глубина бинарного дерева

Это популярная задача на обход деревьев. Важно понять разницу между минимальной и максимальной глубиной, а также оптимизировать обход.

1. Определение минимальной глубины

Минимальная глубина = длина пути от корня до ближайшего листа.

Лист = узел без потомков (нет левого и правого детей).

Примеры:

    3           3           3
   / \         /           / \
  9  20      9            9  20
    /  \                     /  \
   15   7                   15   7

Ответ: 2     Ответ: 2    Ответ: 2
Лист: 9      Лист: 9     Листья: 15, 7 → мин=2

2. Определение структуры дерева

from typing import Optional

class TreeNode:
    def __init__(self, val: int = 0, left: Optional['TreeNode'] = None, 
                 right: Optional['TreeNode'] = None):
        self.val = val
        self.left = left
        self.right = right

3. Наивное решение — DFS (рекурсия)

def minDepth_naive(root: Optional[TreeNode]) -> int:
    """Найти минимальную глубину рекурсивно (неоптимально)."""
    
    # Базовый случай: пустое дерево
    if not root:
        return 0
    
    # Если это лист (нет детей)
    if not root.left and not root.right:
        return 1
    
    # Рекурсивно ищем минимум в обоих поддеревьях
    left_depth = minDepth_naive(root.left) if root.left else float('inf')
    right_depth = minDepth_naive(root.right) if root.right else float('inf')
    
    return 1 + min(left_depth, right_depth)

# Проблема: посещаем ВСЕ узлы, даже если найдём лист раньше

Анализ:

  • Время: O(n) в худшем случае (посещаем все узлы)
  • Память: O(h) где h — высота дерева (стек рекурсии)

4. Оптимальное решение — BFS (очередь)

BFS найдёт первый лист быстрее, чем DFS!

from collections import deque

def minDepth_optimal(root: Optional[TreeNode]) -> int:
    """Найти минимальную глубину через BFS (оптимально)."""
    
    if not root:
        return 0
    
    # Очередь содержит (узел, глубина)
    queue = deque([(root, 1)])
    
    while queue:
        node, depth = queue.popleft()
        
        # Если нашли лист — возвращаем сразу!
        if not node.left and not node.right:
            return depth
        
        # Добавляем потомков в очередь
        if node.left:
            queue.append((node.left, depth + 1))
        if node.right:
            queue.append((node.right, depth + 1))
    
    return 0

# Оптимизация: BFS останавливается на первом листе!

Анализ:

  • Время: O(n) в худшем случае, но часто O(log n) — O(n/2) на практике
  • Память: O(w) где w — максимальная ширина дерева
  • Преимущество: Ранний выход при нахождении листа

5. Визуализация BFS vs DFS

Дерево:
        3
       / \
      9  20
        /  \
       15   7

BFS (очередь):
Шаг 1: Посещаем 3 (глубина=1) → это не лист, добавляем детей
Шаг 2: Посещаем 9 (глубина=2) → это ЛИСТ! Ответ: 2

Визит узлов: 3, 9 (всего 2 узла)


DFS (стек):
Шаг 1: Посещаем 3 → спускаемся в левое поддерево
Шаг 2: Посещаем 9 → это лист, возвращаемся
Шаг 3: Посещаем левое поддерево 20
Шаг 4: Посещаем 15 → это лист
Шаг 5: Посещаем 7 → это лист

Визит узлов: 3, 9, 20, 15, 7 (всего 5 узлов)

BFS эффективнее на 60%!

6. Правильное решение DFS

Если используем DFS, нужно правильно обработать случаи:

def minDepth_dfs(root: Optional[TreeNode]) -> int:
    """DFS решение с правильной логикой."""
    
    if not root:
        return 0
    
    # Если узел листа:
    if not root.left and not root.right:
        return 1
    
    # ВАЖНО: если один из детей отсутствует,
    # это НЕ листья! Нельзя просто возвращать float('inf')
    left_depth = minDepth_dfs(root.left) if root.left else float('inf')
    right_depth = minDepth_dfs(root.right) if root.right else float('inf')
    
    return 1 + min(left_depth, right_depth)

# Альтернатива (более ясная):
def minDepth_dfs_clear(root: Optional[TreeNode]) -> int:
    """DFS с явной обработкой случаев."""
    
    if not root:
        return 0
    
    # Лист: оба потомка None
    if not root.left and not root.right:
        return 1
    
    # Внутренний узел: минимум из существующих поддеревьев
    min_child_depth = float('inf')
    
    if root.left:
        min_child_depth = min(min_child_depth, minDepth_dfs_clear(root.left))
    
    if root.right:
        min_child_depth = min(min_child_depth, minDepth_dfs_clear(root.right))
    
    return 1 + min_child_depth

7. Полный пример с тестами

from typing import Optional
from collections import deque
import unittest

class TreeNode:
    def __init__(self, val: int = 0, left: Optional['TreeNode'] = None,
                 right: Optional['TreeNode'] = None):
        self.val = val
        self.left = left
        self.right = right

def minDepth(root: Optional[TreeNode]) -> int:
    """Оптимальное решение через BFS."""
    if not root:
        return 0
    
    queue = deque([(root, 1)])
    
    while queue:
        node, depth = queue.popleft()
        
        # Нашли лист — возвращаем сразу
        if not node.left and not node.right:
            return depth
        
        # Добавляем потомков
        if node.left:
            queue.append((node.left, depth + 1))
        if node.right:
            queue.append((node.right, depth + 1))
    
    return 0

class TestMinDepth(unittest.TestCase):
    
    def setUp(self):
        """Создаём тестовые деревья."""
        # Дерево 1:
        #     3
        #    / \
        #   9  20
        #     /  \
        #    15   7
        self.tree1 = TreeNode(3)
        self.tree1.left = TreeNode(9)
        self.tree1.right = TreeNode(20)
        self.tree1.right.left = TreeNode(15)
        self.tree1.right.right = TreeNode(7)
        
        # Дерево 2: только корень
        self.tree2 = TreeNode(1)
        
        # Дерево 3: пустое
        self.tree3 = None
        
        # Дерево 4: скошенное (только левые дети)
        #     1
        #    /
        #   2
        #  /
        # 3
        self.tree4 = TreeNode(1)
        self.tree4.left = TreeNode(2)
        self.tree4.left.left = TreeNode(3)
    
    def test_example_case(self):
        """Стандартный пример из задачи."""
        self.assertEqual(minDepth(self.tree1), 2)
    
    def test_single_node(self):
        """Дерево с одним узлом (листом)."""
        self.assertEqual(minDepth(self.tree2), 1)
    
    def test_empty_tree(self):
        """Пустое дерево."""
        self.assertEqual(minDepth(self.tree3), 0)
    
    def test_skewed_tree(self):
        """Скошенное дерево только с левыми детьми."""
        self.assertEqual(minDepth(self.tree4), 3)
    
    def test_complete_tree(self):
        """Полное сбалансированное дерево."""
        #       1
        #      / \
        #     2   3
        #    / \
        #   4   5
        tree = TreeNode(1)
        tree.left = TreeNode(2)
        tree.right = TreeNode(3)
        tree.left.left = TreeNode(4)
        tree.left.right = TreeNode(5)
        
        # Листья на уровне 3: 4, 5, 3
        # Минимальная глубина = 2 (путь 1→3)
        self.assertEqual(minDepth(tree), 2)
    
    def test_right_skewed_tree(self):
        """Скошенное дерево только с правыми детьми."""
        #     1
        #      \
        #       2
        #        \
        #         3
        tree = TreeNode(1)
        tree.right = TreeNode(2)
        tree.right.right = TreeNode(3)
        
        self.assertEqual(minDepth(tree), 3)

if __name__ == '__main__':
    unittest.main()

8. Сравнение подходов

ПодходВремениПамятиПроМинусы
BFSO(n), но часто O(min_depth * log n)O(w)Ранний выходНемного сложнее
DFS наивныйO(n)O(h)ПростойПосещает все узлы
DFS правильныйO(n)O(h)ПростойПосещает все узлы

9. Сложные граничные случаи

def handle_edge_cases():
    # Дерево, где корень имеет только одного ребёнка
    #     1
    #    /
    #   2
    tree = TreeNode(1)
    tree.left = TreeNode(2)
    # Минимальная глубина НЕ 1! (1 не лист)
    # Ответ: 2
    
    # Дерево с чередующимися детьми
    #       1
    #      / \
    #     2   3
    #    /     \
    #   4       5
    tree = TreeNode(1)
    tree.left = TreeNode(2)
    tree.right = TreeNode(3)
    tree.left.left = TreeNode(4)
    tree.right.right = TreeNode(5)
    # Листья: 4 (глубина 3), 5 (глубина 3)
    # Ответ: 3

10. Оптимизация памяти

def minDepth_memory_optimized(root: Optional[TreeNode]) -> int:
    """Двусторонний BFS для экономии памяти."""
    if not root:
        return 0
    
    # Используем две очереди для двусторонней очереди
    from collections import deque
    
    if not root.left and not root.right:
        return 1
    
    queue = deque([(root, 1)])
    
    while queue:
        node, depth = queue.popleft()
        
        if not node.left and not node.right:
            return depth
        
        # Добавляем только существующих потомков
        if node.left:
            queue.append((node.left, depth + 1))
        if node.right:
            queue.append((node.right, depth + 1))
    
    return 0

Вывод

Для минимальной глубины бинарного дерева:

  1. BFS оптимален — находит первый лист быстро
  2. DFS нужно писать правильно — обработать случаи с одним потомком
  3. Главная ошибка — путать листья с узлами, имеющими одного потомка
  4. Граничные случаи — пустое дерево, один узел, скошенные деревья

Выбирайте BFS для лучшей средней производительности!

Минимальная глубина бинарного дерева | PrepBro