堆排序

  • 堆:堆是完全二叉树。和其他的树一样,用数组表示堆,保留索引为0的位置(这个位置可以用来保存堆中元素的数),对于一个下标为i的节点,它的左子节点的下标为2×i,右子节点是2×i+1。
  • 堆的叶子节点是从(n/2)+1处开始的。这一点可以这样证明:假设一个堆有n个节点, 设下标为m的节点是该堆的第一个子节点,那么一定有2×m>n,且m节点之前的节点,一定不是叶子节点,那么就有(m-1)×2<=n,也就是说:
    (n/2)+1 >= m > n/2,
    由于m是自然数,所以m只可以取值(n/2)+1。
  • 根据堆的性质,定义如下两个宏来计算堆的父子节点:
    #define PARENT(_Child)	((_Child) / 2)
    #define LEFT(_Parent)	(2 * (_Parent))
    #define RIGHT(_Parent)	(2 * (_Parent) + 1)
    

    为了保持大根堆的性质,定义一个函数,用来使某个节点满足该性质:

    void heapify(int* arr, int pos)
    {
    	const int size = arr[0];
    	while(pos <= size / 2)
    	{
    		int left = LEFT(pos);
    		int right = RIGHT(pos);
    		int largest = pos;
    
    		if(left <= size && arr[left] > arr[largest])
    			largest = left;
    		
    		if(right <= size && arr[right] > arr[largest])
    			largest = right;
    
    		if(largest == pos)
    			break;
    		std::swap(arr[largest], arr[pos]), pos = largest;
    	}
    }
    

    递归的版本可能更明确:

    void recursively_heapify(int* arr, int pos)
    {
    	const int size = arr[0];
    	int left = LEFT(pos);
    	int right = RIGHT(pos);
    	int largest = pos;
    
    	if(left <= size && arr[left] > arr[largest])
    		largest = left;
    
    	if(right <= size && arr[right] > arr[largest])
    		largest = right;
    
    	if(largest != pos)
    	{
    		std::swap(arr[pos], arr[largest]);
    		recursively_heapify(arr, largest);
    	}
    }
    

    函数的参数pos指向一个父节点,比较它以及它的两个子节点,使三者满足大根堆的性质。


    下面的make_heap在有了heapify之后就变得很简单了:

    void make_heap(int *arr)
    {
    	int size = arr[0];
    	for(int i = size / 2; i > 0; --i)
    	{
    		heapify(arr, i);
    	}
    }
    

    它接受一个数组,这个数组的第0个索引标识数组的长度。函数从size/2开始迭代,是因为size/2之后的都是叶子节点,肯定满足堆的性质。对非叶子节点调用heapify,循环结束,堆就形成了。


    接下来就是sort_heap,由于堆的第1个元素是最大的元素,排序只需要每次都把这个最大元素放在最后,并“减小”堆的大小,直到所有的堆元素都排序完成:

    void sort_heap(int *arr)
    {
    	int& size = arr[0];
    	const int csize = size;
    	for(; size > 0; )
    	{
    		std::swap(arr[1], arr[size]);
    		--size;
    		heapify(arr, 1);
    	}
    	arr[0] = csize;
    }
    

    pop_heap弹出并返回堆的最大元素:

    int pop_heap(int* arr)
    {
    	int result = heap_get_max(arr);
    
    	arr[1] = arr[arr[0]];
    	--arr[0];
    
    	heapify(arr, 1);
    	return result;
    }
    

    get_max返回最大的元素:

    int heap_get_max(int* arr)
    {
    	return arr[1];
    }
    

    increase_heap增大对应节点上的值:

    void increase_heap(int* arr, int pos, int val)
    {
    	assert(arr[pos] <= val);
    	arr[pos] = val;
    	int parent = arr[pos / 2];
    	while(pos > 1 && arr[pos] > parent)
    	{
    		int posParent = pos / 2;
    		std::swap(arr[pos], arr[posParent]);
    		pos = posParent;
    	}
    }
    

    注意,在增大了某个元素之后,这个元素可能违反了堆的性质,所以在循环中比较它的父元素,直到它小于父元素(已经满足堆的性质)或者到根元素为止。


    接下来就是插入元素:

    void insert_heap(int* arr, int val)
    {
    	++arr[0];
    	increase_heap(arr, arr[0], val);
    }
    

    先增大堆的大小,然后在堆的最后一个位置,增大这个位置的值到指定的值。

    make_heap的时间复杂度是O(lg(n)),其递归式为T(N)<=T(2N/3)+O(1),其中,T(2N/3)是因为:

    所以,每次的迭代,最多把序列划分为原来的2/3。根据主方法,可以证明它是O(lg(n))。

    make_heap的复杂度是O(n),而不是O(nlg(n)),堆的高度是,下面要证明的是,在任意高度h上,至多有个结点


    那么,make_heap的时间复杂度就可以表示为:

    其中,O(h)为heapify作用在高度为h的节点上的时间。

    上式中的可以写为:,这是一个几何级数:

    heapify中:

    这样就很明确了,这也是一个几何级数,而且对应上面求导后的几何级数,带入x=1/2就是这个级数了。所以,这个级数的解为(1/2)/((1-(1/2))^2),为2。所以make_heap的复杂度为O(2n),也就是O(n)。

    sort_heap的过程是make_heap+heapify×(for loop),n+lgn×n = O(n)。

posted @ 2012-08-28 09:16  Gallagher  阅读(247)  评论(0)    收藏  举报