数据结构与算法⑫(第四章_中_续一)堆排序(详解)

本篇讲讲八大排序之一的:堆排序 概念复习:

数据结构与算法⑩(第四章_上)树和二叉树和堆的概念及结构_GR C的博客-CSDN博客

数据结构与算法⑪(第四章_中)堆的分步构建_GR C的博客-CSDN博客

一、堆排序的概念

堆排序(Heapsort):利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。通过堆来进行选择数据,需要注意的是 排升序要建大堆,排降序建小堆。(易混淆)

排升/降序思路就是建大/小堆,然后把最后一个元素和第一个元素互换,然后把新的最后的一个元素不看作堆内的数据:size-- ; 再向下调整,重复这样,效率就高了。

时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定

二、堆排序的实现

假设我们要对下列数组来使用堆排序:

int arr[ ] = {70, 56, 30, 25, 15, 10, 75};

根据我们之前学到的知识,数组是可以直接看作为完全二叉树的,所以我们可以把它化为堆。此时我们就可以 "选数" (堆排序本质上是一种选择排序)。

第一步:构建堆

第一步就是要想办法把 arr 数组构建成堆(这里我们先排降序构建成小堆)。

构建小堆可以用两种方法,分别为向上调整算法和向下调整算法:

我们这里用向下调整算法,因为等下排序堆的时候也会用到

向下调整算法我们在堆那个章节也学过了,这里我们再来复习一下:

void justDown(int arr[], int sz, int father_idx)
{
    int child_idx = father_idx * 2 + 1; // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) // 最坏情況:调到叶子(child_idx >= 数组范围时必然已经调到叶子)
    {
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] < arr[child_idx]))
        {   // 如果右孩子存在且右孩子比左孩子小
            child_idx = child_idx + 1;// 让其代表右孩子
        }
        if (arr[child_idx] < arr[father_idx])//如果孩子的值小于父亲的值(不符合小堆的性质)
        {                                
            Swap(&arr[child_idx], &arr[father_idx]);
            father_idx = child_idx;          // 更新下标往下走
            child_idx = father_idx * 2 + 1;  // 计算出该节点路线的新父亲
        }
        else // 如果孩子的值大于父亲的值(符合小堆的性质)
        {             
            break;                       
        }
    }
}

向下调整算法有一个前提:左右子树必须同为大堆或小堆

如果左子树和右子树不是同一个堆,怎么办?

可以用递归解决,但是我们能用循环就用循环来解决:

叶子所在的子树是不需要调的。所以,我们从倒着走的第一个非叶子结点的子树开始调,然后--。

//先创建小堆
void HeapSort(int arr[], int sz) 
{
    int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点(sz-1)的父亲
    while (father >= 0)
    {
        AdjustDown(arr, sz, father);
        father--;
    }
}

测试堆是否创建好了:

#include <stdio.h>

void Swap(int* px, int* py) 
{
    int tmp = *px;
    *px = *py;
    *py = tmp;
}

void justDown(int arr[], int sz, int father_idx)
{
    int child_idx = father_idx * 2 + 1; // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) // 最坏情況:调到叶子(child_idx >= 数组范围时必然已经调到叶子)
    {
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] < arr[child_idx]))
        {   // 如果右孩子存在且右孩子比左孩子小
            child_idx = child_idx + 1;// 让其代表右孩子
        }
        if (arr[child_idx] < arr[father_idx])//如果孩子的值小于父亲的值(不符合小堆的性质)
        {                                
            Swap(&arr[child_idx], &arr[father_idx]);
            father_idx = child_idx;          // 更新下标往下走
            child_idx = father_idx * 2 + 1;  // 计算出该节点路线的新父亲
        }
        else // 如果孩子的值大于父亲的值(符合小堆的性质)
        {             
            break;                       
        }
    }
}

//先创建小堆
void HeapSort(int arr[], int sz) 
{
    int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
    while (father >= 0)
    {
        justDown(arr, sz, father);
        father--;
    }
}

void PrintArray(int arr[], int sz) 
{
    for (int i = 0; i < sz; i++) 
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = { 70, 56, 30, 25, 15, 10, 75};
    int sz = sizeof(arr) / sizeof(arr[0]);

    printf("建堆前: ");
    PrintArray(arr, sz);

    HeapSort(arr, sz);

    printf("建堆后: ");
    PrintArray(arr, sz);

    return 0;
}

运行结果:10 15 30 25 56 70 75

第二步:排序

现在堆已经构建完毕了,我们可以开始设计排序部分的算法了。

如果是排升序,建小堆的话:

① 选出最小的数,放到第一个位置,这很简单,直接取顶部就可以得到最小的数。

② 但问题来了,如何选出次小的数呢?

如果是大堆的话就很麻烦,

排降序,建小堆,(排升序的话就要建大堆,大于小于号换就行,后面有完整代码)

我们要排降序,我们可以让原来小堆的堆顶数和最后的数进行交换

10

15 30

25 56 70 75

变成

75

15 30

25 56 70 10

这并不会带来堆结构的破坏!我们把10不看作堆的一部分即可。

(这时75的左右子树都是小堆)

