文章标题:二叉树相关之堆排序与时间复杂度剖析(其二)
文章内容:
在开始阅读本篇之前,建议先了解上一篇内容:深入理解数据结构第一弹——二叉树(1)——堆-CSDN博客
前言:
很多学习数据结构的人可能会遇到这样的情况:明明一开始就学了时间复杂度,但在后续自己编写程序或解答涉及时间复杂度判断的题目时,仍难以准确判断。接下来,我们结合上期所学的堆,深入剖析时间复杂度这一概念,同时进一步理解堆的概念,以便后续应用堆进行排序等操作。
目录
一、堆排序
1、堆排序的大致思路
2、堆排序的实例说明
二、堆排序的时间复杂度
向下排序的时间复杂度
向上排序的时间复杂度
堆排序整体的时间复杂度
总结
一、堆排序
1、堆排序的大致思路
在上一篇中我们已经了解了堆的相关内容,堆分为大堆和小堆两种形式。堆排序正是利用了堆的这一特性。例如,对于数组 {7,8,3,5,1,9,5,4},其在堆中的分布如下:

需要注意的是,降序排序需要借助小堆来实现,升序排序则需借助大堆。比如上述数组,若要得到降序序列,由于堆顶元素是堆中最小的,可将堆顶元素与堆尾元素交换,然后将堆尾元素排除在外继续进行降序排列。

2、堆排序的实例说明
堆排序本质上是对堆的进一步应用,若已理解前一章内容,直接看代码即可。
(除test.c外,其余代码从上一章复用)
Seqlist.h
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int sz;
int capacity;
}HP;
// 初始化
void HeapInit(HP* php);
// 销毁
void HeapDestory(HP* php);
// 插入
void HeapPush(HP* php, HPDataType x);
// 删除
void HeapPop(HP* php);
// 找堆顶元素
HPDataType HeapTop(HP* php);
// 判断是否为空
bool HeapEmpty(HP* php);
// 算个数
int HeapSize(HP* php);
test.c
// 堆排序
void HeapSort(int* a, int n)
{
// 建堆——向下调整建堆O(N-log(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--;
}
}
int main()
{
int a[] = { 7,8,3,5,1,9,5,4 };
HeapSort(a, sizeof(a) / sizeof(int));
return 0;
}
Seqlist.c
// 堆相关操作
// 初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = 0;
php->sz = 0;
}
// 销毁
void HeapDestory(HP* php)
{
free(php->a);
free(php);
}
// 交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 向上调整(小堆)
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 向下调整
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (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 HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->sz == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->sz] = x;
php->sz++;
// 向上调整
AdjustUp(php->a, php->sz - 1);
}
// 删除堆顶元素
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->sz - 1]);
php->sz--;
// 向下调整
AdjustDown(php->a, php->sz, 0);
}
// 判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->sz == 0;
}
// 获取堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
// 获取堆中元素个数
int HeapSize(HP* php)
{
assert(php);
return php->sz;
}
通过实现上述代码,即可实现堆排序。

二、堆排序的时间复杂度
我们知道实现堆时有向上排序和向下排序两种方式。在上述堆排序示例中使用的是向下排序,原因是其时间复杂度更低。接下来分析两种排序的时间复杂度。
向下排序的时间复杂度

向上排序的时间复杂度

堆排序整体的时间复杂度
计算堆排序整体的时间复杂度需综合上述两步的时间复杂度。
第一步:
这一步实际上是多次进行向下调整建堆,其时间复杂度根据渐进表示法可表示为O(N – log(N)),由于当N较大时,log(N)相对于N可忽略,故可表示为O(N)。
第二步:
第二步外循环需执行N次,内循环看似每次都是完整的向下排序,但随着循环次数增加,内部向下排序的时间复杂度逐渐降低,因为堆尾已排好序的元素不再参与后续堆排序。所以这一步的时间复杂度实际为O(N*logN)。
因此,堆排序的整体时间复杂度为O(N + N*logN)
总结
至此,关于堆排序及其时间复杂度的讲解就告一段落。若有理解上的疑问,欢迎在评论区指出或私信交流,也欢迎各位大佬前来探讨!
创作不易,还望各位大佬点赞支持!