二叉树相关之堆排序与时间复杂度剖析(其二)

1个月前发布 gsjqwyl
11 0 0

文章标题:二叉树相关之堆排序与时间复杂度剖析(其二)

文章内容:

在开始阅读本篇之前,建议先了解上一篇内容:深入理解数据结构第一弹——二叉树(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)

总结

至此,关于堆排序及其时间复杂度的讲解就告一段落。若有理解上的疑问,欢迎在评论区指出或私信交流,也欢迎各位大佬前来探讨!

创作不易,还望各位大佬点赞支持!

© 版权声明

相关文章

没有相关内容!

暂无评论

none
暂无评论...