如何生成最大不平衡的 AVL 树

Posted

技术标签:

【中文标题】如何生成最大不平衡的 AVL 树【英文标题】:How to generate maximally unbalanced AVL trees 【发布时间】:2013-11-06 12:05:25 【问题描述】:

我写了一个C language library of AVL trees as general purpose sorted containers。出于测试目的,我想有一种方法来填充树,使其最大程度地不平衡,即,使其包含的节点数具有最大高度。

AVL 树有一个很好的特性,如果从空树开始,按升序(或降序)顺序插入节点,树总是完全平衡的(即,对于给定数量的节点,它的最小高度) .从空树 T0 开始,为每个节点数 n 生成一个完全平衡的 AVL 树 Tn 的整数键序列就是

k1 = 0 kn+1 = kn+1 ,即 kn = n-1

我正在寻找一个(希望是简单的)整数键序列,当它插入到最初为空的树 T0 中时,会生成 AVL 树 T0, 。 .., Tn 都是最大un平衡的。

我也对只有最后一棵树 Tn 最大程度不平衡的解决方案感兴趣(节点数 n 将是算法的参数)。

满足约束的解

max(k1, ..., kn) - min(k1, ..., k n) + 1 ≤ 2 n

是可取的,但不是严格要求的。 4 n 而不是 2 n 的键范围可能是一个合理的目标。

我无法在 Internet 上找到有关通过插入生成最大高度的 AVL 树的任何信息。当然,我正在寻找的生成树的序列将包括所有所谓的斐波那契树,它们是具有给定深度且节点数最少的 AVL 树。有趣的是,英文***在关于 AVL 树的文章中甚至没有提到斐波那契树(也没有提到斐波那契数字!),而德语***有一个非常好的 article 完全致力于它们。但我仍然对我的问题一无所知。

欢迎使用 C 语言小技巧。

【问题讨论】:

4 byte ints * 2^31 ints = 2^33 bytes 或 8GB 内存,无需测量树指针。我希望操作系统和程序的 RAM 占用空间很小...并且没有没有其他进程正在运行... 对,64 位整数,因为您无法在 32 位架构中引用更多的 2GB 内存。 由于一致的升序/降序创建了一个完美平衡的树,也许将插入分成升序和降序段会造成不平衡。插入排序示例:[-1,-2,...,INTMIN,INTMAX,INTMAX-1,...,0]。没有证据,只是一个想法...... @awashburn,您使用 64 个键的序列产生的最大深度为 7,而不是 8,我认为它不会更好地使用更高的 2 次方。无论如何,我已经编辑了我的问题。对键范围的约束几乎消失了。 balanceFactor = height(left subtree) - height(right subtree) 不是最大不平衡树吗?生成这似乎微不足道(显然不是 AVL 插入)。 【参考方案1】:

基本解决方案

斐波那契树有几个属性可以用来形成一个紧凑的斐波那契树:

    斐波那契树中的每个节点本身就是一棵斐波那契树。 高度为 n 的斐波那契树中的节点数等于 Fn+2 - 1。 节点与其左子节点之间的节点数等于该节点左子节点的右子节点中的节点数。 节点与其右子节点之间的节点数等于该节点右子节点的左子节点中的节点数。

不失一般性,我们假设我们的斐波那契树具有以下附加属性:

    如果一个节点的高度为 n,那么左孩子的高度为 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);
    

封闭式解决方案

如果您查看由基本解决方案生成的每对单词之间的差异,您会发现它们在斐波那契数列的两个连续元素之间交替出现。这种交替模式由Fibonacci word定义:

斐波那契字是二进制数字(或任何两个字母字母表中的符号)的特定序列。斐波那契字是由重复连接形成的,就像斐波那契数是由重复加法形成的一样。

原来有一个closed-form solution for the Fibonacci word:

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;
    

【讨论】:

