目录
一、堆排序
1.思路
在之前我们了解到了堆的特性,大堆可以将一组数据中最大的数调整到堆顶,小堆可以将一组数据中最小的数调整到堆顶,于是我们可以利用这个性质来对数组排序。
在进行堆排序之前我们要先建堆,建堆有两种方法,向上调整和向下调整,二者的时间复杂度我们会在第三部分分析,分别为
在之前出堆的实现中,我们不可以直接将推定元素去除,后节点整体前移。我们还是将堆顶元素与堆末节点交换位置,找一个变量n,来记录要操作的节点数,每次交换后n--,这样就可以将排序好的数放在末尾。然后将堆顶元素向下调整,重复此过程。
所以升序用大堆,降序用小堆。
2.代码实现
void HeapSort(int* a, int n){
for (int i = (n - 1 - 1) / 2; i >= 0; --i){
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0){
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
二、top-k问题
top-k是一个经典问题,在一堆数据中,筛选出最大的十个(k个)数据,应该怎么做?
1.朴素思路
首先想到的是将所有数据建大堆,然后 HeapPop k次,就可以获得最大的k个数据。
但是这种方法不仅时间复杂度高,而且空间复杂度奇高无比。
2.思路
我们可以建含有k个节点的小堆,每次将一个数据与堆顶数据比较,若小于堆顶数据,则不变,若大于堆顶数据,则替换堆顶数据,然后向下调整,大的数据就沉到堆底。
就像如下的液体分层一样,密度大的才可以沉下去。
3.代码实现
为了模拟topk问题,我们创建一个txt文件包含用时间戳创建的随机数据。
void CreateNDate(){
// 造数据
int n = 100000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL){
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i){
int x = (rand() + i) % 10000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
top-k的主体
void PrintTopK(const char* file, int k){
FILE* fout = fopen(file, "r");
if (fout == NULL){
perror("fopen error");
return;
}
// 建一个k个数小堆
int* minheap = (int*)malloc(sizeof(int) * k);
if (minheap == NULL){
perror("malloc error");
return;
}
// 读取前k个,建小堆
for (int i = 0; i < k; i++){
fscanf(fout, "%d", &minheap[i]);
AdjustUp(minheap, i);
}
int x = 0;
while (fscanf(fout, "%d", &x) != EOF){
if (x > minheap[0]){
minheap[0] = x;
AdjustDown(minheap, k, 0);
} //核心code
}
for (int i = 0; i < k; i++){
printf("%d ", minheap[i]);
}
printf("\n");
free(minheap);
fclose(fout);
}
三、建堆时间复杂度
谈起建堆,有两种建堆方式,向上调整建堆和向下调整建堆
具体的代码实现在我之前的博客里有,我现在只专注于求取两种方法的时间复杂度。
通过了解建堆的原理,我们可以得到这样的基本公式
1.向下调整建堆
我们从堆顶节点开始,有 个节点,其要移动 层
依次的,第二层有 个节点,要移动 层
……
倒数第二层有 个节点,要移动 层
于是有最坏估计下总时间
用高中的数列求和知识,错位相减法可以求出
而 得到
有
由时间复杂度的计算,我们知道后面的对数可以忽略
于是有向下调整建堆的时间复杂度是
2.向上调整建堆
我们从堆底节点开始,有 个节点,其要移动 层
依次的,倒数第二层有 个节点,要移动 层
……
倒数第二层有 个节点,要移动 层
于是有最坏估计下总时间
仍然错位相减可以有
于是
可以得到向上调整建堆的时间复杂度是