Как создать максимально несбалансированные деревья AVL
Я написал библиотека языков C деревьев AVL в качестве контейнеров общего назначения. Для целей тестирования я хотел бы иметь способ заполнить дерево так, чтобы оно было максимально несбалансированным, т. е. чтобы оно имело максимальную высоту для количества узлов, которые оно содержит.
деревья AVL имеют хорошее свойство, что если, начиная с пустого дерева, вы вставляете узлы в порядке возрастания (или убывания), дерево всегда точно сбалансировано (т. е. имеет свой минимум высота для заданного количества узлов). Одна последовательность целочисленных ключей, которая генерирует точно сбалансированное дерево AVL Tn для каждого числа узлов n, начиная с пустого дерева T0, просто
- k1 = 0
- kn+1 = kn+1, то есть kn = n-1
Я ищу (надеюсь, простую) последовательность целочисленных ключей, которые при вставке в изначально пустые дерево Т0, генерирует деревья AVL T0, ..., Tn вот и все максимально unсбалансированный.
меня также заинтересовало бы решение, где только последнее дерево, Tn, максимально несбалансирован (число узлов n будет параметром алгоритма).
решение, удовлетворяющее ограничение
- max (k1, ..., kn) - min (k1, ..., kn) + 1 ≤ 2 n
желательно, но не строго обязательна. Разумной целью может быть ключевой диапазон 4 n вместо 2 n.
Я не смог найти ничего в интернете относительно генерации, путем вставки, деревьев AVL максимальной высоты. Конечно, последовательность сгенерированных деревьев, которую я ищу, будет включать все так называемые деревья Фибоначчи, которые являются деревьями AVL заданной глубины с минимальным количеством узлов. Забавно, в Английская Википедия даже не упоминает деревья Фибоначчи (ни числа Фибоначчи!) в статье о деревьях AVL, в то время как немецкая Википедия имеет очень хороший статьи полностью посвященный им. Но я все еще в неведении относительно своего вопроса.
C язык бит twiddling хаки приветствуются.
3 ответов
Простое Решение
деревья Фибоначчи имеют несколько свойств, которые могут быть использованы для формирования компактного дерева Фибоначчи:
- каждый узел в дереве Фибоначчи сам является деревом Фибоначчи.
- число узлов в дереве Фибоначчи высотой n равно Fn+2 - 1.
- число узлов между узлом и его левым дочерним элементом равно числу узлов в правом левом дочернем узле ребенок.
- число узлов между узлом и его правым дочерним элементом равно числу узлов в левом дочернем элементе правого дочернего элемента узла.
без потери общности предположим, что наше дерево Фибоначчи имеет следующее дополнительное свойство:
- если узел имеет высоту N, то левый ребенок высота н-2, и право ребенка высота N-1.
объединяя эти свойства, мы обнаруживаем, что количество узлов в между узлом высоты n и его левым и правым потомками равно Fn-1 - 1, и мы можем использовать этот факт для создания компактного дерева Фибоначчи:
static int fibs[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170};
void fibonacci_subtree(int root, int height, int *fib)
{
if (height == 1) {
insert_into_tree(root);
} else if (height == 2) {
insert_into_tree(root + *fib);
} else if (height >= 3) {
fibonacci_subtree(root - *fib, height - 2, fib - 2);
fibonacci_subtree(root + *fib, height - 1, fib - 1);
}
}
...
for (height = 1; height <= max_height; height++) {
fibonacci_subtree(0, height, fibs + max_height - 1);
}
этот алгоритм генерирует минимальное количество узлов, возможных для заданной высоты, а также создает минимально возможный диапазон. Вы можете сдвинуть диапазон, сделав корневой узел чем-то отличным от нуля.
Компактный Алгоритм Заполнения
только основное решение производит деревья Фибоначчи, которые всегда имеют Fn+2 - 1 узлов. Что делать, если вы хотите создать несбалансированное дерево с другим количеством узлов, все еще минимизируя диапазон?
в этом случае вам нужно создать следующее большее дерево Фибоначчи с несколькими изменениями:
- некоторое количество элементов в конце последовательности вставляться не будет.
- эти элементы создадут пробелы, и расположение этих пробелов должно быть гусеничный.
- разница между узлами должна быть соответствующим образом снижена.
вот один подход, который по-прежнему использует рекурсивный характер решения:
void fibonacci_subtree(int root, int height, int *fib, int num_gaps, bool prune_gaps)
{
if(height < 1)
return;
if(prune_gaps && height <= 2) {
if(!num_gaps) {
if(height == 1) {
insert_into_tree(root);
} else if(height == 2) {
insert_into_tree(root + *fib);
}
}
return;
}
if(height == 1) {
insert_into_tree(root);
} else {
int max_rr_gaps = *(fib - 1);
int rr_gaps = num_gaps > max_rr_gaps ? max_rr_gaps : num_gaps;
num_gaps -= rr_gaps;
int max_rl_gaps = *(fib - 2);
int rl_gaps = num_gaps > max_rl_gaps ? max_rl_gaps : num_gaps;
num_gaps -= rl_gaps;
int lr_gaps = num_gaps > max_rl_gaps ? max_rl_gaps : num_gaps;
num_gaps -= lr_gaps;
int ll_gaps = num_gaps;
fibonacci_subtree(root - *fib + lr_gaps, height - 2, fib - 2, lr_gaps + ll_gaps, prune_gaps);
fibonacci_subtree(root + *fib - rl_gaps, height - 1, fib - 1, rr_gaps + rl_gaps, prune_gaps);
}
}
основной цикл немного сложнее для размещения произвольного диапазона ключей:
void compact_fill(int min_key, int max_key)
{
int num_nodes = max_key - min_key + 1;
int *fib = fibs;
int max_height = 0;
while(num_nodes > *(fib + 2) - 1) {
max_height++;
fib++;
}
int num_gaps = *(fib + 2) - 1 - num_nodes;
int natural_max = *(fib + 1) - 1;
int max_r_gaps = *(fib - 1);
int r_gaps = num_gaps > max_r_gaps ? max_r_gaps : num_gaps;
natural_max -= r_gaps;
int root_offset = max_key - natural_max;
for (int height = 1; height <= max_height; height++) {
fibonacci_subtree(root_offset, height, fibs + max_height - 1, num_gaps, height == max_height);
}
}
Решение Закрытой Формы
если вы посмотрите на различия между каждой парой слов, генерируемых базовым решением, вы найдите, что они чередуются между двумя последовательными элементами последовательности Фибоначчи. Этот переменный шаблон определяется Фибоначчи слово:
слово Фибоначчи-это определенная последовательность двоичных цифр (или символов из любого двухбуквенного алфавита). Слово Фибоначчи формируется повторным сцеплением таким же образом, как числа Фибоначчи формируются повторным сложением.
оказывается есть закрытые-форма решение для Слова Фибоначчи:
static double phi = (1.0 + sqrt(5.0)) / 2.0;
bool fibWord(int n)
{
return 2 + floor(n * phi) - floor((n + 1) * phi);
}
вы можете использовать это решение закрытой формы для решения проблемы с помощью двух вложенных циклов:
// Used by the outer loop to calculate the first key of the inner loop
int outerNodeKey = 0;
int *outerFib = fibs + max_height - 1;
for(int height = 1; height <= max_height; height++) {
int innerNodeKey = outerNodeKey;
int *smallFib = fibs + max_height - height + 3; // Hat tip: @WalterTross
for(int n = fibs[height] - 1; n >= 0; n--) {
insert_into_tree(innerNodeKey);
// Use closed-form expression to pick between two elements of the Fibonacci sequence
bool smallSkip = 2 + floor(n * phi) - floor((n + 1) * phi);
innerNodeKey += smallSkip ? *smallFib : *(smallFib + 1);
}
if(height & 0x1) {
// When height is odd, add *outerFib.
outerNodeKey += *outerFib;
} else {
// Otherwise, backtrack and reduce the gap for next time.
outerNodeKey -= (*outerFib) << 1;
outerFib -= 2;
}
}
Я нашел этот ответ на свой вопрос, но я все еще надеюсь, что можно найти более простой и, особенно, более эффективный во времени и не менее эффективный в пространстве алгоритм, надеюсь, с гораздо лучшими свойствами диапазона ключей.
идея состоит в том, чтобы генерировать деревья Фибоначчи до заданной высоты (которая должна быть известна заранее), полностью избегая всех вращений деревьев. Промежуточные деревья удерживаются AVL-сбалансированными по выбору порядка вставки. Так как они имеют высоту нижние из двух деревьев Фибоначчи, которые они связывают, все они максимально несбалансированы.
вставки выполняются практически путем вставки всех узлов в последовательности деревьев Фибоначчи, но для каждого виртуального дерева эффективно вставлять только узлы, которые являются поддеревьями высоты 1. Это "инкрементные" узлы между двумя последовательными деревьями Фибоначчи.
вот как это работает в случае max_height = 5
:
insert 0
=> Fibonacci tree of height 1 (1 node):
0
insert 8
=> Fibonacci tree of height 2 (2 nodes):
0
8
insert -8
insert 12
=> Fibonacci tree of height 3 (4 nodes):
0
-8 8
12
insert -4
insert 4
insert 14
=> Fibonacci tree of height 4 (7 nodes):
0
-8 8
-4 4 12
14
insert -12
insert -2
insert 6
insert 10
insert 15
=> Fibonacci tree of height 5 (12 nodes):
0
-8 8
-12 -4 4 12
-2 6 10 14
15
и вот код (упрощенно):
void fibonacci_subtree(int root, int height, int child_delta)
{
if (height == 1) {
insert_into_tree(root);
} else if (height == 2) {
insert_into_tree(root + child_delta);
} else if (height >= 3) {
fibonacci_subtree(root - child_delta, height - 2, child_delta >> 1);
fibonacci_subtree(root + child_delta, height - 1, child_delta >> 1);
}
}
...
for (height = 1; height <= max_height; height++) {
fibonacci_subtree(0, height, 1 << (max_height - 2));
}
обновление
на решение от godel9 решает проблему распространения ключей этого решения. Вот вывод кода godel9:
insert 0
=> Fibonacci tree of height 1 (1 node):
0
insert 3
=> Fibonacci tree of height 2 (2 nodes):
0
3
insert -3
insert 5
=> Fibonacci tree of height 3 (4 nodes):
0
-3 3
5
insert -2
insert 1
insert 6
=> Fibonacci tree of height 4 (7 nodes):
0
-3 3
-2 1 5
6
insert -4
insert -1
insert 2
insert 4
insert 7
=> Fibonacci tree of height 5 (12 nodes):
0
-3 3
-4 -2 1 5
-1 2 4 6
7
и вот код в версии, ближайшей к моей (здесь со статическим fibs
array):
static int fibs[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170 };
void fibonacci_subtree(int root, int height, int *fib)
{
if (height == 1) {
insert_into_tree(root);
} else if (height == 2) {
insert_into_tree(root + *fib);
} else if (height >= 3) {
fibonacci_subtree(root - *fib, height - 2, fib - 2);
fibonacci_subtree(root + *fib, height - 1, fib - 1);
}
}
...
for (height = 1; height <= max_height; height++) {
fibonacci_subtree(0, height, fibs + max_height - 1);
}
конечное дерево Фибоначчи высоты H имеет FH+2 - 1 узлов без "отверстий" между ключевыми значениями и имеет kМакс - kroot = FH+1 - 1. Диапазон ключей может быть удобно расположен, при необходимости, путем смещения значения ключа корня и опционально обмена влево и вправо в алгоритме.
остается нерешенным компактное заполнение любого заданного диапазона ключей целочисленными ключами (хотя это тривиально для точно сбалансированных деревьев). С помощью этого алгоритма, если вы хотите сделать максимально несбалансированное дерево с n узлами (с целочисленные ключи), где n не является числом Фибоначчи-1, и вы хотите уменьшить возможный диапазон ключей, вы можете найти первую высоту, которая может вместить n узлов, а затем запустить алгоритм для этой высоты, но остановиться, когда N узлов были вставлены. Ряд " дыр " останется (в худшем случае ок. n / φ ≅ n / 1.618).
вопреки моему интуитивному пониманию, когда я писал введение в это решение, эффективность этого алгоритма во времени, с или без godel9 важное улучшение, уже оптимальное, так как это O(n) (так что, когда вставки включены, он становится O(N log n)). Это связано с тем, что количество операций пропорционально сумме узлов всех деревьев Фибоначчи из TF1 = T1 ТFH = Tn, т. е. N = Σh=1...H(Fh+2 - 1) = FH+4 - H-1. Но два последовательных Фибоначчи числа имеют асимптотическое отношение φ ≅ 1.618, то золотой пропорции, так что N / n φ φ2 ≅ 2.618. Вы можете сравнить это с полностью сбалансированными двоичными деревьями, где применяются очень похожие Формулы, только с "логарифмом" 2 вместо φ.
хотя я сомневаюсь, что стоило бы избавиться от φ2 фактор, учитывая простоту текущего кода, по-прежнему интересно отметить следующее: При добавлении " инкрементного" узлы любого промежуточного дерева Фибоначчи высотой h, разница между двумя последовательными ключами этой "границы Фибоначчи" (мой термин) либо FH-h+3 или FH-h+4, в своеобразном шахматном порядке. Если бы мы знали правило генерации для этих различий, мы могли бы заполнить дерево просто двумя вложенными циклами.
интересный вопрос. Похоже, у вас уже есть хорошее решение, но я бы нашел более комбинаторный подход проще.
предположения:
пусть U (n) представляет число узлов в максимально несбалансированном дереве AVL высотой n.
U (0) = 0
U (1) = 1
U (n) = U(n-1) + U (n-2) + 1 для n>=2 (т. е. корневой узел плюс два максимально несбалансированных subtrees)
для удобства предположим, что U(n-1) всегда является левым поддеревом, а U (n-2) всегда является правым.
пусть каждый узел будет представлен уникальной строкой, представляющей путь от корня до узла. Корневой узел-это строка emptry, узлы уровня 1 - "L" и "R", узлы уровня 2 - "LL", "LR", "RL" и " RR " и т. д.
выводы:
допустимая строка для узел на уровне A в U (n) имеет длину символов и удовлетворяет неравенству: n - count ("L") - 2 * count ("R") >= 1
count ("L") + count ("R") = A или count ("L") = A - count ("R")
таким образом, count ("R")
-
мы можем использовать следующие функции для генерации всех допустимых путей на заданном уровне и определить значения ключа в каждом узле.
void GeneratePaths(int height, int level) { int rLimit = height - level - 1; GeneratePaths(height, rLimit, level, string.Empty, 0); } void GeneratePaths(int height, int rLimit, int level, string prefix, int prefixlen) { if (prefixlen + 1 < level) { GeneratePaths(height, rLimit, level, prefix + "L", prefixlen + 1); if (rLimit > 0) GeneratePaths(height, rLimit - 1, level, prefix + "R", prefixlen + 1); } else if (prefixlen + 1 == level) { InsertNode(prefix + "L", height) if (rLimit > 0) InsertNode(prefix + "R", height); } } void InsertNode(string path, int height) { int key = fibonacci(height); int index = height - 2; for (int i=0; i < path.length(), i++) { int difference = fibonacci(index); char c = path.charAt(i); if (c == 'L') { key -= difference; index -= 1; } else if (c == 'R') { key += difference; index -= 2; } } InsertKey(key); }
Если вы используете эти функции чтобы создать дерево U(5), вы получите этот результат. (Обратите внимание, что клавиши на левом краю дерева следуют последовательности Фибоначчи от 1 до 5,)
5
3 7
2 4 6 8
1 3 4 6
1