Деревья (Trees)

tags: dsa trees bst leetcode patterns


BST (Binary Search Tree)

Инвариант BST

  • Для каждого узла: left.val < node.val < right.val (классический вариант без дублей)
  • Позволяет отсекать половину дерева при поиске → O(h) для search/insert/delete
  • h = высота: O(log n) для сбалансированного, O(n) в худшем случае (вырожденное в список)

Ключевая особенность: inorder → sorted

Inorder обход BST даёт значения в отсортированном порядке — это главное свойство для большинства задач

Типовые паттерны для BST

1. Prev в inorder (минимальная разница, валидация)

Когда: нужно сравнивать соседние по величине значения

var getMinimumDifference = function(root) {
  let prev = null;
  let ans = Infinity;
 
  function inorder(node) {
    if (!node) return;
    inorder(node.left);
 
    if (prev !== null) {
      ans = Math.min(ans, node.val - prev);
    }
    prev = node.val;
 
    inorder(node.right);
  }
 
  inorder(root);
  return ans;
};

2. K-th smallest/largest

Когда: нужен элемент на позиции k в отсортированном порядке

var kthSmallest = function(root, k) {
  let count = 0;
  let result = null;
 
  function inorder(node) {
    if (!node || result !== null) return;
 
    inorder(node.left);
 
    count++;
    if (count === k) {
      result = node.val;
      return;
    }
 
    inorder(node.right);
  }
 
  inorder(root);
  return result;
};

3. Two Sum в BST

Вариант A: DFS + HashSet

var findTarget = function(root, k) {
  const seen = new Set();
 
  function dfs(node) {
    if (!node) return false;
 
    if (seen.has(k - node.val)) return true;
    seen.add(node.val);
 
    return dfs(node.left) || dfs(node.right);
  }
 
  return dfs(root);
};

Вариант B: Inorder + два указателя (экономит память)

var findTarget = function(root, k) {
  const sorted = [];
 
  function inorder(node) {
    if (!node) return;
    inorder(node.left);
    sorted.push(node.val);
    inorder(node.right);
  }
 
  inorder(root);
 
  let l = 0, r = sorted.length - 1;
  while (l < r) {
    const sum = sorted[l] + sorted[r];
    if (sum === k) return true;
    if (sum < k) l++;
    else r--;
  }
  return false;
};

4. Search/Insert

Search: используй свойство BST для отсечения половины

var searchBST = function(root, val) {
  if (!root || root.val === val) return root;
  return val < root.val 
    ? searchBST(root.left, val) 
    : searchBST(root.right, val);
};

Insert:

var insertIntoBST = function(root, val) {
  if (!root) return new TreeNode(val);
 
  if (val < root.val) {
    root.left = insertIntoBST(root.left, val);
  } else {
    root.right = insertIntoBST(root.right, val);
  }
 
  return root;
};

5. Delete Node

Когда: удаление узла с сохранением свойств BST (3 случая: лист, 1 ребенок, 2 ребенка)

var deleteNode = function(root, key) {
  if (!root) return null;
 
  if (key < root.val) {
    root.left = deleteNode(root.left, key);
  } else if (key > root.val) {
    root.right = deleteNode(root.right, key);
  } else {
    // Узел найден
    if (!root.left) return root.right;
    if (!root.right) return root.left;
 
    // 2 ребенка: ищем successor (min в правом поддереве)
    let minNode = root.right;
    while (minNode.left) minNode = minNode.left;
 
    root.val = minNode.val;
    root.right = deleteNode(root.right, minNode.val);
  }
  return root;
};

6. Range Sum Query

Когда: нужна сумма значений в диапазоне [low, high]

var rangeSumBST = function(root, low, high) {
  if (!root) return 0;
 
  // Отсекаем ветки вне диапазона
  if (root.val < low) return rangeSumBST(root.right, low, high);
  if (root.val > high) return rangeSumBST(root.left, low, high);
 
  // Узел в диапазоне — идём в обе стороны
  return root.val 
    + rangeSumBST(root.left, low, high) 
    + rangeSumBST(root.right, low, high);
};

7. Балансировка BST

Когда: нужно перестроить дерево для оптимальной высоты

var balanceBST = function(root) {
  const arr = [];
  // 1. Inorder -> Sorted Array
  function inorder(node) {
    if (!node) return;
    inorder(node.left);
    arr.push(node.val);
    inorder(node.right);
  }
 
  // 2. Sorted Array -> Balanced BST
  function build(l, r) {
    if (l > r) return null;
    const mid = l + Math.floor((r - l) / 2);
    const node = new TreeNode(arr[mid]);
    node.left = build(l, mid - 1);
    node.right = build(mid + 1, r);
    return node;
  }
 
  inorder(root);
  return build(0, arr.length - 1);
};

