学习数据结构的时候就学习过堆,不过忘了很多了,《编程珠玑》里也有这一章,因此重新总结一下。
堆的两个性格:(1)顺序性质:任何结点的值都小于或等于子结点的值。(对于最小堆而言)
(2)形状性质:二叉树结构,最多在两层上具有叶结点,其中最底层的结点尽可能的靠左分布。
堆的实现:考虑以数组实现,对于大小为n的堆,声明数组x[n+1],下标从1开始并浪费x[0]。树种常见的函数定义如下:
root = 1;
value(i) = x[i];
leftchild(i) = 2*i;
rightchild(i) = 2*i+1;
parent(i) = i /2;
null(i) = (i < 1) or (i > n).
堆的两个关键函数:即上滤siftup和下滤siftdown函数,后面操作都要反复调用这两个函数。
siftup函数:当存在heap(1,n-1),考虑在n处插入一个新的元素。新插入的元素可能会破坏堆的性质,如果新插入的元素比它的父节点小,就需要将元素和父结点交换,直到到达合适的位置并成为根的右结点为止。
void siftup(n) { for(int i = n; x[i] < x[i/2] && i > 1; i /= 2) swap(x[i], x[i/2] }
void siftdown(n) { for(int i = 1; (c = i*2) <= n; i = c) { if(c + 1 <=n && x[c+1] < x[c] ++c; if(x[i] <= x[c]) break; swap(x[i], x[c]; } }
有了数据结构,开始考虑对堆进行的一些操作了。
构建堆:一般的算法是将N个关键字以任意顺序放入树中,保持结构特性。考虑将结点上滤可以完成一颗具有堆序的树。
//上滤建堆 for(int i = 2; i <= n; ++i) siftup(i);堆的插入操作:把新加入的元素放在堆得末尾,实现上滤就可以了,同时考虑边界。
void insert(t) { if(n >= maxsize) /*report error/ ++n; x[n] = t; siftup[n]; }函数extractmin查找并删除集合中的最小元素,然后重新组织使其具有堆性质。思想是从堆顶取出元素,将堆的最后一个元素赋给堆顶然后下滤。
elementtype extractmin() { if(n < 1) /*report error*/ t = x[1]; x[1] = x[n--]; siftdown(1); return t; }
封装成c++类:
//priqueue.h template<class T> class priqueue{ private: int n, maxsize; T *x; void swap(int i, int j) [T t = x[i]; x[i] = x[j]; x[j] = t;} public: priqueue(int m) { maxsize = m; x = new T[maxsize+1]; n = 0; } void siftup(n) { for(int i = n; x[i] < x[i/2] && i > 1; i /= 2) swap(i, i/2); } void siftdown(n) { for(int i = 1; (c = i*2) <= n; i = c) { if(c + 1 <=n && x[c+1] < x[c] ++c; if(x[i] <= x[c]) break; swap(i, c); } } void insert(t) { if(n >= maxsize) std::cout << "元素已满“ << std::endl; ++n; x[n] = t; siftup[n]; } T extractmin() { if(n < 1) std::cout << "优先队列不存在” << std::endl; T t = x[1]; x[1] = x[n--]; siftdown(1); return t; } };
堆排序:通过建立优先队列然后反复调用extractmin函数可以实现排序,建优先队列和extractmin的时间开销均为O(nlogn);但是优先队列和数组的空间开销分别为n+1和n.利用堆排序可以直接在数组上排序。时间复杂度O(nlogn).
//优先队列排序 template<class T> void pqsort(T v[], int n) { priqueue<T> pq(n); int i; for(i = 0; i < n; ++i) pq.insert(v[i]); for(i = 0; i < n; ++i) v[i] = pq.extractmin(); }
//堆排序 void swap(int *v, int i, int j) { int tmp = v[i]; v[i] = v[j]; v[j] = tmp; } void siftup(int *v, int n) { for(int i = n; i>1 && v[i] > v[i/2]; i /= 2) swap(v, i, i/2); } void siftdown(int *v, int n) { int c; for(int i = 1; (c = 2*i) <= n; i = c) { if(c+1 <= n && v[c] < v[c+1]) ++c; if(v[i] >= v[c]) break; swap(v, i, c); } } //堆排序 void heapsort(int *v, int n) { int i; for(i = 2; i <= n-1; ++i) siftup(v, i); for(i = 0; i < 14; i++) cout << v[i] << " "; cout << endl; for(i = n-1; i > 1; --i) { swap(v, 1, i); siftdown(v, i-1); } }
上面讨论的堆舍弃了数组0位置不用,如果考虑以0位置作为根的话,则左子结点为2*i+1,父结点为(i-1)/2,重新利用堆排序如下:
void swap(int *v, int i, int j) { int tmp = v[i]; v[i] = v[j]; v[j] = tmp; } void siftup(int *v, int n) { int c; for(int i = n-1; i>0 && v[i] > v[c=(i-1)/2]; i = c) swap(v, i, c); } void siftdown(int *v, int n) { int c; for(int i = 0; (c = 2*i+1) <= n-1; i = c) { if(c+1 <= n-1 && v[c] < v[c+1]) ++c; if(v[i] >= v[c]) break; swap(v, i, c); } } //堆排序 void heapsort(int *v, int n) { int i; for(i = 1; i <= n-1; ++i) siftup(v, i+1); for(i = 0; i < n-1; i++) cout << v[i] << " "; cout << endl; for(i = n-1; i > 0; --i) { swap(v, 0, i); siftdown(v, i); } }