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

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

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

一、堆排序的概念

堆排序(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)

本篇完。

目录
相关文章
|
1月前
|
移动开发 算法 前端开发
前端算法之堆排序
前端算法之堆排序
19 1
|
1天前
|
算法 Python
数据结构算法--4堆排序
堆排序过程概述:建立大根堆,将堆顶最大元素移出并替换为末尾元素,调整保持堆性质,重复此过程直至堆为空,实现排序。时间复杂度为O(nlogn)。Python中可用heapq模块进行堆操作。
|
2天前
|
搜索推荐 算法 Java
Java中的快速排序、归并排序和堆排序是常见的排序算法。
【6月更文挑战第21天】Java中的快速排序、归并排序和堆排序是常见的排序算法。快速排序采用分治,以基准元素划分数组并递归排序;归并排序同样分治,先分割再合并有序子数组;堆排序通过构建堆来排序,保持堆性质并交换堆顶元素。每种算法各有优劣:快排平均高效,最坏O(n²);归并稳定O(n log n)但需额外空间;堆排序O(n log n)且原地排序,但不稳定。
14 3
|
1月前
|
算法
[数据结构]——二叉树——堆排序
[数据结构]——二叉树——堆排序
|
1月前
|
存储 搜索推荐 算法
[数据结构]————排序总结——插入排序(直接排序和希尔排序)—选择排序(选择排序和堆排序)-交换排序(冒泡排序和快速排序)—归并排序(归并排序)
[数据结构]————排序总结——插入排序(直接排序和希尔排序)—选择排序(选择排序和堆排序)-交换排序(冒泡排序和快速排序)—归并排序(归并排序)
|
16天前
|
存储 算法 C语言
数据结构和算法——堆排序(选择排序、思路图解、代码、时间复杂度、堆排序及代码)
数据结构和算法——堆排序(选择排序、思路图解、代码、时间复杂度、堆排序及代码)
13 0
|
17天前
|
测试技术
深入理解数据结构第二弹——二叉树(2)——堆排序及其时间复杂度
深入理解数据结构第二弹——二叉树(2)——堆排序及其时间复杂度
16 0
|
1月前
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
25 4
|
1月前
|
人工智能 搜索推荐 算法
sort-04-heap sort 堆排序算法详解
这是一个关于排序算法的系列文章摘要,包括了10篇关于不同排序算法的链接,如冒泡排序、快速排序、选择排序、堆排序等。堆排序是一种基于比较的排序算法,利用了近似完全二叉树的结构并满足最大堆或最小堆的性质。最大堆中,每个节点的值都大于或等于其子节点。文章详细解释了最大堆的概念、节点访问方式以及堆的操作,包括堆调整和堆排序的过程,并通过图解展示了堆排序的具体步骤。此外,还提供了一个Java实现的堆排序代码示例。整个排序系列来源于一个开源项目,读者可以通过链接查看完整内容。
sort-04-heap sort 堆排序算法详解
|
1月前
|
存储 机器学习/深度学习 算法
初阶数据结构之---堆的应用(堆排序和topk问题)
初阶数据结构之---堆的应用(堆排序和topk问题)

热门文章

最新文章