Binary Tree (общие деревья)

Обходы (traversals)

DFS — три порядка

// Preorder (NLR): node → left → right
function preorder(node) {
  if (!node) return;
  visit(node);
  preorder(node.left);
  preorder(node.right);
}
 
// Inorder (LNR): left → node → right
function inorder(node) {
  if (!node) return;
  inorder(node.left);
  visit(node);
  inorder(node.right);
}
 
// Postorder (LRN): left → right → node
function postorder(node) {
  if (!node) return;
  postorder(node.left);
  postorder(node.right);
  visit(node);
}

BFS (level-order)

Стандартный шаблон (без shift для производительности):

function levelOrder(root) {
  if (!root) return [];
 
  const result = [];
  const queue = [root];
 
  for (let i = 0; i < queue.length; i++) {
    const node = queue[i];
    result.push(node.val);
 
    if (node.left) queue.push(node.left);
    if (node.right) queue.push(node.right);
  }
 
  return result;
}

Типовые паттерны для Binary Tree

1. Максимальная глубина (высота)

Postorder — вычисляем после детей

var maxDepth = function(root) {
  if (!root) return 0;
  return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
};

2. Диаметр дерева

Паттерн: на каждом узле считаем высоту + обновляем глобальный максимум

var diameterOfBinaryTree = function(root) {
  let diameter = 0;
 
  function height(node) {
    if (!node) return 0;
 
    const leftH = height(node.left);
    const rightH = height(node.right);
 
    // Обновляем диаметр через этот узел
    diameter = Math.max(diameter, leftH + rightH);
 
    return 1 + Math.max(leftH, rightH);
  }
 
  height(root);
  return diameter;
};

3. Инвертирование дерева

Preorder или Postorder — меняем left↔right

var invertTree = function(root) {
  if (!root) return null;
 
  // Swap
  [root.left, root.right] = [root.right, root.left];
 
  invertTree(root.left);
  invertTree(root.right);
 
  return root;
};

4. Симметричное дерево

Паттерн: сравниваем зеркальные узлы

var isSymmetric = function(root) {
  function isMirror(left, right) {
    if (!left && !right) return true;
    if (!left || !right) return false;
 
    return left.val === right.val
      && isMirror(left.left, right.right)
      && isMirror(left.right, right.left);
  }
 
  return isMirror(root, root);
};

5. Path Sum

DFS с накоплением суммы

var hasPathSum = function(root, targetSum) {
  if (!root) return false;
 
  // Лист — проверяем сумму
  if (!root.left && !root.right) {
    return root.val === targetSum;
  }
 
  const remaining = targetSum - root.val;
  return hasPathSum(root.left, remaining) 
    || hasPathSum(root.right, remaining);
};

6. Lowest Common Ancestor (LCA)

Postorder — находим в детях, решаем снизу вверх

var lowestCommonAncestor = function(root, p, q) {
  if (!root || root === p || root === q) return root;
 
  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);
 
  // Если оба нашлись в разных ветках — текущий узел и есть LCA
  if (left && right) return root;
 
  // Иначе возвращаем того, кто нашёлся
  return left || right;
};

7. Right Side View

BFS с отслеживанием последнего узла на уровне

var rightSideView = function(root) {
  if (!root) return [];
 
  const result = [];
  const queue = [root];
 
  while (queue.length > 0) {
    const levelSize = queue.length;
 
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
 
      // Последний узел уровня
      if (i === levelSize - 1) {
        result.push(node.val);
      }
 
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }
 
  return result;
};

8. Serialize & Deserialize

Preorder для сериализации

var serialize = function(root) {
  const result = [];
 
  function preorder(node) {
    if (!node) {
      result.push('null');
      return;
    }
    result.push(node.val);
    preorder(node.left);
    preorder(node.right);
  }
 
  preorder(root);
  return result.join(',');
};
 
var deserialize = function(data) {
  const values = data.split(',');
  let index = 0;
 
  function build() {
    if (values[index] === 'null') {
      index++;
      return null;
    }
 
    const node = new TreeNode(parseInt(values[index++]));
    node.left = build();
    node.right = build();
    return node;
  }
 
  return build();
};

9. Построение дерева из массивов

Preorder + Inorder → Tree

