算法导论 第六章 堆排序
进入排序专题了。
这次我们要讲的是堆排序,而说到堆排序,肯定就免不了要了解一下堆这个数据结构((我们这里说的都是二叉堆)。
什么是堆呢?
堆就是一个完全二叉树,而完全二叉树。。。就是除了最后一层可以不满外(也可以满),其他层都是满的,并且最后一层的叶子节点都位于最左端的二叉树。
上图就是一个标准的完全二叉树,红色的是其叶子节点,可以看到最后一层的节点都位于左端,并且除了最后一层都是满的。
我们所要介绍的堆排序就是基于这样一个数据结构。
而堆根据父节点和子节点的关系,又分为大根堆和小根堆,大根堆就是指任意父节点大于两个子节点的堆,如下:
小根堆可以堆就是任意父节点小于子节点的堆,如下:
为了方便的实现堆排序,我们可以先写一个优先队列priority_queue。
其原型如下:
#include <cstdio> #define INF 999999999 using namespace std; class priority_queue { private: // 队列用数组存1~n,0可作为交换区间 int *a; public: // 构造函数 priority_queue(); // 析构函数 ~priority_queue(); // 队列长度 int length; // 比较函数(确定升序或降序) bool cmp(const int& a,const int& b); // 建堆 void buildHeap(); // 重构子树 void heapify(int k); // 更新某个节点的值并调整其父节点 bool update(int k,int v); // 入队 void push(int v); // 出队 bool pop(); // 取队头 int top(); // 取左节点下标 int left(int k); // 取右节点下标 int right(int k); // 取父节点下标 int parent(int k); // 交换下标为i和j的两个节点的值 void exchange(const int& i,const int& j); };
然后我们首先来看构造函数
priority_queue::priority_queue() { a = new int[100]; length=0; } priority_queue::~priority_queue() { delete []a; }
这个数组的大小可以根据需要来调整。
然后看5个附带的基本操作:
bool priority_queue::cmp(const int &a,const int &b){return a > b;} int priority_queue::left(int k){return k<<1;} int priority_queue::right(int k){return (k<<1)|1;} int priority_queue::parent(int k){return k>>1;} void priority_queue::exchange(const int &i,const int &j){a[0]=a[i];a[i]=a[j];a[j]=a[0];}
再就是重构和建堆还有更新操作:
void priority_queue::heapify(int k) { int t; int l=left(k); int r=right(k); while(l<=length) { if(cmp(a[l],a[k]))t=l; else t=k; if(r<=length&&cmp(a[r],a[t]))t=r; if(t!=k) { exchange(k,t); k=t; l=left(k); r=right(k); } else break; } } void priority_queue::buildHeap() { for(int i=length/2;i>=1;i--)heapify(i); } bool priority_queue::update(int k,int v) { if(cmp(v,a[k])==false)return false; a[k]=v; while(k>1&&cmp(a[k],a[parent(k)])) { exchange(k,parent(k)); k = parent(k); } return true; }
最后是入队,出队和取队头:
void priority_queue::push(int v) { length++; a[length]=(cmp(1,2)?INF:-INF); update(length,v); } bool priority_queue::pop() { if(length<1)return false; exchange(1,length); length--; heapify(1); return true; } int priority_queue::top(){return a[1];}
就是这样一个基本的优先队列就完成了。
其中建堆操作其实也可以通过不断的push实现。
然后我们实现堆排序就比较简单了。
代码如下:
int main() { priority_queue a; int s[] = {15,13,9,5,12,8,7,4,0,6,2,1}; for(int i=0;i<12;i++)a.push(s[i]); int l = 12;do { l--; s[l]=a.top(); } while(a.pop()); for(int i=0;i<12;i++)printf("%d ",s[i]); printf("\n"); return 0; }
其实就是将数组push进优先队列中,然后再pop回去,就得到了一个排序过后的数组。
第一步中每次push都是O(lgn),所以最坏的复杂度是O(nlgn),第二部中同样。
话说buildHeap的复杂度貌似是O(n)来着,不过也没事了,反正最终结果整个堆排序的复杂度是O(nlgn)。