Давно прошли те школьные годы. Получил работу айтишником в больнице. Сейчас пытаюсь заняться программированием. Сейчас я работаю над бинарными деревьями, и мне было интересно, как лучше всего определить, сбалансировано ли дерево по высоте.
Я думал об этом:
public boolean isBalanced(Node root){
if(root==null){
return true; //tree is empty
}
else{
int lh = root.left.height();
int rh = root.right.height();
if(lh - rh > 1 || rh - lh > 1){
return false;
}
}
return true;
}
Это хорошая реализация? или я что-то упускаю?
java
algorithm
data-structures
binary-tree
user69514
источник
источник
Ответы:
Наткнулся на этот старый вопрос, когда искал что-то еще. Я заметил, что вы так и не получили полного ответа.
Способ решения этой проблемы - начать с написания спецификации функции, которую вы пытаетесь написать.
Спецификация: правильно сформированное двоичное дерево называется "сбалансированным по высоте", если (1) оно пусто или (2) его левый и правый дочерние элементы сбалансированы по высоте, а высота левого дерева находится в пределах 1 от высота правого дерева.
Теперь, когда у вас есть спецификация, писать код несложно. Просто следуйте спецификации:
Перевод этого на язык программирования по вашему выбору должен быть тривиальным.
Дополнительное упражнение : этот наивный набросок кода слишком много раз пересекает дерево при вычислении высот. Можете ли вы сделать его более эффективным?
Супер бонусное упражнение : предположим, что дерево сильно разбалансировано. Например, миллион узлов глубиной с одной стороны и три глубоких - с другой. Есть ли сценарий, при котором этот алгоритм взрывает стек? Можете ли вы исправить эту реализацию, чтобы она никогда не взорвала стек, даже если дано сильно несбалансированное дерево?
ОБНОВЛЕНИЕ : Donal Fellows отмечает в своем ответе, что есть разные определения «сбалансированного», которые можно выбрать. Например, можно было бы взять более строгое определение «сбалансированной по высоте» и потребовать, чтобы длина пути до ближайшего пустого дочернего элемента находилась в пределах одного из путей к самому дальнему пустому дочернему элементу . Мое определение менее строгое, поэтому допускает больше деревьев.
Можно также быть менее строгим, чем мое определение; можно сказать, что сбалансированное дерево - это такое дерево, в котором максимальная длина пути к пустому дереву на каждой ветви отличается не более чем на два, три или некоторую другую константу. Или что максимальная длина пути составляет некоторую часть минимальной длины пути, например, половину или четверть.
Обычно это не имеет значения. Смысл любого алгоритма балансировки дерева состоит в том, чтобы гарантировать, что вы не попадете в ситуацию, когда у вас есть миллион узлов с одной стороны и три с другой. Определение Донала прекрасно в теории, но на практике сложно найти алгоритм балансировки дерева, который соответствует этому уровню строгости. Снижение производительности обычно не оправдывает затрат на внедрение. Вы тратите много времени на ненужную перестановку деревьев, чтобы достичь уровня баланса, который на практике не имеет большого значения. Кого волнует, что иногда требуется сорок ветвей, чтобы добраться до самого дальнего листа в несовершенно сбалансированном дереве с миллионами узлов, когда теоретически может потребоваться только двадцать в идеально сбалансированном дереве? Дело в том, что для этого никогда не нужно миллион. Обычно достаточно перейти от наихудшего случая в миллион к худшему случаю - сорок; вам не нужно полностью переходить к оптимальному случаю.
источник
Баланс - действительно тонкое свойство; вы думаете, что знаете, что это такое, но так легко ошибиться. В частности, даже (хороший) ответ Эрика Липперта неверен. Это потому, что понятия высоты недостаточно. Вам необходимо иметь представление о минимальной и максимальной высоте дерева (где минимальная высота - это наименьшее количество шагов от корня до листа, а максимальное - это ... ну, вы понимаете). Учитывая это, мы можем определить баланс как:
(На самом деле это означает, что ветви сами сбалансированы; вы можете выбрать одну и ту же ветвь как для максимума, так и для минимума.)
Все, что вам нужно сделать, чтобы проверить это свойство, - это простой обход дерева с отслеживанием текущей глубины. Когда вы в первый раз возвращаетесь назад, это дает вам базовую глубину. Каждый раз после этого, когда вы возвращаетесь назад, вы сравниваете новую глубину с базовой линией.
В коде:
Я полагаю, вы могли бы сделать это без использования паттерна Observer, но мне легче рассуждать таким образом.
[РЕДАКТИРОВАТЬ]: Почему нельзя просто взять высоту каждой стороны. Рассмотрим это дерево:
Хорошо, немного сумбурно, но каждая сторона корня сбалансирован:
C
это глубина 2,A
,B
,D
,E
являются глубина 3, иF
,G
,H
,J
являются глубина 4. Высота левой ветви 2 (помните , высота уменьшается , как вы траверсы ветвь), высота правой ветви равна 3. Тем не менее, дерево в целом не сбалансировано, поскольку разница в высоте междуC
и составляет 2F
. Вам нужна минимаксная спецификация (хотя фактический алгоритм может быть менее сложным, поскольку должно быть только две разрешенные высоты).источник
Это только определяет, сбалансирован ли верхний уровень дерева. То есть, у вас может быть дерево с двумя длинными ветвями слева и справа, без ничего посередине, и это вернет истину. Вам необходимо рекурсивно проверить
root.left
иroot.right
увидеть, сбалансированы ли они внутренне, прежде чем возвращать истину.источник
Дополнительная реакция на упражнения. Простое решение. Очевидно, что в реальной реализации можно обернуть это или что-то еще, чтобы не требовать от пользователя включать высоту в свой ответ.
источник
out height
» переменное обозначение)Решение для почтового заказа, пройти по дереву только один раз. Сложность времени - O (n), пространство - O (1), это лучше, чем решение сверху вниз. Я даю вам реализацию java-версии.
источник
left == -1
значит? Когда такое случится? Предполагаем ли мы, что рекурсивный вызов подразумевает, чтоleft == -1
это правда, если все поддеревья левых дочерних элементов неуравновешены?left == 1
означает, что левое поддерево неуравновешено, тогда несбалансировано все дерево. Нам больше не нужно проверять правильное поддерево, и мы можем вернуться-1
.Определение бинарного дерева со сбалансированной высотой:
Итак, пустое двоичное дерево всегда сбалансировано по высоте.
Непустое двоичное дерево сбалансировано по высоте, если:
Рассмотрим дерево:
Как видно, левое поддерево
A
сбалансировано по высоте (так как оно пусто), как и его правое поддерево. Но все же дерево не сбалансировано по высоте, так как условие 3 не выполняется, как высота левого поддерева0
и высота правого поддерева2
.Также следующее дерево не сбалансировано по высоте, хотя высота левого и правого поддерева равны. Ваш существующий код вернет true для этого.
Таким образом, слово каждый в определении очень важно.
Это будет работать:
Ссылка на Ideone
источник
Сбалансировано ли двоичное дерево или нет, можно проверить с помощью обхода порядка уровней:
источник
Это делается намного сложнее, чем есть на самом деле.
Алгоритм следующий:
Пусть B = глубина узла самого низкого уровня
Если abs (AB) <= 1, то дерево сбалансировано
источник
Какие сбалансированные средства немного зависят от имеющейся структуры. Например, B-дерево не может иметь узлы больше определенной глубины от корня или меньше, если на то пошло, все данные живут на фиксированной глубине от корня, но это может быть несбалансированным, если распределение листьев к листьям -но-один узел неровный. Пропускаемые списки вообще не имеют понятия о балансе, полагаясь вместо этого на вероятность для достижения достойной производительности. Деревья Фибоначчи намеренно выходят из равновесия, откладывая перебалансировку для достижения превосходной асимптотической производительности в обмен на периодические более длительные обновления. Деревья AVL и Red-Black прикрепляют метаданные к каждому узлу для достижения инварианта баланса глубины.
Все эти и многие другие структуры присутствуют в стандартных библиотеках наиболее распространенных систем программирования (кроме python, RAGE!). Внедрение одного или двух - это хорошая практика программирования, но, вероятно, не лучшее использование времени для создания собственного продукта, если только ваша проблема не связана с какой-то особенной производительностью, не удовлетворяемой никакими готовыми коллекциями.
источник
Примечание 1. Высота любого поддерева вычисляется только один раз.
Примечание 2: Если левое поддерево не сбалансировано, то вычисление правого поддерева, потенциально содержащего миллион элементов, пропускается.
источник
Балансировка обычно зависит от длины самого длинного пути в каждом направлении. Вышеупомянутый алгоритм не сделает этого за вас.
Что вы пытаетесь реализовать? Вокруг есть самобалансирующиеся деревья (АВЛ / Красно-черный). На самом деле деревья Java сбалансированы.
источник
Если это для вашей работы , я предлагаю:
источник
источник
1
(то же самое для minDepth). Правильная глубина, однако, должна быть0
. У корня дерева всегда есть0
глубинаВот полное проработанное протестированное решение на C # (извините, я не java dev) (просто скопируйте вставку в консольное приложение). Я знаю, что определение сбалансированности различается, поэтому не всем могут понравиться результаты моих тестов, но, пожалуйста, просто посмотрите на несколько иной подход проверки глубины / высоты в рекурсивном цикле и выхода при первом несоответствии без сохранения высоты / уровня / глубины узла на каждом узле (только поддерживая его в вызове функции).
источник
источник
RE: @ lucky с использованием BFS для обхода порядка уровней.
Мы проходим по дереву и сохраняем ссылку на переменные min / max-level, которые описывают минимальный уровень, на котором узел является листом.
Я считаю, что решение @lucky требует модификации. Как предлагает @codaddict, вместо того, чтобы проверять, является ли узел листом, мы должны проверить, является ли ЛИБО левый или правый дочерний элемент нулевым (не оба). В противном случае алгоритм будет считать это допустимым сбалансированным деревом:
В Python:
Это решение должно удовлетворять всем условиям, указанным в начальном вопросе, работать во времени O (n) и пространстве O (n). Переполнение памяти будет направлено в кучу, а не вызовет рекурсивный стек вызовов.
В качестве альтернативы, мы могли бы сначала пройти по дереву, чтобы итеративно вычислить максимальную высоту + кэширования для каждого корневого поддерева. Затем в другом итеративном прогоне проверьте, никогда ли не различаются ли кешированные высоты левого и правого поддеревьев для каждого корня более чем на единицу. Это также будет выполняться за время O (n) и пространство за O (n), но итеративно, чтобы не вызвать переполнение стека.
источник
Ну, вам нужен способ определить высоту левого и правого, и сбалансированы ли левый и правый.
И я бы просто
return height(node->left) == height(node->right);
Что касается написания
height
функции, прочтите: Понимание рекурсииисточник
О каком дереве вы говорите? Есть самобалансирующиеся деревья. Проверьте их алгоритмы, где они определяют, нужно ли им переупорядочивать дерево для поддержания баланса.
источник
Вот версия, основанная на общем обходе в глубину. Должен быть быстрее, чем другой правильный ответ, и справляться со всеми упомянутыми «проблемами». Извиняюсь за стиль, я вообще не знаю Java.
Вы все равно можете сделать это намного быстрее, вернувшись раньше, если установлены оба параметра max и min и имеют разницу> 1.
источник
источник
источник
Вот что я пробовал для бонусного упражнения Эрика. Я пытаюсь размотать свои рекурсивные циклы и вернуться, как только обнаруживаю, что поддерево не сбалансировано.
источник
источник
Пустое дерево сбалансировано по высоте. Непустое двоичное дерево T сбалансировано, если:
1) Левое поддерево T сбалансировано
2) Правое поддерево T сбалансировано
3) Разница между высотами левого поддерева и правого поддерева не более 1.
Сложность времени: O (n)
источник
Чтобы получить лучшую производительность, особенно на огромных деревьях, вы можете сохранить высоту в каждом узле, так что это компромисс между пространством и производительностью:
Пример реализации добавления и того же для удаления
источник
источник
Разве это не сработает?
Любое несбалансированное дерево всегда терпит неудачу.
источник