一.堆排序
关于排序我们还是挺熟悉的,像是冒泡排序,快速排序等等。这里多介绍一种也挺牛的排序,叫做堆排序。在我的上一篇博客里我们了解到了关于堆的概念,堆又分为大堆和小堆。那么如果我们在进行堆排序的过程中,我们要如何选择大堆和小堆?
如果是升序,我们就用建大堆的方式,反之降序,建小堆。
堆排序的话我们用向下调整就可以完成全部的步骤了。
1.代码演示
直接就先把向下调整法放在这里。
void AdjustDown(HPDataType* a, int n, int parent)
{
// 先假设左孩子小
int child = parent * 2 + 1;
while (child < n) // child >= n说明孩子不存在,调整到叶子了
{
// 找出小的那个孩子
if (child + 1 < n && a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
有了向下调整的思路,我们就可以来写函数了。
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;
}
}
这里面有两个问题需要解释一下,一个是为什么要降序建小堆,升序建大堆。还有一个是为什么上面代码for循环里的i等于(n-1-1)/2.
2.降序建小堆,升序建大堆
其实降序建小堆而不采用降序建大堆是为了更加省事一点。我来举个例子:
这个我们看起来好像很简单的就把最大的找出来了,但是9的两个孩子节点是没有大小关系的,我们在排序完9之后,后面的节点的关系就全乱了,兄弟变成了父亲。我们必须要重新排序。
然而我们降序建小堆的话就不会出现这样的情况,我们会把根和最后一个节点调换位置,然后再进行向下调整的过程。这个跟堆的删除类似:
3.从最后一个节点的父亲节点开始调整
之后的就是什么说的为什么上面代码for循环里的i等于(n-1-1)/2。
其实这句话的意思就是从第一个非叶子节点开始向下调整。比如上图中的,如果要调整的话,我们就会从30这个数的位置开始调整。n代表堆里有多少个数据,n-1就是最后一个节点的下标,n-1-1除以2就是最后一个节点的父亲节点,然后在进行向下调整。
二.堆排序的时间复杂度
我们知道,关于一个堆,我们可以向上调整和向下调整,那么哪一种调整的方式更加好呢?
1.向下调整的时间复杂度
这里面需要用到高中的一些关于等比数列的知识。
因为树的高度为h,我们可以算出全部的节点个数n是多少,第一层是2^0,第二层是2^1,后面依次到最后一层2^(h-1)。等比数列相加最后得到的结果就是上图中的2^h-1。
时间复杂度也就是O(n).
2.向上调整的时间复杂度
很多人可能会认为向上调整的时间复杂度和向下调整的时间复杂度一样,实际上差别也是很大的。
对比一下两种调整方式就有很大的差别。列出来的等式也与上面的有很大差别:
这个依旧是通过等比数列的错位相减法来求的。计算方式与上面的一样。
T(N)=-N + (N+1)*(log(N+1)-1)+1 。
最后它的时间复杂度也就是O(N*logN)。
三.topk问题
topk问题就是说在N个数了找前k个大的数字。例如王者荣耀里的国服排名,就是典型的topk问题。知道了topk表达的是啥意思我们就可以想起简单的代码来实现这个问题。
比如直接将这N个数建立一个大的堆,最后逐个取堆顶元素。这个取堆顶元素其实就是上面我们所说的降序建小堆,升序建大堆,只不过少了一些循环而已。时间复杂度在建堆时是O(N),pop时是O(K*logN)。但是这种方法有一种致命缺陷,比如我们中国有十四亿人,我想要找出最富有的10个人,那么我们在建完这个堆时需要的内存将近4G。
对于大量的数据,如果我们取使用这种直接建堆的方式,是不是就不太合适了。那么还有一种方法,我们取前k个数建个小堆。堆顶的元素是这k个数中最小的数,每一次就跟下一个元素比较一下,如果比堆顶元素大,就把他替换掉,然后就向下调整就行了。
我们就在十万个数里找最大的前十个数。
下面是代码演示:
void CreatData()
{
int n = 100000;//创建十万个数
srand((unsigned int)time(NULL));//用时间函数创建变化的种子
FILE* fin = fopen("data.txt", "w");//以写的形式打开文件
if (fin == NULL)
{
perror("fin fopen");
return;
}
for (int i = 0; i < n; i++)
{
int random = rand() + i;
fprintf(fin, "%d\n", random);//往文件里写数
}
fclose(fin);//关闭文件
}
void Heaptopk()
{
int k;
printf("请输入k:\n");
scanf_s("%d", &k);
int* kminheap = (int*)malloc(sizeof(int) * k);//有多少个数就创建多少空间
if (kminheap == NULL)
{
perror("malloc fail");
return;
}
FILE* fout = fopen("data.txt", "r");//以读的形式打开文件
for (int i = 0; i < k; i++)
{
fscanf_s(fout, "%d", &kminheap[i]);//把文件里的前k个数写进我们创建的那个数组
}
//建k个数的小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
//读取剩下的数
int x = 0;
while (fscanf_s(fout, "%d", &x) > 0)//成功读取到了数字进入循环
{
if (x > kminheap[0])
{
Swap(&x, &kminheap[0]);
AdjustDown(kminheap, k, 0);
}
}
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
int main()
{
CreatData();
Heaptopk();
return 0;
}
关于向下调整的方式大家可以看我的上一篇博客。
到这里本篇差不多就结束了,后面也会持续更新关于树的相关知识,感谢观看,如有错误还请多多指出。