为什么在由数组实现的堆中,索引0未被使用?

xji*_*xji 30 algorithm heap

我正在学习数据结构,每个源都告诉我在实现堆时不要使用数组的索引0,而不给出任何解释原因.我在网上搜索,搜索了StackExchange,但找不到答案.

Jim*_*hel 58

没有理由为什么在数组中实现的堆必须使索引0处的项不被使用.如果将根设置为0,则项目at array[index]的子项位于array[index*2+1]array[index*2+2].节点at array[child]的父节点位于array[(child-1)/2].

让我们来看看.

                  root at 0       root at 1
Left child        index*2 + 1     index*2
Right child       index*2 + 2     index*2 + 1
Parent            (index-1)/2     index/2
Run Code Online (Sandbox Code Playgroud)

因此,将根设置为0而不是1会花费额外的添加来查找左子项,并使用额外的减法来查找父项.

我看不到那些额外的指令在运行时间上有很大的不同.

我之所以认为从一个基于0的数组的语言开始1是错误的,请参阅/sf/answers/3486429341/和我的博客文章但这就是我们一直以来的方式!

  • @Juan:你确定吗?我正在寻找`priority_queue`的C ++ STL代码,它基于0。我不知道您认为“最重要的地方”是什么,但是我记得Java和Python堆实现也是基于0的。在实践中,我看到的唯一基于1的堆位于大学生项目中,并且很少有人滚动自己的堆而不使用提供的库。 (2认同)

小智 19

正如我在CLRS书中发现的那样,在性能方面有一些意义,因为一般来说,班次操作员的工作速度非常快.

在大多数计算机上,LEFT过程可以2*i通过简单地 i左边的二进制表示移位一位位置来计算一条指令.类似地,RIGHT过程可以通过将 i 的二进制表示移位一位位置然后加1作为低位来快速计算2*i+1.PARENT过程可以i/2通过向右移位一位位置来计算.

因此,在索引1处启动堆可能会更快地计算父,左和右子索引.

  • 这对于过去 20 年制造的任何 CPU 来说都无关紧要。访问任何元素的时间比添加要长数百倍,如果是缓存未命中,则需要数千倍。此外,由于添加是无条件发生的,因此它永远不会停止管道。至于做移位而不是除法,这可能很有用,因为它释放了执行单元,但任何值得考虑的编译器都知道 `/2` 可以被移位替换,并且如果你编写 `i/2` 就会为你做这件事 (2认同)

xji*_*xji 6

(在搜索过程中,我想出了自己的答案,但我不知道它是否正确。)

如果索引0用于根节点,则对其子节点的后续计算将无法进行,因为我们有indexOfLeftChild = indexOfParent * 2indexOfRightChild = indexOfParent * 2 + 1。然而0 * 2 = 00 * 2 + 1 = 1,并不能代表我们想要的亲子关系。因此,我们必须从1让数组表示的树符合我们想要的数学属性开始。

  • 我们**不必**从 1 开始,因为没有什么强制我们按原样使用这些方程,但从 0 开始会在方程中添加一些“-1”和“+1”。 (9认同)
  • @Dukeling OK,所以按照数学上(概念上)的定义,堆应该有一个索引为“1”的根(整个结构从1开始)。我们可能会选择用 array[0] 来实现这个根,但如果是这样,我们就必须做一些`+1`、`-1`,这会有点烦人。所以通常我们从 array[1] 开始。我的这个解释正确吗? (3认同)

Dav*_*tat 5

正如 AnonJ 所观察到的,这是一个品味问题,而不是技术必要性。从 1 而不是 0 开始的一件好事是,在二进制字符串 x 和正整数之间存在双射,将二进制字符串 x 映射到以二进制写成 1x 的正整数。字符串 x 给出了从根到索引节点的路径,其中 0 表示“取左孩子”,1 表示“取右孩子”。

另一个考虑是,否则未使用的“第零”位置可以保存一个值减去无穷大的哨兵,在没有分支预测的架构上,这可能意味着运行时间的不可忽视的改进,因为在向上循环中只有一个测试而不是二。