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

news2025/1/11 10:00:28

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

比特数据结构与算法(第四章_上)树和二叉树和堆的概念及结构_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;                       
        }
    }
}

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

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

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

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

对30向下调整后变成了 :

70

56 75

25 15 10 30

再--对56向下调整,堆没变

再--对70向下调整,成功变成小堆:

75

56 70

25 15 10 30

//先创建小堆
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

第二步:排序

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

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

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

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

在我们上面的小堆加上几位数:

关系乱了还要重新建堆,这样时间复杂度是O(N^2),太慢了

所以我们上面想的是排降序,建小堆,(排升序的话就要建大堆,大于小于号换就行,后面有完整代码)

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

75

56 70

25 15 10 30

变成

30

56 70

25 15 10 75

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

再进行向下调整,就可以找到次大的数了。此时 时间复杂度为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;
}

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

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

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

本篇完。

下一篇:利用堆解决Topk问题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/373894.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【博学谷学习记录】超强总结,用心分享 | 架构师 Tomcat源码学习总结

文章目录TomcatTomcat功能需求分析Tomcat两个非常重要的功能&#xff08;身份&#xff09;Tomcat的架构&#xff08;设计实现&#xff09;连接器的设计连接器架构分析核心功能ProtocolHandler 组件1.EndPoint组件EndPoint类结构图2.Processor组件Processor类结构图3.Adapter组件…

3.2 网站图的爬取路径

深度优先与广度优先方法都是遍历树的一种方法&#xff0c;但是网站的各个网页 之间的关系未必是树的结构&#xff0c;它们可能组成一个复杂的图形结构&#xff0c;即有回路。如果在前面的网站中每个网页都加一条Home的语句&#xff0c;让每个网页都能回到主界面&#xff0c;那么…

windows服务器实用(4)——使用IIS部署网站

windows服务器实用——IIS部署网站 如果把windows服务器作为web服务器使用&#xff0c;那么在这个服务器上部署网站是必须要做的事。在windows服务器上&#xff0c;我们一般使用IIS部署。 假设此时前端给你一个已经完成的网站让你部署在服务器上&#xff0c;别人可以在浏览器…

【Linux】-- 基于阻塞队列的生产者消费者模型

目录 前言 总结&#xff1a; 第一个问题的解决 基于BlockingQueue的生产者消费者模型 第二个问题的解决 wait的唤醒漏洞 深度理解生产者消费者模型 代码体现 锁的设计 总结&#xff1a; 前言 在多线程的条件变量遗留到此的问题。 #问&#xff1a;条件满足时&#xff0…

linux 防火墙管理-firewalld

什么是Firewalld 当前很多linux系统中都默认使用 firewalld&#xff08;Dynamic Firewall Manager of Linux systems&#xff0c;Linux系统的动态防火墙管理器&#xff09;服务作为防火墙配置管理工具。 “firewalld”是firewall daemon。它提供了一个动态管理的防火墙&#x…

Java知识复习(五)JVM虚拟机

1、虚拟机的自动内存管理和C/C的区别 C/C开发程序时需要为每一个new操作去写对应的delete/free操作&#xff0c;不容易出现内存泄漏和溢出问题。而Java程序将内存控制权交给了Java虚拟机 2、JVM的运行机制 1、Java程序的具体运行过程如下&#xff1a; Java源文件被编译器编…

c盘爆满--如何清理电脑C盘

问题 c盘饱满很多天了&#xff0c;今天终于忍无可忍&#xff0c;开始展开对c盘的处理 c盘的基本处理有两步&#xff0c; 第一步&#xff0c;电脑系统清理 1,c盘右键属性&#xff0c;有个磁盘清理&#xff0c;好像是系统更新的一些缓存资源&#xff0c;可以直接清理 当然这只…

Hadoop MapReduce

目录1.1 MapReduce介绍1.2 MapReduce优缺点MapReduce实例进程阶段组成1.3 Hadoop MapReduce官方示例案例&#xff1a;评估圆周率π&#xff08;PI&#xff09;的值案例&#xff1a;wordcount单词词频统计1.4 Map阶段执行流程1.5 Reduce阶段执行流程1.6 Shuffle机制1.1 MapReduc…

BigScience bloom模型

