Maximum Depth of Binary Tree
Вам дано бинарное дерево, хранящееся в массиве tree в порядке уровней. Корень находится по индексу 0, дети узла с индексом i находятся по индексам 2*i+1 (левый) и 2*i+2 (правый), -1 обозначает пустое место, а в конце массива могут быть дополнительные элементы -1. Верните максимальную глубину дерева: количество узлов на самом длинном пути от корня до листа.
Функция
- treeinteger-array
- двоичное дерево в порядке уровней, где -1 обозначает пустое место
- Возвращаетinteger
- количество узлов на самом длинном пути от корня до листа
Ограничения
1 ≤ tree.length ≤ 32767- Каждый элемент
tree[i]равен-1или является значением, для которого выполняется0 ≤ tree[i] ≤ 1000. tree[0]никогда не равно-1, поэтому в дереве есть как минимум один узел.- После последнего узла в массиве могут находиться дополнительные элементы
-1. - Оба потомка пустого места тоже пусты, а глубина не превышает
14.
Примеры
- Ввод
- tree = [5, 8, 1, -1, 3, -1, -1, -1, -1, 6]
- Вывод
- 4
- Пояснение
- Самый длинный путь —
5,8,3,6(индексы0,1,4,9), в нём 4 узла. Путь через1заканчивается после 2 узлов.
- Ввод
- tree = [7, -1, -1]
- Вывод
- 1
- Пояснение
- Две записи
-1— это пустые места для потомков корневого узла. Один корневой узел сам по себе образует путь из одного узла, поэтому глубина равна1, а не0.
- Ввод
- tree = [2, -1, 9, -1, -1, -1, 4]
- Вывод
- 3
- Пояснение
- У корня
2нет левого потомка. Его правый потомок9с индексом2имеет4с индексом6в качестве правого потомка — путь из 3 узлов.
+13 скрытых тестов при отправке
Дополнительный вопрос
Как бы вы вернули значения на самом длинном пути от корня к листу, а не только его длину? Если несколько путей имеют одинаковую длину, какой из них вы бы вернули и как бы указали это в контракте?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Подумай о корне. Если бы ты знал глубину его левого поддерева и глубину его правого поддерева, какой была бы глубина всего дерева?
Это
1плюс большая из глубин двух поддеревьев, а глубина пустого места равна0. То же правило действует для каждого узла, поэтому обход, который знает глубину каждого узла, может найти ответ.Храните стек пар: индекс узла и его глубину, начиная с корня на глубине 1. Извлеките пару, запомните наибольшую встреченную глубину и добавьте в стек каждого потомка с индексом
2*i+1и2*i+2, если он находится внутри массива и не равен-1, увеличив глубину на единицу.
Решение
Глубина определяется самой длинной ветвью, и нельзя узнать, какая именно ветвь самая длинная, не просмотрев каждый узел. Поэтому задача заключается в полном обходе, который отслеживает глубину на каждом узле. Рекурсия, поиск в ширину по уровням и поиск в глубину с собственным стеком выполняют эту задачу за один проход; они различаются тем, как отслеживают своё текущее положение.
Рекурсия в двух поддеревьях
Идея
Сначала разберёмся, как перемещаться по массиву. У узла с индексом i левый потомок находится по индексу 2*i+1, а правый — по индексу 2*i+2. Потомок существует, только если его индекс находится в пределах массива и значение по этому индексу не равно -1. В массиве [5, 8, 1, -1, 3, -1, -1, -1, -1, 6] у корня 5 потомки находятся по индексам 1 и 2, у узла 8 с индексом 1 левая позиция 3 пуста, а справа от него, по индексу 4, находится узел 3; ниже этого узла 3 находится узел 6 с индексом 9.
Теперь разберём идею. Самый длинный путь через узел идёт вниз по тому из двух поддеревьев, которое глубже. Поэтому глубина поддерева с корнем в узле с индексом i равна 1 для самого узла плюс большая из глубин поддеревьев с корнями в узлах 2*i+1 и 2*i+2. Пустая позиция имеет глубину 0, что завершает рекурсию. Глубина листа равна 1 + max(0, 0) = 1, а значения возвращаются вверх к корню.
Каждый узел посещается один раз, поэтому время работы составляет O(n). В стеке вызовов хранится по одному кадру на каждый уровень текущего пути — O(h), где h — глубина; здесь она не превышает 14. Именно это ограничение делает рекурсию безопасной в этой задаче. В дереве на указателях, имеющем форму длинной цепочки, тот же код достиг бы предела рекурсии, который в Python составляет 1000 кадров.
Алгоритм
- Напиши
depth(i): еслиiвыходит за конец массива илиtree[i]равен-1, верни0. - В противном случае верни
1 + max(depth(2*i+1), depth(2*i+2)). - Верни
depth(0).
def maxDepth(tree):
def depth(i):
# An index past the end or a -1 is an empty spot: depth 0.
if i >= len(tree) or tree[i] == -1:
return 0
return 1 + max(depth(2 * i + 1), depth(2 * i + 2))
return depth(0)Поиск в ширину, уровень за уровнем
Идея
Максимальная глубина — это количество уровней в дереве, поэтому можно считать уровни, а не проходить по путям. Очередь посещает узлы в порядке уровней: сначала поместите в неё корень, а затем каждый раз, когда извлекаете узел, добавляйте его существующих потомков в конец очереди.
Чтобы подсчитать уровни, обрабатывайте очередь пакетами. Перед каждым пакетом узнайте, сколько узлов находится в очереди. Это в точности узлы одного уровня, поскольку потомки, которых вы добавляете во время обработки пакета, встают за ними. Извлеките столько узлов, поместите их потомков в очередь и прибавьте 1 к глубине. Когда очередь опустеет, глубина будет равна количеству пакетов. В первом примере пакеты — это [5], [8, 1], [3] и [6], поэтому ответ — 4.
Каждый узел один раз попадает в очередь и один раз извлекается из неё; временная сложность — O(n). В очереди одновременно находится один уровень; пространственная сложность для самого широкого уровня w — O(w). В полном дереве на нижнем уровне находится примерно половина узлов: 8192 из 16383 при глубине 14.
Алгоритм
- Помести корневой индекс
0в очередь и установиdepth = 0. - Пока очередь не пуста, прибавляй
1кdepthи считывай размер очереди. - Извлеки столько индексов. Для каждого добавь в очередь дочерние индексы
2*i+1и2*i+2, которые находятся внутри массива и не равны-1. - Когда очередь опустеет, верни
depth.
from collections import deque
def maxDepth(tree):
n = len(tree)
queue = deque([0])
depth = 0
while queue:
depth += 1
for _ in range(len(queue)): # exactly the nodes of this level
i = queue.popleft()
for child in (2 * i + 1, 2 * i + 2):
if child < n and tree[child] != -1:
queue.append(child)
return depthПоиск в глубину с явным стеком
Идея
Ты можешь проходить по путям, как это делает рекурсия, не выполняя ни одного рекурсивного вызова. Используй собственный стек и храни каждый узел вместе с его глубиной, поскольку ничто другое не запоминает, насколько глубоко он находится. Начни с пары (0, 1): корня на глубине 1.
Извлеки пару, сравни её глубину с наибольшей из уже встреченных и добавь в стек каждого существующего потомка с depth + 1. Каждый узел дерева добавляется в стек ровно один раз вместе с длиной пути, который к нему ведёт, поэтому ответ — это максимальная глубина среди извлечённых пар. В первом примере 6 с индексом 9 добавляется в стек как (9, 4), и ни одна пара не уходит глубже.
Время работы — O(n). В стеке хранятся ожидающие обработки соседние узлы вдоль текущего пути — не более примерно одного на каждом уровне, поэтому затраты памяти составляют O(h), как и при рекурсии, но без риска переполнить стек вызовов. Этот вариант стоит выбирать, когда дерево может быть глубоким; он также без изменений подходит для деревьев на основе указателей.
Алгоритм
- Помести
(0, 1)в стек и установиbest = 0. - Извлеки пару
(i, depth)и установиbestравным большему изbestиdepth. - Для каждого индекса потомка
2*i+1и2*i+2, который находится в пределах массива и не равен-1, помести его в стек сdepth + 1. - Повторяй, пока стек не опустеет, затем верни
best.
def maxDepth(tree):
n = len(tree)
best = 0
stack = [(0, 1)] # (node index, depth of that node); the root is never empty
while stack:
i, depth = stack.pop()
best = max(best, depth)
for child in (2 * i + 1, 2 * i + 2):
if child < n and tree[child] != -1:
stack.append((child, depth + 1))
return best
Ловушки и крайние случаи
Большинство неправильных ответов на эту задачу отличаются на единицу или возникают из-за того, что пустое место принимают за узел.
- Подсчёт рёбер вместо узлов. Глубина одного узла здесь равна
1; если вернуть для него0или3для пути из 4 узлов, результат будет на единицу меньше. - Пропуск проверки границ. У листа ближе к концу массива индексы дочерних узлов могут выходить за его последнюю позицию, поскольку массив может заканчиваться сразу после последнего узла. Перед чтением
tree[child]проверьтеchild < n. - Определение глубины по длине массива. В конце массива могут быть дополнительные записи
-1, поэтому его длина может соответствовать уровню глубже, чем любой настоящий узел. - Восприятие
-1как значения. Оно обозначает отсутствующий узел, поэтому его нельзя добавлять в стек или очередь и нельзя учитывать при подсчёте. - Предположение, что дерево сбалансировано. Ответ определяется самой длинной ветвью, например цепочкой из 14 узлов слева, где все правые места пусты.
- Чтение размера очереди внутри цикла в версии с обходом в ширину. Размер меняется при добавлении дочерних узлов, поэтому сохраните его до начала обработки группы.
- Путаница со смещением в Lua и R, где нумерация массивов начинается с 1. Оставьте индексы узлов с нуля для вычисления
2*i+1и считывайтеtree[i + 1].
Частые вопросы4
Какова временная сложность алгоритма нахождения максимальной глубины бинарного дерева?
Каждый подход посещает каждый узел один раз, поэтому временная сложность составляет O(n). Версии с поиском в глубину используют дополнительную память O(h) для исследуемого пути, где h — это глубина. Версия с поиском в ширину использует O(w) для самого широкого уровня, на котором в полном дереве может находиться около половины узлов.
Следует ли использовать DFS или BFS для определения максимальной глубины бинарного дерева?
Оба алгоритма дают правильный ответ за время O(n). Поиск в глубину короче в записи и использует память, пропорциональную глубине, поэтому подходит для широких и неглубоких деревьев. Поиск в ширину напрямую подсчитывает уровни и использует память, пропорциональную самому широкому уровню, поэтому подходит для глубоких и узких деревьев. Для определения минимальной глубины преимущество у BFS, поскольку он может остановиться на первом встреченном листе.
Как найти максимальную глубину бинарного дерева без рекурсии?
Используй явный стек пар: узел и его глубина. Начни с корня на глубине 1, извлекай пару, записывай её глубину и добавляй каждого потомка с глубиной на единицу больше. Наибольшая извлечённая глубина и будет ответом. Также подойдёт очередь, обрабатываемая по одному уровню за раз: считай по одному за каждый уровень.
В чём разница между глубиной и высотой бинарного дерева?
Глубина узла — это количество шагов от корня до него, а высота узла — количество шагов от него до самого глубокого листа. Максимальная глубина дерева и высота корня — это одно и то же число. В этой задаче считаются узлы, поэтому глубина единственного узла равна 1; в некоторых книгах вместо этого считают рёбра, что даёт на единицу меньше.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def maxDepth(tree):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
tree = [5, 8, 1, -1, 3, -1, -1, -1, -1, 6]
Ожидается
4