版权声明:本文为博主原创文章,若需转载,请评论说明,并在文章显著位置标明原文出处。 https://blog.csdn.net/lishang6257/article/details/83047381
堆排序
#include <iostream>
#include <vector>
/*
非降序排序
时间复杂度:o(nlgn)
空间复杂度:o(nlgn)
建堆:o(n)
维护堆:o(lgn)
不稳定
*/
using namespace std;
void swap(int &a,int &b){int c = a;a = b;b = c;}
int Max(const vector<int> &a,int n,int i){
if(2*i + 2 > n) return i;//该节点左孩子不存在
if(2*i + 3 > n) {
//该节点右孩子不存在
if(a[i] >= a[2*i + 1]) return i;
else return 2*i + 1;
}
if(a[i] >= a[2*i + 1] && a[i] >= a[2*i + 2]) return i;
if(a[2*i + 1] >= a[i] && a[2*i + 1] >= a[2*i + 2]) return 2*i + 1;
return 2*i + 2;
}
void heapify(vector<int>&a,int n,int i)//lgn,即堆高度
{
//最大堆
//n 为堆大小
//i 是指下标
int cur = i;
while(true){
int tmp = Max(a,n,cur);
if(tmp == cur) break;
swap(a[cur],a[tmp]);
cur = tmp;
}
}
//建堆过程
void buildHeap(vector<int> &a) //n
{
for(int i = a.size()/2 - 1;i >= 0;i --){
heapify(a,a.size(),i);
}
}
//堆排序过程
void heapSort(vector<int> &a)
{
buildHeap(a);
for(auto c : a) cout << c << " ";
cout << endl;
for(int i = a.size() - 1;i > 0;i --){
swap(a[i],a[0]);
heapify(a,i,0);
for(auto c : a) cout << c << " ";
cout << endl;
}
}
int main()
{
vector<int> a = {4,1,3,2,16,9,10,14,8,7};
heapSort(a);
for(auto c : a) cout << c << " ";
return 0;
}
- 基本概念
- 最大堆:对于任何一个结点的键值总大于孩子结点的键值
- 最小堆:对于任何一个结点的键值总小于孩子节点的键值
- 思维引擎
堆排序顾名思义,利用这种数据结构达到排序的目的。这里主要运用了三个主要操作:建堆,维护堆,堆排序。- 问题描述:对序列 进行非降序排序。
- 建堆:将原序列生成最大(小)堆的过程。
从 到 依次维护以该节点为根节点的子树的最大(小)堆性质。 - 维护堆:
由于仅有根节点破坏了堆的性质,调整方案如下:- 将子树的根节点作为起始节点,
- (以最大堆为例)若 小于孩子结点,则与孩子结点交换,并将 用此孩子更新。
- 重复操作 直至 大于孩子结点,那么此时形成的堆就为最大堆。
- 堆排序:
1. 将原始序列 建成最大堆。
2. 将 与 中的元素交换,那么 保存了序列的最大值,而 则成了仅 不满足最大堆性质的堆,对堆 进行堆维护。
3. 在依次将 与 … 交换,重复操作 ,使得 全部排序。 - 值得注意的是在对维护的过程中,发现以 为根节点的子堆中仅有 不满足最大(小)堆性质
- 时间复杂度
堆排序的时间复杂度是一个十分有趣的问题的,里面的一些数学背景我会在后面一一指出,并给出相关的证明。整体的思路来源于算法导论第三版相应内容。- 维护堆:这里很容易可以观察到维护堆的时间复杂度与子树的高度有关(子树根节点与孩子结点置换时最多只能置换子树高度次,所以推测时间复杂度
,下面是数学证明:
这边我们要指出为什么偏偏是 不是其他的呢,这里我们首先假设最后一层半满,那么我就可以得出最后一层(记为第 层)的节点个数为 ,那么根节点的左子树的的节点总个数就是 ,并且在维护堆的过程中我们只能堆根节点的左子树( 个节点)或者右子树( 个节点)进行操作,显然上述不等式成立。 - 建堆
这个时间复杂度是很坑的,当直接从代码本身去观察,对于 个节点,每一个节点都需要 左右的复杂度,所以是 然而啪啪打脸。
这里的证法和算法导论有些出入,但是方法是没有问题的,有兴趣的笔友可以验证一下。但为什么没有达到 呢?从公式的结果来看,大多数的节点根本没有达到 的复杂度,这里导致缩水。那为什么仅少了 了,这里我推测是 极限造成的。 - 堆排序
这里面没什么花露水,直接外层 ,内层 ,至于为什么没发生上述情况,我没有做详细的证明,但在个人推断中,建堆是从 开始,堆排是 开始,上述的 无法再消掉造成的,证明和建堆证明雷同。 - 堆排序不会因为数据分布造成像快排一样退化,是一个优异的算法,虽然是个不稳定的,但是人家是原址的,在考虑为什么不替代快排中,考虑好后填坑(总感觉挖了很多坑)
- 维护堆:这里很容易可以观察到维护堆的时间复杂度与子树的高度有关(子树根节点与孩子结点置换时最多只能置换子树高度次,所以推测时间复杂度
,下面是数学证明:
- 空间复杂度
堆排序属于原址性排序,不需要额外空间 - 稳定性
是不稳定,原因同快速排序和简单的选择排序。