再进行向下调整,就可以找到次小的数了(15)再重复上面操作。此时 时间复杂度为O(N*logN)

这样我们完整的降序HeapSort代码就是

(不过这里建议直接看升序的代码看看能不能理解,我们后面讲的八大排序只讲升序)

//堆排序 - 降序
void HeapSort(int arr[], int sz) 
{
    //创建小堆,选出最小的数  O(N)
    int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点(sz-1)的父亲
    while (father >= 0) 
    {
        justDown(arr, sz, father);
        father--;
    }

    //交换后调堆  O(N * logN)
    int end = sz - 1;
    while (end > 0) 
    {
        Swap(&arr[0], &arr[end]);
        justDown(arr, end, 0);
        end--;
    }
}

降序排序完整代码:

#include <stdio.h>

void Swap(int* px, int* py)
{
    int tmp = *px;
    *px = *py;
    *py = tmp;
}

void justDown(int arr[], int sz, int father_idx)
{
    int child_idx = father_idx * 2 + 1; // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) // 最坏情況:调到叶子(child_idx >= 数组范围时必然已经调到叶子)
    {
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] < arr[child_idx]))
        {   // 如果右孩子存在且右孩子比左孩子小
            child_idx = child_idx + 1;// 让其代表右孩子
        }
        if (arr[child_idx] < arr[father_idx])//如果孩子的值小于父亲的值(不符合小堆的性质)
        {
            Swap(&arr[child_idx], &arr[father_idx]);
            father_idx = child_idx;          // 更新下标往下走
            child_idx = father_idx * 2 + 1;  // 计算出该节点路线的新父亲
        }
        else // 如果孩子的值大于父亲的值(符合小堆的性质)
        {
            break;
        }
    }
}

//完整堆排序_降序
void HeapSort(int arr[], int sz)
{
    //创建小堆,选出最小的数,时间:O(N)
    int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
    while (father >= 0)
    {
        justDown(arr, sz, father);
        father--;
    }

    //交换后调堆  时间:O(N * logN)
    int end = sz - 1;
    while (end > 0)
    {
        Swap(&arr[0], &arr[end]);
        justDown(arr, end, 0);
        end--;
    }
}

void PrintArray(int arr[], int sz)
{
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = { 70, 56, 30, 25, 15, 10, 75 };
    int sz = sizeof(arr) / sizeof(arr[0]);

    printf("排序前: ");
    PrintArray(arr, sz);

    HeapSort(arr, sz);

    printf("排序后: ");
    PrintArray(arr, sz);

    return 0;
}

升序排序完整代码:

(上面的justDown改几个大于号小于号就行)

HeapSort函数是一样的,逻辑不一样而已

#include <stdio.h>

void Swap(int* px, int* py)
{
    int tmp = *px;
    *px = *py;
    *py = tmp;
}

void justDown(int arr[], int sz, int father_idx)
{
    int child_idx = father_idx * 2 + 1; // 计算出左孩子的值(默认认为左孩子大)
    while (child_idx < sz) // 最坏情況:调到叶子(child_idx >= 数组范围时必然已经调到叶子)
    {
        if ((child_idx + 1 < sz) && (arr[child_idx + 1] > arr[child_idx]))
        {   // 如果右孩子存在且右孩子比左孩子大
            child_idx = child_idx + 1;// 让其代表右孩子
        }
        if (arr[child_idx] > arr[father_idx])//如果孩子的值大于父亲的值(不符合大堆的性质)
        {
            Swap(&arr[child_idx], &arr[father_idx]);
            father_idx = child_idx;          // 更新下标往下走
            child_idx = father_idx * 2 + 1;  // 计算出该节点路线的新父亲
        }
        else // 如果孩子的值小于父亲的值(符合大堆的性质)
        {
            break;
        }
    }
}

//完整堆排序_升序
void HeapSort(int arr[], int sz)
{
    //创建大堆,选出最大的数,时间:O(N)
    int father = ((sz - 1) - 1) / 2;  // 计算出最后一个叶子节点的父亲
    while (father >= 0)
    {
        justDown(arr, sz, father);
        father--;
    }

    //交换后调堆  时间:O(N * logN)
    int end = sz - 1;
    while (end > 0)
    {
        Swap(&arr[0], &arr[end]);
        justDown(arr, end, 0);
        end--;
    }
}

void PrintArray(int arr[], int sz)
{
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = { 70, 56, 30, 25, 15, 10, 75 };
    int sz = sizeof(arr) / sizeof(arr[0]);

    printf("排序前: ");
    PrintArray(arr, sz);

    HeapSort(arr, sz);

    printf("排序后: ");
    PrintArray(arr, sz);

    return 0;
}

三、证明建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树。此处为了简化,将采用满二叉树来证明。

(时间复杂度本来看的就是近似值,所以多几个节点不会影响最终结果):

但交换后调堆的时间复杂度是 O(N * logN),所以堆排序的时间复杂度是 O(N * logN)

本篇完。(附下篇链接)

下一篇:利用堆解决Topk问题+堆课后选择题

(穿越回来复习顺便贴个链接:)

数据结构与算法⑬(第四章_中_续二)堆解决Topk问题+堆的概念选择题_GR C的博客-CSDN博客

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值