堆排序(C++) – 虽快,但非主业
堆排序是一个O(nlog(n))级别的算法,因为堆的特性,决定其的级别,好歹是个完全二叉树,怎么滴都带着二分的特性,O(nlog(n))顺理成章了;实现堆排序的算法有很多,这里列举了三种,都是用最大堆来实现的,会用到 最大堆实现
前两种堆排序都用到了上面的最大堆实现中的最大堆类,可点击上面的链接查看,第三种堆排序实现比较特别;然而堆排序是O(nlog(n))级别的算法,但是为什么说非主业呢,看下面的PK赛就直到了~~
堆排序 – 反向复制
反向复制玩法思路是将数组中的元素逐个插入到最大堆中,因为最大堆的性质,所有元素插入后形成了一个最大堆,然后逐个元素再从最大堆中弹出,因为弹出的是这个序列的最大值,所以反向复制到原数组中,逐个弹出,原数组就是从小到大的序列了;因为要创建另一个数组作为堆,反向复制玩法的空间复杂度是O(n),实际复杂度,因为每次插入元素和获取元素都需要维护,维护的时间复杂度是O(log(n)),数据量是n个,所以整体时间复杂度是O(n(log(n))
// 堆排序,玩法一,先将元素逐个插入最大堆,然后再弹出并反向复制回到原数组,就是从小到大有序的数组了
template <typename T>
void heapSortCopyStyle(T arr[], int n) {
// 这个是在栈中创建对象,用new的话是在堆中创建
MaxHeap<T> h = MaxHeap<T>(n);
for(int i;i<n;i++) {
// 往最大堆中插入数组元素,边插入,最大堆就边排好了
h.insert(arr[i]);
}
// 将元素逐个从堆中取出,因为取出的是最大值,所以反向复制给数组
for(int i=n-1;i>=0;i--) {
arr[i] = h.popTheTop();
}
}
堆排序 – 逐级最大堆化
逐级最大堆化的思路:所有叶节点都是一个最大堆,如果叶节点在k层,往上一级k-1层进行最大堆变换,那么k-1层往下都是最大堆了,再往上层走,直到k=1,整个堆就是最大堆了,之所以逐层往上的过程中只需要比较当前节点的两个子节点,这两个子节点肯定是其各自分支中最大的这个排序的具体实现在最大堆实现中第二个构造方法中,这里主要给出接口~~
// 堆排序,玩法二,自下向上逐级最大堆化,这个思路称为heapify
template <typename T>
void heapSortHeapify(T arr[], int n) {
// 调用这个构造方法中,已经用heapify方法进行了最大堆的设置
MaxHeap<T> h = MaxHeap<T>(arr,n);
// 倒序赋值
for(int i=n-1;i>=0;i--) {
arr[i] = h.popTheTop();
}
}
堆排序 – 原地变有序
前面的两种堆排序都是O(n)的空间复杂度,而且数据进行了两次复制,既耗空间又耗时,直接在原数组进行省去了两次复制,虽然多了n次heapify和n次数组中的元素赋值,效率还是高一些,详细的在下面的PK赛;这个玩法的思路是先用heapify方法将数组变成一个最大堆,然后将最大值放到最后一个位置,然后剩下的位置进行最大堆维护,然后再放到最后一个位置的前一个位置,以此类推就是从小到大了的有序序列了~~
// 玩法三的元素向下维护最大堆的方法,上面两种方法用到的堆类中的popDown一样,
// 只是这里因为不用额外空间,直接在原数组中进行,注意这里第一个元素是从下标0开始,堆实现中是从下标1开始的,这里实现了向下维护的方法
void goSpinDown(int arr[], int n, int p) {
// 父节点
int flag = p;
// 考虑有左右子节点的情况
while(flag*2+1<n) {
if(arr[flag*2] > arr[flag*2+1]) {
flag = flag*2;
} else {
flag = flag*2+1;
}
if(arr[p] < arr[flag]) {
swap(arr[p],arr[flag]);
p = flag;
// 如果元素比其左右子节点都大则结束循环
} else {
break;
}
}
// 考虑只有左节点的情况,因为堆必定为完全二叉树,所以可能出现最后的元素为父节点的左子节点,没有右子节点
if(flag*2<n) {
if(arr[p] < arr[flag*2]) {
swap(arr[p],arr[flag*2]);
}
}
}
// 堆排序,玩法三,不用额外空间的玩法,玩法一和玩法二都要O(n)的空间复杂度,玩法三是O(1)
template <typename T>
void heapSortSelfPlay(T arr[], int n) {
// 因为不用额外空间,所以heapify直接在原数组中进行
// 从(n-1)/2开始逐个堆化,(n-1)/2就是为了获取上一级,但是因为这里还要进行赋值
// 因为堆中的元素下标是从0开始的,所以这里要用(n-1)
for(int i=(n-1)/2;i>=0;i--) {
goSpinDown(arr,n,i);
}
// 逐个将最大值放到当前最大堆最后面,然后将剩下的元素维护最大堆,遍历结束数组就是从小到大的有序序列了
for(int i=n-1;i>0;i--) {
swap(arr[0],arr[i]);
goSpinDown(arr,i,0);
}
}
堆排序性能PK赛
比赛规则:
- 对1000万个数据进行排序;
- 归并排序和快速排序为本次PK赛的特约参赛队员;
- 三场比赛,分别是随机序列、基本有序序列、大量元素相同序列;
- 基本有序序列的未有序数据数量是200个;
- 从上到下是归并排序、快速排序、堆排序-- 反向复制、堆排序-- 逐级最大堆化、堆排序-- 原地变有序;
随机数序列:
mergeSortTopDown:1.537
quickSortDoubleWays:1.32
heapSortCopyStyle:4.555
heapSortHeapify:4.448
heapSortSelfPlay:4.307
基本有序序列:
mergeSortTopDown:0.034
quickSortDoubleWays:6.883
heapSortCopyStyle:3.886
heapSortHeapify:2.499
heapSortSelfPlay:2.422
基本相同序列:
mergeSortTopDown:1.037
quickSortDoubleWays:0.902
heapSortCopyStyle:2.418
heapSortHeapify:2.577
heapSortSelfPlay:2.27
比赛结果:
- 从时间上可以看出,堆排序和归并排序,快速排序是同一数量级的排序算法,都是O(nlog(n))级别;
- 堆排序的速度明显要比归并排序和快速排序慢一些,这也是为什么说堆排序虽快,但非主业;
- 堆排序在面对不同的序列类型时用时都差不多,在这个上面表现还算稳定;
- 原地变有序的玩法虽然速度提升不大,还是有所提升的,是三种堆排序算法中最快的;