在完美的二叉树中获取顶点的父级

时间:2013-12-05 16:27:45

标签: algorithm data-structures tree binary-tree postorder

我有一个perfect binary tree,它列举了后订购方式。这种树的一个例​​子是

                         15
                 7               14
             3       6       10      13
           1   2   4   5    8  9   11  12

我知道树的大小。我正在寻找一个公式或一个简单的算法,它将一个数字作为输入(我感兴趣的顶点的ID)并返回一个数字 - 父亲的ID。从顶部遍历树并在O(log n)中获得结果非常容易。有更快的解决方案吗?我最感兴趣的是叶子,所以如果有特殊情况的解决方案,也可以带上它。

7 个答案:

答案 0 :(得分:15)

可以在O(log * n)时间和O(1)空间中找到父索引。

此处 log * n 表示iterated logarithm:在结果小于或等于1之前必须迭代应用对数函数的次数。

实际上,如果我们能够为大型查找表(为树中的每个节点存储父索引)提供O(n)空间,那么可以在O(1)时间内更快地完成 -

下面我将描绘几个不需要任何额外空间的算法,并给出O(log n)最坏情况时间,O(log log n)预期时间,O(log log n)最坏情况时间的结果和O(log * n)最坏的情况时间。它们基于完美二叉树的后序索引的以下属性:

  1. 树最左边路径上的所有索引都等于2 i -1。
  2. 最左边路径上节点的每个右子的索引等于2 i -2。
  3. 最左侧路径及其右侧子树上的任何节点都标有在同一位置上具有最高有效非零位的索引:i
  4. 最左边路径上任何节点的左子树包含2个 i -1个节点。 (这意味着减去2 i -1后我们得到的节点相对于其父节点的位置相似,具有相同的深度,但更接近"特殊&# 34;节点满足属性#1和#2)。
  5. 属性#1和#2给出了简单的算法来获取树的某些节点的父节点:对于形式为2 i -1的索引,乘以{ {1}}并添加2;对于2 i -2形式的索引,只需添加1。对于其他节点,我们可以重复使用属性#4来到满足属性#1或#2的节点(通过减去几个左子树的大小),然后找到位于最左边的一些父节点路径,然后添加所有先前相减的值。属性#3允许快速找到应减去哪些子树的大小。所以我们有以下算法:

    1. 1((x+1) & x) != 0重复第2步。
    2. 清除最重要的非零位并添加((x+2) & (x+1)) != 0。积累差异。
    3. 如果1,则乘以((x+1) & x) == 0并添加2;否则,如果1,请添加((x+2) & (x+1)) == 0
    4. 添加在步骤2中累积的所有差异。
    5. 例如,1(二进制形式12)在步骤#2转换为0b1100,然后转换为0b0101(或小数0b0010 )。累计差异为2。第3步添加10,第4步添加1,结果为10

      其他示例:13(二进制形式10)在步骤#2转换为0b1010(或十进制的0b0011)。第3步将其加倍(3),然后添加61)。步骤#4添加了累积差异(7),因此结果为7

      时间复杂度为O(log n) - 但仅在所有基本操作在O(1)时间内执行时。

      为了提高时间复杂度,我们可以并行执行步骤#2的多次迭代。我们可以得到索引的14高位,并计算它们的人口数。如果在将结果添加到剩余的低位后,总和不会溢出到这些高位,我们可以递归地应用这种方法,其中O(log log n)复杂度。如果我们有溢出,我们可以回滚到原始算法(逐位)。请注意,所有设置的低位都应该被视为溢出。因此产生的复杂性是O(log log n)预期时间

      我们可以使用二进制搜索来处理溢出,而不是逐位回滚。我们可以确定要选择多少个高位(小于n/2),这样我们要么没有溢出,要么(对于索引n/2)所选的非零高位数位正好是0b00101111111111。请注意,我们不需要多次应用此二进制搜索过程,因为当数字中的位数大于O(log log n)时,不会发生第二次溢出。因此产生的复杂性是O(log log n)最坏情况时间。假设所有基本操作都在O(1)时间内执行。如果在O(log log n)时间内实现某些操作(填充计数,前导零计数),那么我们的时间复杂度将增加到O(log 2 log n)。

      我们可以使用不同的策略,而不是将索引的位分成两个相等的集合:

      1. 计算索引的人口数,并将其添加到索引值。从1更改为0的最高有效位决定了高阶/低阶位的分离点。
      2. 计算高阶位的总体数,然后将结果添加到低阶位。
      3. 如果"分离"位为非零且1((x+1) & x) != 0,从步骤#1继续。
      4. 如果设置了所有低位,并且设置了最低有效位,则将此极限情况作为右子进行处理。
      5. 如果((x+2) & (x+1)) != 0((x+1) & x) != 0,也将此处理为正确的孩子。
      6. 如果((x+2) & (x+1)) != 0,则乘以((x+1) & x) == 0并添加2;否则,如果1,请添加((x+2) & (x+1)) == 0
      7. 如果满足步骤#3的条件,这意味着在步骤#2中的添加导致进入"分离"位。其他低位代表一些不能大于原始人口数的数字。此数字中的设置位数不能大于原始值的总体计数的对数。这意味着每次迭代后的设置位数最多为先前迭代中设置位数的对数。因此,最坏情况时间复杂度为O(log * n)。这非常接近O(1)。例如,对于32位数字,我们需要大约2次或更少的迭代。

        该算法的每一步都应该是显而易见的,除了步骤#5,要证明其正确性。请注意,只有在将人口计数结果添加到"分离"位,但只增加高位的总体数不会导致此进位。 "分离" bit对应于值2 i 。所有比特的总体数和仅高阶比特的总体数之间的差异最多为1。因此,步骤#5处理至少2 i -i的值。让我们将逐位算法应用于此值。 2 i -i足够大,因此设置了位i。清除此位并将i-1添加到位1中的值。该值至少为2 i-1 - (i-1),因为我们只减去了2 i-1 并添加了0..i-2。或者,如果我们将索引向右移动一个位置,我们再次至少有2个 i -i。如果我们重复这个过程,我们总会在位置1找到非零位。每个步骤逐渐减小位i-1中的值与0..i-1的最近幂之间的差异。当这个差异到达2时,我们可以停止这种逐位算法,因为当前节点显然是一个正确的子节点。由于这种逐位算法总是得到相同的结果,我们可以跳过它并始终将当前节点作为正确的孩子处理。

        这是该算法的C ++实现。可以在ideone上找到更多详细信息和其他一些算法。

        2

        如果我们只需要为 leaf 节点查找父节点,则可以简化此算法和代码。 (Ideone)。

        uint32_t getMSBmask(const uint32_t x)
        { return 1 << getMSBindex(x); }
        
        uint32_t notSimpleCase(const uint32_t x)
        { return ((x+1) & x) && ((x+2) & (x+1)); }
        
        uint32_t parent(const uint32_t node)
        {
            uint32_t x = node;
            uint32_t bit = x;
        
            while ((x & bit) && notSimpleCase(x))
            {
                const uint32_t y = x + popcnt(x);
                bit = getMSBmask(y & ~x);
                const uint32_t mask = (bit << 1) - 1;
                const uint32_t z = (x & mask) + popcnt(x & ~mask);
        
                if (z == mask && (x & (bit << 1)))
                    return node + 1;
        
                x = z;
            }
        
            if (notSimpleCase(x))
                return node + 1;
            else
                return node + 1 + (((x+1) & x)? 0: x);
        }
        

答案 1 :(得分:4)

让我们来看看你的树:

                     15
             7               14
         3       6       10      13
       1   2   4   5    8  9   11  12

将标签 n 重写为 15-n 。然后我们得到:

                     0
             8               1
         12      9        5       2
       14  13  11  10   7   6   4   3 

也可以写成

                     0
             +8              +1
         +4      +1      +4      +1
       +2  +1  +2  +1  +2  +1  +2   +1 

嗯,有一种模式适合你。因此,在这个标签计划中,留守儿童比他们的父母2^(i+1)多,其中i是孩子的身高,而正确的孩子比他们的父母高1。我们怎样才能计算出孩子的身高,以及这是一个左或右孩子?

不幸的是,在没有计算出节点的整个路径的情况下,我无法以任何方式获取此信息,这意味着对数时间。但是,您可以直接从节点的标签推导出节点的路径(此处演示了高度为3的树):

  • 假设我们有一个标签n
  • 如果n == 0,那就是根。
  • 否则:
    • 如果n - 8 >= 0,则它位于根的左子树中。设置n = n-8
    • 如果n - 8 < 0,则它位于根的右侧子树中。设置n = n-1
  • 现在n == 0,它是最后一步中发现的任何子树的根。
  • 否则:
    • 如果n - 4 >= 0,则在最后一步中发现的任何子树的左子树中。设置n = n-4
    • 如果n - 4 < 0,则在最后一步中发现的任何子树的右子树中。设置n = n-1
  • 等等,直到你开始测试n-1 >= 0

你可以使用位算术和-1完成所有这些操作,并且在现实世界中它的速度非常快(在万亿节点树中计算它只需要比10节点长约12倍树(忽略内存问题))但它仍然是对数时间。

无论如何,一旦你知道标签的高度以及它是左或右孩子,你可以使用我前面提到的关系轻松地计算父母的标签。

答案 2 :(得分:4)

function getParent(node, size)
{
    var rank = size, index = size;

    while (rank > 0) {
        var leftIndex = index - (rank + 1)/2;
        var rightIndex = index - 1;

        if (node == leftIndex || node == rightIndex) {
            return index;
        }

        index = (node < leftIndex ? leftIndex : rightIndex);
        rank  = (rank - 1)/2;
    }
}

它从根开始,决定进入哪个分支,并重复直到找到该节点。排名是同一级别上最左侧节点的索引:1, 3, 7, 15, ..., k^2 - k + 1

输入参数为:

  • node - 您想要父节点的节点的索引。 (从1开始)
  • size - 根节点的索引;在你的例子中15

示例:

>>> r = []; for (var i = 1; i <= 15; i++) r.push(parent(i,15)); r;
[3, 3, 7, 6, 6, 7, 15, 10, 10, 14, 13, 13, 14, 15, undefined]

答案 3 :(得分:1)

如果您被允许查询节点子节点的ID,您可以做一些有用的事情。

琐碎的案例1:如果是x = size,那就是根。

琐碎案例2:如果x是一个叶子(要查找子ID的查询),请尝试x + 1。如果x + 1不是叶子(子ID的其他查询),则xx + 1的正确子项。如果x + 1 是一片叶子,则xx + 2的左子。

对于内部节点:x的孩子是x - 1(右孩子)和x - (1 << height(x) - 1)(左孩子,右孩子是完美的二叉树,所以它有2 h -1个节点)。因此,使用xleft_child(x)之间的差异,可以确定x的高度:height(x) =ctz(x - left_child(x)),但它实际上是该子树的大小&# 39;这是必需的,所以你无论如何都要1 << height,所以可以删除ctz
因此,x的父级是x + 1(iff right_child(x + 1) == x)或x的父级是
x + (x - left_child(x)) * 2(否则)。

这不仅仅是对ID进行数学计算,但假设您被允许在恒定时间内请求节点的子节点,这是一个恒定时间算法。

答案 4 :(得分:0)

对于非答案答案感到抱歉,但我认为不可能在O(log n)以内完成,甚至允许使用恒定时间算术和按位逻辑运算。

节点索引中的每个位可能对从节点到根的遍历中的几乎每个左/右/停止决策都有影响,包括第一个。此外,检查树的每个级别的节点的索引,它们是非周期性的(并且不仅仅是2的幂)。这意味着(我认为)恒定数量的算术运算不足以识别水平,或节点是左或右孩子。

然而,这是一个令人着迷的问题,我很想被证明是错的。我只是浏览了Hacker's Delight的副本 - 我对他们玩的一些奇特的数字基础寄予厚望,但没有什么比较适合。

答案 5 :(得分:0)

import math
def answer(h,q=[]):
    ans=[]
    for x in q:
        if(True):
            curHeight=h;
            num=int(math.pow(2,h)-1)
            if(x==num):
                ans.append(-1)
            else:
                numBE=int(math.pow(2,curHeight)-2)
                numL=int(num-int(numBE/2)-1)
                numR=int(num-1)
                flag=0
                while(x!=numR and x!=numL and flag<10):
                    flag=flag+1
                    if(x>numL):
                        num=numR
                    else:
                        num=numL
                    curHeight=curHeight-1
                    numBE=int(math.pow(2,curHeight)-2)
                    numL=num-(numBE/2)-1
                    numR=num-1
                ans.append(num)
    return ans

答案 6 :(得分:-1)

当你说'那是枚举后订购方式'时你的意思是你有树的索引表示吗?我的意思是,内部数据结构是一个数组还是类似的?

此外,您是否拥有您对其父级感兴趣的元素的索引?

如果两个问题的答案都是'是':假设子元素索引是k,则父元素索引由

给出
(k - 1) / 2