Читать книгу Карьера программиста. Как устроиться на работу в Google, Microsoft или другую ведущую IT-компанию - Гейл Макдауэлл - Страница 32
Часть VIII. Вопросы собеседования
Структуры данных. Вопросы и советы
4. Деревья и графы
ОглавлениеОчень часто кандидаты уверены, что задачи о деревьях и графах содержат больше всего подвохов. Вариант поиска структуры данных сложнее, чем вариант с линейно-организованной структурой (массив или связный список). Кроме того, время выполнения в наихудшем случае и среднее время могут существенно различаться, а вы должны оценить оба аспекта любого алгоритма. Скорость, с которой вы реализуете дерево или граф «с нуля», также играет важную роль.
Потенциальные ловушки
Деревья и графы – рассадник неоднозначностей и неправильных предположений. Убедитесь, что не упустили из виду какие-либо аспекты и сможете обосновать ваше решение в случае необходимости.
Бинарное дерево vs бинарное дерево поиска
Когда задан вопрос о бинарном дереве, многие кандидаты считают, что речь идет о бинарном дереве поиска. Уточните, что имел в виду интервьюер. Бинарное дерево поиска предполагает, что для всех узлов левые дети меньше или равны текущему узлу, а правые – больше текущего узла.
Сбалансировано vs несбалансировано
В большинстве случаев предполагается, что деревья сбалансированы, но не всегда. Попросите интервьюера уточнить задачу. Если дерево несбалансировано, вам придется создавать алгоритм, учитывая оптимизацию. Существует множество способов сбалансировать дерево так, чтобы глубина поддеревьев оставалась в пределах заданного диапазона. Но это не означает, что левое и правое поддеревья будут одинакового размера.
Полнота дерева
Полные деревья – это деревья, в которых все листья находятся внизу, а все остальные узлы, не являющиеся листьями, имеют по два потомка. Нужно отметить, что полные деревья – чрезвычайная редкость, так как дерево должно содержать 2n узлов, чтобы удовлетворять этому условию.
Обход бинарного дерева
Перед собеседованием попробуйте разобраться в реализации обхода дерева в прямом и обратном порядке. Наиболее часто используется обход в прямом порядке, левое поддерево – вершина – правое поддерево.
Балансировка: красно-черные и АВЛ-деревья
Хотя знание способов балансировки дерева поможет продемонстрировать, что вы являетесь великолепным разработчиком программного обеспечения, эти вопросы очень редко возникают во время собеседования. Вы должны знать время выполнения операций на сбалансированных деревьях и быть знакомы с основными способами балансировки, но подробности мало кого заинтересуют.
Префиксное дерево
Префиксное (нагруженное) дерево (trie) – это разновидность обычного n-арного дерева, в узлах которого хранятся не ключи, а символьные метки. Каждый путь по такому дереву является словом. Простое префиксное дерево имеет следующий вид:
Обход графа
Большинство кандидатов неплохо разбираются в бинарных деревьях, но обход графа требует несколько иного подхода. Особенно сложным является поиск в ширину. Не забудьте, что поиск в ширину (BFS, Breadth First Search) и поиск в глубину (DFS, Depth First Search) предназначены для разных сценариев. DFS – это самый простой способ посетить все узлы в графе, или, по крайней мере, посетить каждый узел, пока не найдется искомый. Однако при работе с очень большими деревьями DFS не подходит. В этих случаях лучше воспользоваться BFS.
Поиск в глубину
При DFS мы посещаем узел r и затем проходимся по всем ближайшим к r узлам. При попадании в узел n (соседний с r) мы посещаем всех его соседей. Таким образом, перед перемещением к следующему потомку узла r полностью обследуется узел n.
Обратите внимание, что прямой порядок и другие формы обхода дерева являются разновидностями DFS. Основное отличие состоит в том, что при реализации алгоритма в случае графа необходимо проверить, был ли посещен узел. Если этого не сделать, то можно застрять в бесконечном цикле.
Приведенный псевдокод реализует DFS:
1 void search(Node root) {
2 if (root!= null) return;
3 visit(root);
4 root.visited = true;
5 foreach (Node n in root.adjacent) {
6 if (n.visited == false)
7 search(n);
8 }
9 }
10 }
Поиск в ширину
Поиск в ширину (BFS) не так прост, у большинства кандидатов при первом знакомстве он вызывает затруднения. При BFS перед посещением любого из «внуков» r мы должны посетить всех соседей с узла r. Оптимальным решением в этом случае будет реализация с циклом и очередью:
1 void search(Node root) {
2 Queue queue = new Queue();
3 root.visited = true;
4 visit(root);
5 queue.enqueue(root); // Добавление в конец очереди
6
7 while (!queue.isEmpty()) {
8 Node r = queue.dequeue(); // Удаление из головы очереди
9 foreach (Node n in r.adjacent) {
10 if (n.visited == false) {
11 visit(n);
12 n.visited = true;
13 queue.enqueue(n);
14 }
15 }
16 }
17 }
Если вас попросят реализовать BFS, вспомните, что ключевым моментом является использование очереди. Остальную часть алгоритма можно построить исходя из этого факта.
Вопросы собеседования
4.1. Реализуйте функцию, проверяющую сбалансированность бинарного дерева. Предположим, что дерево считается сбалансированным, если разница высот двух поддеревьев любого узла не превышает 1.
4.2. Разработайте алгоритм поиска маршрута между двумя узлами для направленного графа.
4.3. Напишите алгоритм создания бинарного дерева поиска с минимальной высотой для отсортированного (по возрастанию) массива.
4.4. Для бинарного дерева поиска разработайте алгоритм, создающий связный список, состоящий из всех узлов заданной глубины (для дерева с глубиной D должно получиться D связных списков).
4.5. Реализуйте функцию проверки, является ли бинарное дерево бинарным деревом поиска.
4.6. Напишите алгоритм поиска «следующего» узла для заданного узла в бинарном дереве поиска. Можно считать, что у каждого узла есть ссылка на его родителя.
4.7. Создайте алгоритм и напишите код поиска первого общего предка двух узлов бинарного дерева. Постарайтесь избежать хранения дополнительных узлов в структуре данных. Примечание: необязательно использовать бинарное дерево поиска.
4.8. Дано: два очень больших бинарных дерева: T1 – с миллионами узлов и T2 – с сотнями узлов. Создайте алгоритм, проверяющий, является ли T2 поддеревом T1. Дерево T2 считается поддеревом T1, если существует такой узел n в T1, что поддерево, «растущее» из n, идентично дереву T2. (Если вы вырежете дерево в узле n и сравните его с T2, они окажутся идентичны.)
4.9. Дано бинарное дерево, в котором каждый узел содержит число. Разработайте алгоритм, выводящий на печать все пути, сумма значений которых соответствует заданной величине. Обратите внимание, что путь может начинаться и заканчиваться в любом месте дерева.
Дополнительные вопросы: сортировка и поиск (9.8), масштабируемость и лимиты памяти (11.2, 11.5), модерирование (17.13, 17.14), тяжелые вычисления (18.6, 18.8, 18.9, 18.10, 18.13).