> Как написать структуру или класс для бинарного дерева поиска (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: Яндекс

Стек: iOS, Swift

> Пример ответа

Короткий ответ

Бинарное дерево поиска в Swift реализуется через класс с тремя свойствами: значение, левый и правый дочерние узлы. Класс предпочтительнее структуры из-за ссылочной семантики - дерево естественно рекурсивно, а структуры создают копии при присваивании, что ломает связи между узлами. Основные операции: insert, search, delete, обходы. Для senior-позиции важно показать понимание рекурсии, опциональных типов и управления памятью через weak-ссылки на родителя, если он нужен.

Подробное объяснение

Выбор между классом и структурой - ключевое архитектурное решение. Структура - value type, при присваивании копируется целиком. Для дерева это означает, что каждый insert создаёт копию всего поддерева, что катастрофически неэффективно по памяти и времени. Класс - reference type, узлы разделяются через ссылки, поэтому дерево остаётся единым объектом.

Базовая структура узла:

SWIFT
final class TreeNode<Value: Comparable> {
var value: Value
var left: TreeNode?
var right: TreeNode?
init(_ value: Value) {
self.value = value
}
}

Generic с Comparable позволяет сравнивать значения. Опциональные left/right - потому что узел может не иметь детей. final - запрет наследования, оптимизация диспетчеризации.

Сам класс дерева содержит только корневой узел:

SWIFT
final class BinarySearchTree<Value: Comparable> {
private(set) var root: TreeNode<Value>?
func insert(_ value: Value) { ... }
func contains(_ value: Value) -> Bool { ... }
func remove(_ value: Value) { ... }
}

Для рекурсивных операций удобно использовать приватные методы, принимающие узел и возвращающие изменённый узел. Это паттерн persistent data structure - мы не мутируем существующие узлы, а создаём новые при необходимости.

Удаление - самая сложная операция. Три случая: узел без детей (просто удаляем), с одним ребёнком (заменяем на ребёнка), с двумя детьми (находим минимальный узел в правом поддереве, копируем его значение, рекурсивно удаляем этот минимальный узел).

Для iOS-разработки важно помнить про weak-ссылку на родителя, если дерево используется в UI-контексте и возможны retain cycles. Но в простой реализации она не обязательна.

На практике

В реальных iOS-приложениях бинарное дерево поиска редко используется напрямую - обычно берут готовые коллекции из стандартной библиотеки. Но знание структуры важно для:

  • понимания сложности алгоритмов (O(log n) для сбалансированного дерева);
  • реализации автокомплита, поиска по префиксу (можно модифицировать в trie);
  • кэширования с приоритетами (совместно с max-heap);
  • собеседований - это классическая задача.

На практике стоит добавить методы обхода: in-order (возвращает отсортированный массив), pre-order, post-order. Для iOS полезен метод map, filter, forEach - чтобы дерево можно было использовать с функциональными конструкциями Swift.

Также стоит реализовать протокол CustomStringConvertible для отладки - это покажет внимание к деталям, важное для senior.

Пример кода

SWIFT
final class BinarySearchTree<Value: Comparable> {
private(set) var root: TreeNode<Value>?
func insert(_ value: Value) {
root = insert(value, into: root)
}
private func insert(_ value: Value, into node: TreeNode<Value>?) -> TreeNode<Value> {
guard let node else {
return TreeNode(value)
}
if value < node.value {
node.left = insert(value, into: node.left)
} else if value > node.value {
node.right = insert(value, into: node.right)
}
return node
}
func contains(_ value: Value) -> Bool {
var current = root
while let node = current {
if value == node.value {
return true
}
current = value < node.value ? node.left : node.right
}
return false
}
func remove(_ value: Value) {
root = remove(value, from: root)
}
private func remove(_ value: Value, from node: TreeNode<Value>?) -> TreeNode<Value>? {
guard let node else { return nil }
if value < node.value {
node.left = remove(value, from: node.left)
} else if value > node.value {
node.right = remove(value, from: node.right)
} else {
if node.left == nil {
return node.right
}
if node.right == nil {
return node.left
}
let minValue = findMin(in: node.right!)
node.value = minValue
node.right = remove(minValue, from: node.right)
}
return node
}
private func findMin(in node: TreeNode<Value>) -> Value {
var current = node
while let left = current.left {
current = left
}
return current.value
}
func inOrderTraversal() -> [Value] {
var result: [Value] = []
inOrderTraversal(from: root, into: &result)
return result
}
private func inOrderTraversal(from node: TreeNode<Value>?, into result: inout [Value]) {
guard let node else { return }
inOrderTraversal(from: node.left, into: &result)
result.append(node.value)
inOrderTraversal(from: node.right, into: &result)
}
}
final class TreeNode<Value: Comparable> {
var value: Value
var left: TreeNode?
var right: TreeNode?
init(_ value: Value) {
self.value = value
}
}

Как отвечать на собеседовании

Начните с вопроса: "Вам нужна структура или класс?" - это покажет, что вы анализируете требования. Затем объясните выбор класса из-за ссылочной семантики. Покажите код поэтапно: сначала узел, потом вставку, поиск, удаление. Для каждого метода называйте сложность: insert - O(h), contains - O(h), где h - высота дерева.

Обязательно упомяните, что в худшем случае (несбалансированное дерево) сложность деградирует до O(n). Спросите интервьюера, нужно ли реализовывать балансировку (AVL или красно-чёрное дерево) - это покажет глубину знаний.

Для senior-позиции добавьте рассуждение о памяти: weak-ссылки для избежания retain cycles, copy-on-write для структур, если бы мы всё-таки выбрали структуру. Также можно упомянуть, что в Swift есть готовые структуры данных, и дерево нужно только для специфических задач.

Что проверяет интервьюер

Интервьюер оценивает:

  • понимание value vs reference semantics в Swift;
  • умение работать с рекурсией и опциональными типами;
  • знание алгоритмической сложности;
  • способность писать чистый, generic-код;
  • внимание к edge cases: пустое дерево, удаление корня, дубликаты;
  • умение объяснять trade-off между простотой и производительностью.

Для senior-уровня важно, чтобы вы не просто написали код, а обосновали каждое решение: почему final, почему private(set), почему итеративный contains вместо рекурсивного.

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

  • Использование структуры вместо класса - ломает ссылочную целостность.
  • Забывают про generic constraint Comparable - компилятор не даст сравнивать значения.
  • Рекурсивный contains без хвостовой оптимизации - для глубоких деревьев может упасть по стеку.
  • Не обрабатывают случай удаления узла с двумя детьми - самая частая ошибка.
  • Не используют weak для родительских ссылок - создают retain cycle.
  • Путают in-order и pre-order обходы.
  • Не учитывают дубликаты: в классическом BST дубликаты либо запрещены, либо идут в правую ветку - нужно явно оговорить.
  • Пишут insert без возврата изменённого узла - теряют ссылки при рекурсивном вызове.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью