> Как написать структуру или класс для бинарного дерева поиска (iOS, Swift)
Уровень: senior · Роль: mobile · Категория: Технические вопросы
Компании: Яндекс
Стек: iOS, Swift
> Пример ответа
Короткий ответ
Бинарное дерево поиска в Swift реализуется через класс с тремя свойствами: значение, левый и правый дочерние узлы. Класс предпочтительнее структуры из-за ссылочной семантики - дерево естественно рекурсивно, а структуры создают копии при присваивании, что ломает связи между узлами. Основные операции: insert, search, delete, обходы. Для senior-позиции важно показать понимание рекурсии, опциональных типов и управления памятью через weak-ссылки на родителя, если он нужен.
Подробное объяснение
Выбор между классом и структурой - ключевое архитектурное решение. Структура - value type, при присваивании копируется целиком. Для дерева это означает, что каждый insert создаёт копию всего поддерева, что катастрофически неэффективно по памяти и времени. Класс - reference type, узлы разделяются через ссылки, поэтому дерево остаётся единым объектом.
Базовая структура узла:
SWIFTfinal class TreeNode<Value: Comparable> {var value: Valuevar left: TreeNode?var right: TreeNode?init(_ value: Value) {self.value = value}}
Generic с Comparable позволяет сравнивать значения. Опциональные left/right - потому что узел может не иметь детей. final - запрет наследования, оптимизация диспетчеризации.
Сам класс дерева содержит только корневой узел:
SWIFTfinal 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.
Пример кода
SWIFTfinal 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 = rootwhile 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 = minValuenode.right = remove(minValue, from: node.right)}return node}private func findMin(in node: TreeNode<Value>) -> Value {var current = nodewhile 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: Valuevar 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 без возврата изменённого узла - теряют ссылки при рекурсивном вызове.
> Похожие задачи по mobile
Что такое сайд таблица (side table) в контексте weak ссылок и как она работает
Как сделать потокобезопасным общий массив при синхронных операциях в concurrent очереди
Всегда ли структуры хранятся в стеке
Как сделать так, чтобы функция имела доступ к оригинальной структуре без копирования
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью