堆排序
- 堆:堆是完全二叉树。和其他的树一样,用数组表示堆,保留索引为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)。