SGI STL:heap排序中adjust函数
本文不介绍具体的heap排序算法,阅读之前请先熟悉STL heap排序的逻辑。本文讨论的是SGI-STL源码中__adjust_heap
函数中的一个细节。先展示源码。
// 实际执行的push函数:
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__push_heap(_RandomAccessIterator __first,
_Distance __holeIndex, _Distance __topIndex, _Tp __value)
{
_Distance __parent = (__holeIndex - 1) / 2;
// 上升至最高高度,并且将hole的移动限定在top以下
while (__holeIndex > __topIndex && *(__first + __parent) < __value) {
*(__first + __holeIndex) = *(__first + __parent);
__holeIndex = __parent;
__parent = (__holeIndex - 1) / 2;
}
*(__first + __holeIndex) = __value;
}
// 实际执行的adjust函数:
template <class _RandomAccessIterator, class _Distance, class _Tp>
void
__adjust_heap(_RandomAccessIterator __first, _Distance __holeIndex,
_Distance __len, _Tp __value)
{
_Distance __topIndex = __holeIndex;
_Distance __secondChild = 2 * __holeIndex + 2;
// 下移至叶节点深度
// 整个过程保证树结构不被破坏
while (__secondChild < __len) {
// 取子节点间大者
if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))
__secondChild--;
*(__first + __holeIndex) = *(__first + __secondChild);
__holeIndex = __secondChild;
__secondChild = 2 * (__secondChild + 1);
}
// 调整特殊情况:树尺寸为奇数而holeIndex被移到了最小的非叶节点
if (__secondChild == __len) {
*(__first + __holeIndex) = *(__first + (__secondChild - 1));
__holeIndex = __secondChild - 1;
}
__push_heap(__first, __holeIndex, __topIndex, __value);
}
这就是heap排序中的两个底层函数,前者(push)的效果是令节点上升至适当位置,后者(adjust)的效果是令节点下溯至最深深度,然后调用push令其上升。那么这里就存在一个问题:
// push函数的标签
void
__push_heap(_RandomAccessIterator __first,
_Distance __holeIndex, _Distance __topIndex,/// 允许上升到的最高高度
_Tp __value)
// 其发挥作用的位置
while (__holeIndex > __topIndex && *(__first + __parent) < __value) {/*..*/}
再看adjust函数对push函数的调用:
_Distance __topIndex = __holeIndex;
// ...
// 中间没有使用到__topIndex
// ...
__push_heap(__first, __holeIndex, __topIndex, __value);
换言之,对于传入adjust函数的参数,即需要调整的节点,adjust函数会原封不动地将其作为__topIndex
传给push函数。翻译成自然语言就是:对于一个节点,首先会将其坠落至最深深度,然后再将其上升至适当位置(对于这里就是上升到可上升到的最高高度),但是最终这个位置不能大于其原本的位置。
那么这里的这个限制就会显得很不自然:既然是上升到可上升的最大位置,那么为什么还要加一层限制,即用其原本的位置作为__topIndex
参数传入push?倘若我是这棵树中值最大的节点,而用adjust函数调整时,下降至最深处之后我不能一次升至根节点,而最高只能升至原来的节点位置。这是不是画蛇添足?
我们来考虑一个特殊情况。这是一个尚未被构造成堆的数据结构,如果以树的形式展示出来如下:
如图,在构造堆之前,x是整个结构中最小的元素,处于根节点位置;y是整个结构中最大的元素,处于最后一个非叶节点的位置,即make_heap
第一次调整的节点。现在利用其构造一个堆:
template <class _RandomAccessIterator, class _Tp, class _Distance>
void
__make_heap(_RandomAccessIterator __first,
_RandomAccessIterator __last, _Tp*, _Distance*)
{
if (__last - __first < 2) return;
_Distance __len = __last - __first;
// 找到最后一个枝节点
_Distance __parent = (__len - 2) / 2;/// 图中的y节点
while (true) {
__adjust_heap(__first, __parent, __len, _Tp(*(__first + __parent)));
if (__parent == 0) return;
__parent--;
}
}
我们来模拟第一次循环:y节点作为__parent
参数被传入adjust函数,在adjust函数中,其首先被下移至最深深度,然后使用push上升。现在我们假设push不使用__topIndex
的限制:
__push_heap(__first, __holeIndex, 0, __value);// 不做最高限度限制
显然,这样y节点将一直上升,最终形成下面的形式:
即我们讨论的两个节点将交换位置。接下来,进入__make_heap
的下一次循环,其目标相对当前x的位置前移——即图中的node1
。显然,node1
是x的兄弟节点,不可能干扰到x的位置。因此在不考虑node1, node2,……的具体值的情况下,第二次调整之后,整个树仍然是上图的形状,即y仍然在y的位置,x仍然在x的位置。
第三次循环也就是最后一次循环,是针对根节点(y)的调整。y作为最大值,如果将y下降至最深,再上升,一定最终会回到根节点。而push和adjust算法保证一个节点的坠落和上升都不会干扰其余节点的相对关系,因此最后一轮循环结束之后整棵树仍然是上图的形状。可以看到,这时x作为最小值,却是两个叶节点的父节点,这违反了完全二叉树的特性,但这个时候__make_heap
函数已经结束了。
究其缘由,只是x节点生来就是一个非叶节点,却没有走过__adjust_heap
函数的流程,即没有经历下溯
→
\to
→上升的过程,就像被忽视了一样。而罪魁祸首就是第一次y节点的上升没有限制高度,导致x节点移入了正在adjust的节点,最终被无视。显然,这个模型和具体是不是根节点等等细节无关,只要形如这个模型,就会有节点被忽视。因此,限制__push_heap
的高度是用来保证充分比较的必要手段。
欢迎各方批评。