我有一个堆(像一个二叉树一样实现:每个节点有两个指向子节点的指针和一个指向父节点的指针)。
给定第k个元素(按BFS顺序),该如何找到它?我认为可以在O(logn)时间内完成。
(我假设“ kth元素(按BFS顺序)”是从输入的从上到下,从左到右扫描的角度表示第k个元素。)
由于您知道二叉堆是完整的二叉树(可能在最后一级除外),因此您知道树的形状是具有一定高度(包含2 k个节点,k个节点)的完美二叉树。节点从左到右填充。当您写出图片中的节点编号并将值一索引时,就会出现这些树的一个真正漂亮的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
请注意,每个层都以一个2的幂的节点开始。因此,假设我们想查找数字13。两个不小于13的最大幂是8,因此我们知道13必须出现在行中
8 9 10 11 12 13 14 15
现在,我们可以使用此知识对从13到树的根的路径进行反向工程。例如,我们知道该行数字的后半部分中有13个,这意味着13个属于根的右子树(如果它属于左子树,那么我们将位于包含8、9、10和11。)这意味着我们可以从根开始,直接舍弃一半的数字以获得
12 13 14 15
我们现在在树中的节点3处。那么我们是左走还是右走?好吧,在这些数字的前半部分中有13,因此我们知道此时需要下降到节点3的左子树中。这将我们带到节点6,现在我们剩下了前半部分。数字:
12 13
13在这些节点的右半部分,所以我们应该下降到右侧,将我们带到节点13。瞧!在那里!
那么这个过程是如何进行的呢?好吧,我们可以使用一个非常非常可爱的技巧。让我们用二进制写出上面相同的树:
0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 ^^^^
我已经指出了节点13的位置。我们的算法以以下方式工作:
让我们考虑一下二进制的含义。查找包含该节点的图层 等同于查找数字中设置的最高有效位 。在具有二进制表示1101的13中,MSB是8位。这意味着我们处于以8开始的层中。
那么,如何确定我们是在左侧子树中还是右侧子树中?好吧,要做到这一点,我们需要查看我们是在本层的上半部还是在下半部。现在,让我来 看看 一个可爱的花招- 看看MSB之后的下一部分 。如果此位设置为0,则我们在范围的前一半,否则,我们在范围的后一半。因此,我们可以通过查看数字的下一位来确定范围的一半!这意味着我们可以通过仅查看数字的下一位来确定要落入哪个子树。
完成此操作后,我们可以重复此过程。在下一级别我们将做什么?好吧,如果下一位为零,我们往左走;如果下一位为一,我们往右走。看看这对13意味着什么:
1101 ^^^ ||| ||+--- Go right at the third node. || |+---- Go left at the second node. | +----- Go right at the first node.
换句话说,我们只需查看MSB之后的数字位,就可以阐明从树的根到所讨论节点的路径!
这是否总是有效!你打赌!让我们尝试数字7。它的二进制表示形式为0111。MSB位于4的位置。使用我们的算法,我们可以这样做:
0111 ^^ || |+--- Go right at the second node. | +---- Go right at the first node.
从原始图片看,这是正确的选择!
这是此算法的一些粗略的C / C ++伪代码:
Node* NthNode(Node* root, int n) { /* Find the largest power of two no greater than n. */ int bitIndex = 0; while (true) { /* See if the next power of two is greater than n. */ if (1 << (bitIndex + 1) > n) break; bitIndex++; } /* Back off the bit index by one. We're going to use this to find the * path down. */ bitIndex--; /* Read off the directions to take from the bits of n. */ for (; bitIndex >= 0; bitIndex--) { int mask = (1 << bitIndex); if (n & mask) root = root->right; else root = root->left; } return root; }
我尚未测试此代码! 用唐·努斯(Don Knuth)的话来说,我刚刚证明了它在做正确的事情。我可能在这里有一个错误的错误。
那么这段代码有多快?好吧,第一个循环运行,直到找到大于n的2的第一个幂,这需要O(log n)时间。循环的下一部分一次通过n个位向后计数,在每一步执行O(1)工作。因此,整个算法总共花费 O(log n)时间 。
希望这可以帮助!