var buildTree = function(preorder, inorder) {
  if (preorder.length === 0) return null;
 
  const rootVal = preorder[0];
  const root = new TreeNode(rootVal);
  const mid = inorder.indexOf(rootVal);
 
  root.left = buildTree(
    preorder.slice(1, mid + 1),
    inorder.slice(0, mid)
  );
  root.right = buildTree(
    preorder.slice(mid + 1),
    inorder.slice(mid + 1)
  );
 
  return root;
};

N-ary Tree (произвольное количество детей)

Структура узла

function Node(val, children) {
  this.val = val;
  this.children = children || [];
}

Обходы N-ary

Preorder

var preorder = function(root) {
  const result = [];
 
  function traverse(node) {
    if (!node) return;
    result.push(node.val);
    for (const child of node.children) {
      traverse(child);
    }
  }
 
  traverse(root);
  return result;
};

Postorder

var postorder = function(root) {
  const result = [];
 
  function traverse(node) {
    if (!node) return;
    for (const child of node.children) {
      traverse(child);
    }
    result.push(node.val);
  }
 
  traverse(root);
  return result;
};

Level Order

var levelOrder = function(root) {
  if (!root) return [];
 
  const result = [];
  const queue = [root];
 
  while (queue.length > 0) {
    const levelSize = queue.length;
    const level = [];
 
    for (let i = 0; i < levelSize; i++) {
      const node = queue.shift();
      level.push(node.val);
      queue.push(...node.children);
    }
 
    result.push(level);
  }
 
  return result;
};

Максимальная глубина N-ary

var maxDepth = function(root) {
  if (!root) return 0;
 
  let max = 0;
  for (const child of root.children) {
    max = Math.max(max, maxDepth(child));
  }
 
  return 1 + max;
};

Сложности (Time & Space)

DFS (рекурсивный)

  • Time: O(n) — посещаем каждый узел один раз
  • Space: O(h) — стек рекурсии, где h = высота дерева
    • Сбалансированное: O(log n)
    • Худший случай (список): O(n)

BFS

  • Time: O(n) — посещаем каждый узел один раз
  • Space: O(w) — очередь, где w = максимальная ширина уровня
    • Худший случай: O(n) для полного дерева на последнем уровне

Мини-шпаргалка: какой паттерн выбрать?

ЗадачаПаттернКлюч
Отсортированный порядок в BSTInorder DFSleft→node→right
Минимальная разница в BSTInorder + prevСоседи в sorted
K-й элемент в BSTInorder + counterОстановка на k-м
Высота/глубинаPostorder DFSВычисляем после детей
ДиаметрPostorder + global maxВысота + макс путь
Копирование структурыPreorder DFSУзел до детей
Удаление дереваPostorder DFSДети до узла
По уровнямBFSОчередь
Path sumDFS + accumulatorПередаём остаток
LCAPostorder DFSРешаем снизу вверх
СимметрияЗеркальная рекурсияСравниваем left↔right
Валидация BSTInorder + prev ИЛИ диапазоныГлобальная проверка

Шпаргалка: задача → паттерн → ключ

ЗадачаПаттернКлюч
Отсортированный порядок в BSTInorder DFSleft→node→right
Минимальная разница в BSTInorder + prevСоседи в sorted
K-й элемент в BSTInorder + counterОстановка на k-м
Высота/глубинаPostorder DFSВычисляем после детей
ДиаметрPostorder + global maxВысота + макс путь
Копирование структурыPreorder DFSУзел до детей
Удаление дереваPostorder DFSДети до узла
По уровнямBFSОчередь
Path sumDFS + accumulatorПередаём остаток
LCAPostorder DFSРешаем снизу вверх
СимметрияЗеркальная рекурсияСравниваем left↔right
Валидация BSTInorder + prev ИЛИ диапазоныГлобальная проверка

Типичные ошибки

BST

  • ❌ Локальная проверка BST (left.val < node.val < right.val) — не учитывает глубокие поддеревья
  • ✅ Использовать диапазоны (min, max) или prev в inorder

Binary Tree

  • ❌ Использовать queue.shift() в цикле без причины — O(n) на каждый shift в JS
  • ✅ Использовать индекс for (let i = 0; i < queue.length; i++) или shift только для level-разделения
  • ❌ Забывать про null-checks перед обращением к .left/.right
  • ✅ Всегда проверять if (!node) return ...

Общее

  • ❌ Путать preorder/inorder/postorder — важно понимать порядок visit
  • ✅ Визуализировать на бумаге порядок посещения для конкретного примера
  • ❌ Смешивать DFS и BFS без понимания разницы в Space complexity
  • ✅ DFS экономит память на узких деревьях, BFS — на широких

Полезные ссылки