我有一个perfect binary tree,它列举了后订购方式。这种树的一个例子是
15
7 14
3 6 10 13
1 2 4 5 8 9 11 12
我知道树的大小。我正在寻找一个公式或一个简单的算法,它将一个数字作为输入(我感兴趣的顶点的ID)并返回一个数字 - 父亲的ID。从顶部遍历树并在O(log n)
中获得结果非常容易。有更快的解决方案吗?我最感兴趣的是叶子,所以如果有特殊情况的解决方案,也可以带上它。
答案 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)最坏的情况时间。它们基于完美二叉树的后序索引的以下属性:
i
。属性#1和#2给出了简单的算法来获取树的某些节点的父节点:对于形式为2 i -1的索引,乘以{ {1}}并添加2
;对于2 i -2形式的索引,只需添加1
。对于其他节点,我们可以重复使用属性#4来到满足属性#1或#2的节点(通过减去几个左子树的大小),然后找到位于最左边的一些父节点路径,然后添加所有先前相减的值。属性#3允许快速找到应减去哪些子树的大小。所以我们有以下算法:
1
和((x+1) & x) != 0
重复第2步。((x+2) & (x+1)) != 0
。积累差异。1
,则乘以((x+1) & x) == 0
并添加2
;否则,如果1
,请添加((x+2) & (x+1)) == 0
。例如,1
(二进制形式12
)在步骤#2转换为0b1100
,然后转换为0b0101
(或小数0b0010
)。累计差异为2
。第3步添加10
,第4步添加1
,结果为10
。
其他示例:13
(二进制形式10
)在步骤#2转换为0b1010
(或十进制的0b0011
)。第3步将其加倍(3
),然后添加6
(1
)。步骤#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
更改为0
的最高有效位决定了高阶/低阶位的分离点。1
和((x+1) & x) != 0
,从步骤#1继续。((x+2) & (x+1)) != 0
和((x+1) & x) != 0
,也将此处理为正确的孩子。((x+2) & (x+1)) != 0
,则乘以((x+1) & x) == 0
并添加2
;否则,如果1
,请添加((x+2) & (x+1)) == 0
。如果满足步骤#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的其他查询),则x
是x + 1
的正确子项。如果x + 1
是一片叶子,则x
是x + 2
的左子。
对于内部节点:x
的孩子是x - 1
(右孩子)和x - (1 << height(x) - 1)
(左孩子,右孩子是完美的二叉树,所以它有2 h -1个节点)。因此,使用x
和left_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