简介项目叫 BigScience,模型叫 BLOOM,BLOOM 的英文全名代表着大科学、大型、开放科学、开源的多语言语言模型。拥有 1760 亿个参数的模型.BLOOM 是去年由 1000 多名志愿研究人员,学者 在一个名为“大科学 BigScience”的项目中创建的.BLOOM 和今天其他可用大型语言模型存在的一…

信号的FFT变换与加窗

1. fft 傅里叶变换 1.1 傅里叶变换的本质 数学上有一种公式叫做 泰勒展开&#xff1a; 泰勒公式&#xff1a; 其表达的思想&#xff0c;是任意一函数可以有多个指数函数构成 当指数函数的个数趋近于无穷多个&#xff0c;那么组合出来的函数将会逼近原函数&#xff1b; …

Pandas数据查询

Pandas数据查询 Pandas查询数据的几种方法 df.loc方法&#xff0c;根据行、列的标签值查询 df.iloc方法&#xff0c;根据行、列的数字位置查询 df.where方法 df.query方法 .loc既能查询&#xff0c;又能覆盖写入&#xff0c;强烈推荐&#xff01; Pandas使用df.loc查询数据…

深度学习基础(二)-学习是怎么个回事

深度学习基础(一) 引入了一个 helloworld&#xff0c;提出了神经网络的简单关系&#xff0c;也就是一个基础公式 a(L) Sigmoid( a(L-1)*W(L) b(L)) a(L): 第L层神经元被激活之后 进行Sigmoid函数收敛 得到的值 b(L): 第L层神经元被激活阈值 W(L): 第L层神经元 与 第L-1层…

Android安卓中jni封装代码打包为aar

前文【Android安卓中jni与Java之间传递复杂的自定义数据结构】已经介绍jni编译c++代码且已经成功封装成java,但是c++是以源代码形式继承在app中,本文介绍如何将前述jni c++代码以隐藏源代码封装成aar的形式。 1、aar打包 1.1、新建module 按照流程 File -> New Module …

学习周报2.26

文章目录前言文献阅读摘要方法结果深度学习Encoder-Decoder&#xff08;编码-解码&#xff09;信息丢失的问题Attention机制总结前言 This week,I read an article about daily streamflow prediction.This study shows the results of an in-depth comparison between two di…

Oracle-RAC集群主机重启问题分析

问题背景: 在对一套两节点Oracle RAC19.18集群进行部署时&#xff0c;出现启动数据库实例就会出现主机出现重启的情况&#xff0c;检查发现主机重启是由于节点集群被驱逐导致​。 问题: 两节点Oracle RAC19.18集群,启动数据库实例会导致主机出现重启。 问题分析: 主机多次出现…

2023年第八周总周结 | 开学倒数第一周

为什么要做周总结&#xff1f; 1.避免跳相似的坑 2.客观了解上周学习进度并反思&#xff0c;制定可完成的下周规划 一、上周存在问题 发现自己反复犯同样问题&#xff0c;不想反思就不会意识到。总想以面带点的学习&#xff0c;实际上却在原地踏步。问题导向使用ChatGPT&#…

目标检测:DETR详解

1. 概述 DETR: End-to-End Object Detection with Transformers, DETR 是 Facebook 团队于 2020 年提出的基于 Transformer 的端到端目标检测,是Transformer在目标检测的开山之作 – DEtection TRansformer。 相比于传统的Faster-rcnn,yolo系列,DETR有以下几个优点:1).无需…

微信实时音视频通话数据流分析

一、实时音视频的架构 实时音视频通信架构主要包括P2P、SFU、MCU三种方式&#xff0c;其中点对点通信通常以P2P优先&#xff0c;P2P走不通的场景再借助于SFU/MCU。 P2P方式&#xff0c;终端之间点对点的相互收发数据流&#xff0c;音视频流不经过服务器&#xff1b; SFU是端侧…

scrapy下载图片

&#x1f431; 个人主页&#xff1a;莎萌玩家&#x1f64b;‍♂️ 作者简介&#xff1a;全栈领域新星创作者、专注于全栈各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01;&#x1f4ab;系列专栏&#xff1a;网络爬虫、WEB全栈开发&#x1f4e2; 资料…

二叉树的后序遍历-java递归+非递归-力扣145双百方案

一、题目描述给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序遍历 。示例 1&#xff1a;输入&#xff1a;root [1,null,2,3]输出&#xff1a;[3,2,1]示例 2&#xff1a;输入&#xff1a;root []输出&#xff1a;[]示例 3&#xff1a;输入&#xff1a;root [1]…