Heap Sort-堆排序
Algorithm:
堆排序就是利用堆(假设是大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素的次大值。如此反复执行,便能得到一个有序序列了。
Ex:大顶堆定义(所有结点都大于等于其孩子)
重复上面的过程,就可以排好序了。
在程序中,需要 解决两个问题
1、如何构造大顶堆
2、堆顶元素和最后一个元素交换后,如何调整剩下的元素成为一个新的堆。
Code:
void Heap_Adjust(vector<int> &v, int i,int len) //len为调整的长度
{
int lchild = 2 * i + 1; //左孩子节点
int rchild = 2 * i + 2; //右孩子节点
int temp = i;
int key = v[i];
if (i < (len / 2)) //不是叶节点
{
if (lchild < len&&v[lchild] > v[temp]) //必须是v[temp]
temp = lchild;
if (rchild < len&&v[rchild] > v[temp])
temp = rchild;
if (temp != i)
{
v[i] = v[temp];
v[temp] = key;
Heap_Adjust(v, temp,len); //避免调整后temp节点不是堆
}
}
}
void Heap_Sort(vector<int> &v)
{
//Build Heap
for (int i = v.size() / 2-1; i >= 0; i--)
Heap_Adjust(v, i,v.size());
//调整
for (int k = v.size() - 1; k >= 0; k--)
{
int key = v[0];
v[0] = v[k];
v[k] = key;
Heap_Adjust(v, 0,k);
}
}
void Adjust(vector<int> &A,int i,int n){
int l=2*i+1;
int r=2*i+2;
while(l<n || r<n){
int cur=i;
if(l<n && A[l]<A[cur])
cur=l;
if(r<n && A[r]<A[cur])
cur=r;
if(cur != i){
swap(A[cur],A[i]);
l=cur*2+1;
r=cur*2+2;
i=cur;
}
else
break;
}
}
Analysis:
它的运行时间主要是消耗在初始构建堆和重建堆时的反复筛选上。
在构建堆的过程中,因为我们是完全二叉树从最下层右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次去堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根节点的距离为(log2i)+1),并且需要去n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
所以总体来说,堆排序的时间复杂度为O(nlogn)。由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn)。这在性能上显然远远好于冒泡、简单选择、直接插入的O(n^2)的时间复杂度。
空间复杂度上,它只有一个用来交换的暂存单元,也非常不错。不过由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
另外,由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
2017/5/13更新
如果排序几乎有序,可以使用堆排序,(因为归并排序和快速排序都不依赖数组的初始顺序)
Ex1:已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。
可以建立一个大小为k的小根堆(0~k-1),然后把堆顶元素取出,再对(1~k)调整,重复过程可以得到。
Ex2:请设计一个高效算法,判断数组中是否有重复值。必须保证额外空间复杂度为O(1)。
需要使用非递归的堆排序,(因为快速排序,归并排序的空间复杂度都不是常数级别的)