嗨@godel9,您答案的第一部分可能会导致压缩键范围的正确方向(这很重要),但肯定会增加复杂性。我将不得不找时间更好地分析它。同时:高度为h的斐波那契树的节点nn = F(h+2)-1,其中F(i)是第i个斐波那契数 太棒了!这基本上解决了我算法的密钥传播问题!我冒昧地将第一个斐波那契数 (0) 添加到您的 fibs 向量中,以保持简洁和更容易与理论联系(并修复了一个小的语法错误)。您甚至可以在主循环中写 fib + max_height - 1 以便于解释(编译器会将其取出)。现在,除了分配和填充fibs 到所需大小之外,我会做些什么来适应我的需求和口味,是(a)左右交换(b)从min_key + fibs[max_height + 1] - 1 开始。 ...但是您无需根据我的需要和口味更改代码!我自己并没有在我的解决方案中左右交换,只是为了使输出树更易于阅读(我的解决方案的起始根是min_key + (1 &lt;&lt; (max_height - 1)) - 1 @WalterTross 感谢您做出这些更正。我已将您的建议纳入了将fib + max_height - 1 移动到主循环中。很高兴我能帮助你。 :) 您的“封闭式解决方案”确实非常有趣,尽管最后我个人更喜欢您的解决方案,它直接改进了我的解决方案。除非有人提出了一个不同的解决方案来克服剩余的限制而没有太多的麻烦,否则你会得到奖金。不过,为了将您的答案标记为已接受的答案,我必须要求您重新排列它,以免让第一次阅读的读者感到困惑。【参考方案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));
   

更新

solution by 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 数组):

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 个节点,键值之间没有“洞”,并且有 kmax - kroot = FH+1 - 1. key range可以很方便的定位,必要时可以通过偏移root的key值,算法中可选左右交换.

尚未解决的是使用整数键对任何给定键范围进行紧凑填充(而对于完全平衡的树来说这是微不足道的)。使用这种算法,如果你想用n 个节点(带有整数键),其中 n 不是斐波那契数 - 1,并且您想要尽可能小的键范围,您可以找到可以容纳 n 个节点的第一个高度,然后针对该高度运行算法,但是当n个节点被插入时停止。将保留许多“漏洞”(在最坏的情况下,大约为 n/φ ≅ n/1.618)。

与我写这个解决方案的介绍时的直觉理解相反,这个算法的时间效率,不管有没有 godel9 的重要改进,已经是最优的,因为它是 O(n)(所以当包括插入,它变成 O(n log n))。这是因为操作数与从 TF1 = T1 到 TFH = Tn,即N = Σh=1...H(F h+2 - 1) = FH+4 - H - 1。但是两个连续的斐波那契数的渐近比 φ ≅ 1.618,golden ratio,所以 N/n ≅ φ2 ≅ 2.618。您可以将其与完全平衡的二叉树进行比较,其中适用的公式非常相似,只是“对数”为 2 而不是 φ。

虽然我怀疑摆脱 φ2 因素是否值得,但考虑到当前代码的简单性,注意以下几点仍然很有趣:当您添加“增量”时任何高度为 h 的中间斐波那契树的节点,这个“斐波那契前沿”(我的术语)的两个连续键之间的差异是 FH-h+3 或 FH-h+ 4,以一种特殊的交替模式。如果我们知道这些差异的生成规则,我们可以简单地用两个嵌套循环填充树。

【讨论】:

我认为没有更好的答案,因为,您至少需要该数量的节点(在您的示例中,需要 12 个节点才能获得高度 5)才能获得最大不平衡树,否则它将获得旋转。 我想你想在你的循环中使用fibonacci_subtree(0, height, 1 &lt;&lt; (max_height - 2)) 谢谢@godel9!当我在答案中将深度更改为高度时,错字出现【参考方案3】:

有趣的问题。看起来你已经有了一个很好的解决方案,但我会发现一个更容易组合的方法。

假设:

令 U(n) 表示高度为 n 的最大不平衡 AVL 树中的节点数。

U(0) = 0

U(1) = 1

U(n) = U(n-1) + U(n-2) + 1 for n>=2(即一个根节点加上两个最大不平衡子树)

为方便起见,我们假设 U(n-1) 始终是左子树,U(n-2) 始终是右子树。

让每个节点由一个唯一的字符串表示,该字符串表示从根到节点的路径。根节点为空字符串,一级节点为“L”和“R”,二级节点为“LL”、“LR”、“RL”、“RR”等

结论:

U(n) 中 A 级节点的有效字符串为 A 个字符长并且满足不等式: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

【讨论】:

以上是关于如何生成最大不平衡的 AVL 树的主要内容,如果未能解决你的问题,请参考以下文章

AVL树平衡旋转详解

AVL树

图解数据结构树之AVL树

深入理解平衡二叉树(AVL)

java 此实现使用AVL平衡树。允许树中任何节点的高度为两个子树的最大差异为一。

数据结构14.自平衡二叉查找树_AVL树