首先,我收到了 Mark Weiss 的回复,他的电子邮件基本上说代码是正确的(完整回复在这个答案的底部)。
他还这样说:
因此,最小项位于数组索引 1 中,如 findMin 所示。要进行插入,请遵循从底部到根的路径。
索引 1?嗯...然后我不得不返回并重新阅读本章的大部分内容,当我看到图 6.3 时,它点击了。
该数组是从 0 开始的,但被认为是堆的一部分的元素从索引 1 开始存储。图 6.3 如下所示:
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| | A | B | C | D | E | F | G | H | I | J | | | |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7 8 9 10 11 12 13
将值放置在元素 0 是使循环终止的标记值。
因此,通过上面的树,让我们看看插入函数是如何工作的。H
下面标记了这个洞。
首先,我们x
放入第 0 个元素(堆外),并将孔放置在数组中的下一个可用元素处。
H
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| x | A | B | C | D | E | F | G | H | I | J | | | |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7 8 9 10 11 12 13
然后我们冒泡(渗透)孔,将值从“索引的一半”向上移动,直到找到放置x
.
如果我们看一下图 6.5 和 6.6,让我们将实际值放入数组中:
H/2 H
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| 14 | 13 | 21 | 16 | 24 | 31 | 19 | 68 | 65 | 26 | 32 | | | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
0 1 2 3 4 5 6 7 8 9 10 11 12 13
请注意,我们将要插入的值 14 放入索引 0 中,但这在堆之外,这是我们确保循环终止的标记值。
然后我们将该值x
与 处的值进行比较hole / 2
,现在是 11/2 = 5。x 小于 31,因此我们将值向上移动并移动孔:
H/2 H <---------------------------
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| 14 | 13 | 21 | 16 | 24 | 31 | 19 | 68 | 65 | 26 | 32 | 31 | | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
0 1 2 3 4 5 6 7 8 9 10 11 12 13
| ^
+--------- move 31 -----------+
我们再次比较,14 又小于 21 (5 / 2 = 2),所以再一次:
H/2 H <------------
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| 14 | 13 | 21 | 16 | 24 | 21 | 19 | 68 | 65 | 26 | 32 | 31 | | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
0 1 2 3 4 5 6 7 8 9 10 11 12 13
| ^
+-- move 21 ---+
然而,现在 14 不小于 13 (hole / 2 --> 2 / 1 = 1),所以我们找到了正确的位置x
:
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| 14 | 13 | 14 | 16 | 24 | 21 | 19 | 68 | 65 | 26 | 32 | 31 | | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
0 1 2 3 4 5 6 7 8 9 10 11 12 13
^
x
如您所见,如果您查看插图 6.6 和 6.7,这符合预期的行为。
因此,虽然代码没有错,但您遇到了一个可能超出本书范围的小问题。
如果x
被插入的类型是引用类型,您将在当前堆中对刚刚插入的同一个对象有 2 个引用。如果您随后立即从堆中删除该对象,它看起来(但看起来首先让我们看到了......)第 0 个元素仍将保留引用,从而禁止垃圾收集器完成其工作。
为了确保这里没有隐藏的议程,这是马克的完整答案:
嗨,拉斯,
代码是正确的。
二叉堆是一棵完全二叉树,其中在从底部到根的任何路径上,值都不会增加。因此,最小项目位于根部。数组表示将根放在索引 1 处,对于索引 i 处的任何节点,父节点位于 i/2(向下舍入)(左孩子位于 2i,右孩子位于 2i+1,但这不是必需的这里)。
因此,最小项位于数组索引 1 中,如 findMin 所示。要进行插入,请遵循从底部到根的路径。
在 for 循环中:
hole /= 2 表示将孔移动到父级的想法。
x.compareTo(array[hole / 2]) < 0 表示只要 x 小于父级,我们就会留在循环中。
问题是,如果 x 是一个新的最小值,您将永远无法安全地退出循环(从技术上讲,您在尝试比较 x 和 array[0] 时会崩溃)。您可以进行额外的测试来处理角落案例。或者,代码通过在开始时将 x 放入数组 [0] 来解决此问题,并且由于节点 i 的“父”是 i/2,因此可以在索引中找到位于索引 1 中的根的“父” 0. 这保证了如果 x 是新的最小值,则循环终止(然后将 x 放置在索引 1 处的根中,这是新的最小值)。
书中有更长的解释......但这里的基本概念是使用哨兵(或虚拟)值来避免边界情况的额外代码。
问候,
马克·韦斯