SGI STL:heap排序的adjust函数

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(min)
node1
y(max)
node2
node3
node4
node5

如图,在构造堆之前,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节点将一直上升,最终形成下面的形式:

y(max)
node1
x(min)
node2
node3
node4
node5

即我们讨论的两个节点将交换位置。接下来,进入__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的高度是用来保证充分比较的必要手段。

欢迎各方批评。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值