堆排序
堆排序运行时间(n lgn)。它是一种原地(in place)排序算法:在任何时候,数组中只有常数个元素存储在数组外。堆的数据结构不至少在堆排序中有用,还可以构成一个有效的优先队列。
数据结构
二叉堆数据结构是一种数组对象,它可以被看做是一棵完全二叉树。树中的每个节点与数组中存放该节点值的那个元素对应。除了最后一层,树的每一层都是填满。
假设有数组A。
树的根为A[0]现在给定某个节点的下标i,其父节点parent(i),左孩子left(i),右孩子right(i)的下标可以通过简单的计算得到:
parent(i) = (i-1)/2
left(i) = i*2+1
right(i) = i*2+2
二叉堆有两种:大根堆和小根堆。在这两种堆中,节点内的数值都要满足堆的特性,其细节则根据堆的种类决定。
大根堆指的是除了根节点以外的每个节点,有:
A[parent[i]]>=A[i]
小根堆指的是除了根节点以外的每个节点,有:
A[parent[i]]<=A[i]
此处以大根堆为例:
fixDown过程,其运行时间为O(lg n),是保持大根堆性质的关键。
buildHeap过程,以线性时间运行,可以在无序的输入数据基础上构造处最大堆。
heapSort过程,运行时间为O(n lg n),对一个数组进行原地排序。
push、pop过程,运行时间为O(lg n),可以让堆结构作为优先队列使用。
实现
当fixDown被调用时,我们假定以left(i)和right(i)为根的两棵二叉树都是大根堆,但这时A[i]可能小于其子女,这就违反了大根堆的性质。
fixDown让A[i]在大根堆中“下降”,使得以i为根的子树成为大根堆。
时间复杂度:O(lg n)
void fixDown(int a[],int root,int len)//存放堆的数组,当前正在调整的子树的根,数组允许访问部分的长度
{
int tmp=a[root];
int child=root*2+1;
while(child<len)
{
if(child+1<len&&a[child]<a[child+1])
child++;
if(tmp>=a[child])
break;
a[root]=a[child];
root=child;
child=child*2+1;
}
a[root]=tmp;
}
当buildHeap被调用时,A[0…n-1]将自下而上得变成一个最大堆,子数组A[((n-1)/2+1)…n-1]中的元素都是树中的叶子,因此叶子都可以被认为是只含有一个元素的堆,故建树过程中对每一个其他节点都进行一个fixDown的调用。
时间复杂度:O(n)
buildHeap(int a[],int len)
{
for(int i=len/2;i>=0;--i)//buildHeap
fixDown(a,i,len);
}
当pop被调用时,我们返回了根结点,此时整棵树变成了两棵,这时我们需要将最后一个数据放置到根节点处,再进行一次fixDown
时间复杂度:O(lg n)
int pop(int a[],int len)
{
swap(a[0],a[len-1]);
fixDown(a,0,len-1);
return a[len-1];
}
开始时,堆排序算法先用buildHeap将输入数组A造成了一个大根堆,在调用heapSort时,可以通过将A[0]同A[n-1]交换来达到最终的目的。现在只需要将数组的长度减去1,就可以很容易得将剩下的的A[0…n-2]重构成大根堆。原来跟的子女还是大根堆,但新的根元素可能违背了大根堆的性质。这时只需要调用fixDown 就能保持这一性质了。堆排序算法不断重复这个过程,堆的大小由n变成了1。
算法复杂度:O(n lg n)
void heapSort(int a[],int len)
{
for(int i=len;i>1;++i)
pop(a,i);
}