【数据结构】深度解析堆排序

news2024/10/7 18:29:56

目录

💯引言

💯堆的概念

(一)什么是堆

(二)堆的表示

💯堆排序原理

(一)建堆

(二)排序

💯代码实现

💯代码分析

(一)heapify函数

(二)heapSort函数

(三)printArray函数

(四)main函数

💯性能分析

(一)时间复杂度

(二)空间复杂度

(三)稳定性

💯应用场景

💯总结


💯引言

在计算机科学领域,排序算法是非常重要的基础算法之一,它们被广泛应用于各种场景,如数据库查询、数据处理、算法设计等。之前的关于的文章👉【数据结构】堆(Heap)详解

堆排序作为一种高效的排序算法,具有时间复杂度为的优异性能,并且在空间复杂度上也有较好的表现。本文将深入解析堆排序的原理实现过程以及其性能特点,并使用 C 语言进行代码实现。

💯堆的概念

(一)什么是堆

堆是一种特殊的数据结构,它通常可以被看作是一棵完全二叉树。在这棵完全二叉树中,每个节点都满足特定的堆性质。对于大根堆,每个节点的值都大于或等于其左右子节点的值;对于小根堆,每个节点的值都小于或等于其左右子节点的值。

(二)堆的表示

在实际的程序实现中,堆通常用数组来表示。因为完全二叉树的性质,对于数组中索引为i的节点,其左子节点的索引为2i + 1,右子节点的索引为2i + 2,父节点的索引为(i - 1) / 2(向下取整)。这种表示方式使得在操作堆时,可以方便地通过数组索引来访问和修改节点的值,而无需显式地构建二叉树的节点结构和指针。

例如,假设有一个数组arr = [10, 5, 8, 3, 2],它可以表示为如下的完全二叉树(以大根堆为例):

       10
     /    \
    5      8
   / \    /
  3   2

在这个例子中,数组索引 0处的元素10是根节点,它的左子节点是数组索引1处的元素5,右子节点是数组索引2处的元素8;节点5的左子节点是数组索引3处的元素3,右子节点是数组索引4处的元素2

💯堆排序原理

(一)建堆


升序:建大堆
降序:建小堆

堆排序的第一步将待排序的数组构建成一个堆。从数组的中间位置开始,向前遍历每个节点,对每个节点进行堆化操作(调整节点及其子树,使其满足堆性质)。

以大根堆为如果一个节点的值小于其较大的子节点的值,就将它们交换,然后继续对交换后的子节点进行堆化操作,直到该节点及其子树满足大根堆性质。这个过程确保了数组构建成的完全二叉树满足大根堆的要求,即根节点是数组中的最大元素。

“为什么从数组中间位置开始向前遍历进行堆化呢?”这是因为数组后半部分的节点都是叶子节点,它们本身已经满足堆的性质(没有子节点),所以只需要对非叶子节点进行堆化操作。

例如,对于数组{1, 5, 3, 8, 7, 6},中间位置的索引为(6 / 2 - 1) = 2,先对索引为2的节点(值为3)进行堆化,然后依次向前对索引为10的节点进行堆化,最终构建成大根堆。

图解如下: 

 

(二)排序

建堆完成后,数组中的最大元素位于根节点(即数组的第一个位置)。将根节点与数组的最后一个元素交换,此时最大元素被放置到了正确的位置(数组的末尾)。然后对除了最后一个元素之外的数组进行堆化操作,使其再次成为一个大根堆。重复这个过程,每次将根节点与当前未排序部分的最后一个元素交换,然后对剩余部分进行堆化,直到整个数组有序。

比如,在第一次交换后,数组的最后一个元素是当前最大的,然后对剩下的元素重新堆化,找到次大的元素放在根节点,再与剩下未排序部分的最后一个元素交换,以此类推,逐步将数组排序。

 

💯代码实现

#include <stdio.h>

// 调整堆,使其满足大根堆性质
void heapify(int arr[], int n, int i) {
    int largest = i; // 初始化 largest 为当前节点索引
    int left = 2 * i + 1; // 左子节点索引
    int right = 2 * i + 2; // 右子节点索引

    // 如果左子节点大于根节点,更新最大索引
    if (left < n && arr[left] > arr[largest])
        largest = left;

    // 如果右子节点大于当前最大节点,更新最大索引
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // 如果最大节点不是根节点,交换并递归调整
    if (largest!= i) {
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;

        heapify(arr, n, largest); // 递归调整以 largest 为根的子树
    }
}

// 堆排序函数
void heapSort(int arr[], int n) {
    // 建堆
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // 排序
    for (int i = n - 1; i > 0; i--) {
        // 将根节点与最后一个元素交换
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;

        // 对剩余元素进行堆化
        heapify(arr, i, 0);
    }
}

// 打印数组函数
void printArray(int arr[], int n) {
    for (int i = 0; i < n; ++i)
        printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("原始数组: ");
    printArray(arr, n);

    heapSort(arr, n);

    printf("排序后的数组: ");
    printArray(arr, n);

    return 0;
}

💯代码分析

(一)heapify函数

// 调整堆,使其满足大根堆性质
void heapify(int arr[], int n, int i) {
    int largest = i; // 初始化 largest 为当前节点索引
    int left = 2 * i + 1; // 左子节点索引
    int right = 2 * i + 2; // 右子节点索引

    // 如果左子节点大于根节点,更新最大索引
    if (left < n && arr[left] > arr[largest])
        largest = left;

    // 如果右子节点大于当前最大节点,更新最大索引
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // 如果最大节点不是根节点,交换并递归调整
    if (largest!= i) {
        int temp = arr[i];
        arr[i] = arr[largest];
        arr[largest] = temp;

        heapify(arr, n, largest); // 递归调整以 largest 为根的子树
    }
}
  • 函数首先确定当前节点i及其左右子节点的索引leftright
  • 然后通过比较左右子节点与当前节点的值,找到三者中的最大值,并将其索引存储在largest变量中。
  • 如果largest不等于i,说明当前节点不满足大根堆性质,需要将当前节点与largest指向的子节点交换值。然后递归地对largest索引处的子树进行heapify操作,以确保交换后子树仍然满足大根堆性质。

例如,在对某个节点进行堆化时,如果该节点的值小于其左子节点的值,就将它们交换,然后再检查交换后的左子节点及其子树是否满足大根堆性质,如果不满足,继续递归调整。

(二)heapSort函数

// 堆排序函数
void heapSort(int arr[], int n) {
    // 建堆
    for (int i = n / 2 - 1; i >= 0; i--)
        heapify(arr, n, i);

    // 排序
    for (int i = n - 1; i > 0; i--) {
        // 将根节点与最后一个元素交换
        int temp = arr[0];
        arr[0] = arr[i];
        arr[i] = temp;

        // 对剩余元素进行堆化
        heapify(arr, i, 0);
    }
}
  • 首先进行建堆操作。从数组中间位置开始向前遍历,对每个节点调用heapify函数,构建大根堆。
  • 建堆完成后,开始排序过程。每次将根节点(即当前最大元素)与数组的最后一个未排序元素交换,然后对除最后一个元素外的数组进行heapify操作,使剩余部分再次成为大根堆。重复这个过程,直到整个数组有序。

(三)printArray函数

// 打印数组函数
void printArray(int arr[], int n) {
    for (int i = 0; i < n; ++i)
        printf("%d ", arr[i]);
    printf("\n");
}

用于简单地打印数组中的元素,方便查看排序前后的数组状态。

(四)main函数

int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("原始数组: ");
    printArray(arr, n);

    heapSort(arr, n);

    printf("排序后的数组: ");
    printArray(arr, n);

    return 0;
}
  • 定义了一个测试数组arr并初始化,同时计算数组的长度n
  • 打印原始数组,然后调用heapSort函数对数组进行排序,最后打印排序后的数组。

 全过程图解如下: 

 

💯性能分析

(一)时间复杂度

  • 建堆时间复杂度:建堆过程从数组的中间位置开始,对每个节点进行堆化操作。对于一个包含n个元素的数组,建堆的时间复杂度约为O(n)。更准确地分析,建堆的时间复杂度为O(n* {log_{}}^{}n),但是由于堆是一种近似完全二叉树的结构,其高度{log_{}n}^{},且在调整堆的过程中,每个节点的调整时间与树的高度相关,因此可以近似看作是线性时间复杂度。因此:建堆的时间复杂度为O(N)。
  • 排序时间复杂度:在排序阶段,每次将根节点与未排序部分的最后一个元素交换,然后对剩余元素进行堆化,需要进行n-1次操作。每次堆化的时间复杂度为O({log_{}n}^{}),所以排序的时间复杂度为O(n* {log_{}}^{}n)

综合来看,堆排序的时间复杂度为O(n* {log_{}}^{}n),无论是最好、最坏还是平均情况,时间复杂度都是稳定的,这使得堆排序在处理大规模数据时具有很好的性能表现。

 

(二)空间复杂度

堆排序的空间复杂度为O(1)。因为在整个排序过程中,只需要常数级别的额外空间来进行元素的交换和临时变量的存储,不需要像一些其他排序算法(如归并排序)那样需要额外的数组来存储中间结果。

(三)稳定性

堆排序是一种不稳定的排序算法。这是因为在交换根节点和最后一个元素时,可能会改变相同值元素的相对顺序。例如,如果数组中有两个相等的元素,一个在较大元素的子树中,另一个在较小元素的子树中,在排序过程中它们的相对位置可能会发生改变。

 

💯应用场景

  • 海量数据排序:由于堆排序具有较好的时间复杂度和相对较低的空间复杂度,适用于对大规模数据进行排序。在内存有限的情况下,堆排序可以有效地处理大量数据,而不需要过多的额外存储空间。
  • 优先级队列:堆可以很方便地实现优先级队列。在优先级队列中,元素根据其优先级进行排序,堆的性质使得插入和删除最大(或最小)优先级元素的操作可以在O({log_{}n}^{})的时间内完成。例如,操作系统中的任务调度、网络数据包的优先级处理等都可以使用基于堆的优先级队列来实现。

 

💯总结

堆排序是一种高效的排序算法,通过利用堆这种特殊的数据结构,能够在O(n* {log_{}}^{}n)的时间复杂度内对数据进行排序,并且具有较好的空间性能。理解堆排序的原理和实现对于深入掌握算法和数据结构知识具有重要意义。在实际应用中,我们可以根据具体的需求和场景选择合适的排序算法,而堆排序在处理大规模数据和需要实现优先级队列等方面具有独特的优势。通过对堆排序的深入学习,我们也可以更好地理解和应用其他相关的数据结构和算法,提高程序的性能和效率。


💝💝💝如果你对堆排序还有其他疑问或者想要进一步探讨相关内容,欢迎随时交流~

感谢你看到最后,制作不易,点个赞再走吧!我的主页👉【A charmer】

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

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

相关文章

【Sqlite】sqlite内部函数sqlite3_value_text特性

目录 ⚛️1 结论 ☪️2 说明 ☪️3 传入数值转成科学计数法 ♋3.1 只有整数部分 ♏3.2 只有小数部分 ♐3.3 整数小数 ⚛️1 结论 整数(sqlite视为int64)位数 > 20位&#xff0c;sqlite3_value_text 采用科学计数法。否则正常表示。 浮点数(sqlite视为double)的整数部…

STM32 通用同步/异步通信

一、串行通信简介 CPU与外围设备之间的信息交换称为通信。基本的通信方式有并行通信和串行通信两种。STM32单片机提供了功能强大的串行通信模块&#xff0c;即通用同步/异步收发器&#xff08;USART&#xff09;。 1.串行通信 串行通信是数据字节一位一位地依次传送的通信方式。…

HarmonyOS第一课 05 从简单的页面开始-习题

【习题】从简单的页面开始 通过/及格分80/ 满分100 判断题 1.Button作为容器使用时可以通过添加子组件实现包含文字、图片等元素的按钮&#xff0c;其类型包括胶囊按钮、圆形按钮、普通按钮。T 正确(True) 错误(False) 大部分前端框架的按钮都具有这几个类型,鸿蒙也不例外…

Ubuntu+VsCode++搭建C++开发环境

Ubuntu下使用VsCode搭建C开发环境 1、基本工具的安装 首先Ubuntu下安装好C开发的一个些基本工具g、gdb、make、cmake等&#xff0c;安装方式点这里 检查一下安装环境 $ g --version g (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation,…

位图的应用

目录 问题引入 位图概念 位图的实现 应用2&#xff1a;找到只出现一次的整数 应用三&#xff1a;找交集 STL中的位图 问题引入 面试题 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在 这40亿个数中。【腾讯】 解决…

幂,你去哪儿了-《分析模式》漫谈37

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 “Analysis Patterns”的第3章的图3.5&#xff0c;原文的图是&#xff1a; 2004&#xff08;机械工业出版社&#xff09;中译本的图是&#xff1a; direct翻译成分子&#xff0c;inv…

master节点k8s部署]33.ceph分布式存储(四)

总结ceph分布式存储&#xff08;三&#xff09;中提到的三种方法&#xff1a; 1.创建rbda&#xff0c;并且在创建pv的时候配置该rbda,以下代码仅展示关键信息。 [rootxianchaomaster1 ~]# cat pv.yaml apiVersion: v1 kind: PersistentVolume metadata: name: ceph-pv ...…

MySQL多表查询:行子查询

先看我的表数据 dept表 emp表 行子查询 子查询返回的结果是一行&#xff08;可以是多列&#xff09;, 这种子查询称为行子查询 常用的操作符: , <>, IN, NOT IN 例子1. 查询与“张无忌” 的薪资及直属领导相同的员工信息 拆解成两个问题 a. 查询"张无忌"…

基于SpringBoot+Vue+MySQL的汽车租赁系统

系统展示 用户前台界面 管理员后台界面 系统背景 随着城市化和交通需求的不断增加&#xff0c;汽车租赁业务成为了现代社会的一个重要组成部分。汽车租赁服务为人们提供了一种灵活便捷的交通解决方案&#xff0c;让用户在无需购买车辆的情况下&#xff0c;根据实际需要租赁车辆…

端口冲突的解决方案以及SpringBoot自动检测可用端口demo

端口冲突的解决方案 端口冲突通常发生在尝试运行两个或多个应用程序或服务时&#xff0c;它们尝试使用同一个端口号&#xff0c;导致系统无法正确分配资源。 各种端口错误 你是否遇到过下面这些报错信息呢&#xff1f; Windows 系统报错&#xff1a; 系统错误 1004 套接字操作…

图像转3D视差视频:DepthFlow、kling

1、DepthFlow 参看: https://github.com/BrokenSource/DepthFlow 通过深度图实现图像3d效果 安装 https://brokensrc.dev/get/pypi/#installing pip insatll depthflow shaderflow broken-source pianola spectronote turbopipe 使用 1、下载项目 git clone https://gith…

约数个数约数之和

好久没发文章了.......不过粉丝还是一个没少...... 今天来看两道超级恶心的数论题目&#xff01; No.1 约数个数 No.2 约数之和 先来看第一道&#xff1a;约数个数 题目描述 给定 n 个正整数 ai​,请你输出这些数的乘积的约数个数,答案对 10^97 取模 输入格式 第一行包含…

Python_文件处理

一个完整的程序一般都包括数据的存储和读取&#xff1b;我们在前面写的程序数据都没有进行实际的存储&#xff0c;因此python解释器执行完数据就消失了。实际开发中&#xff0c;我们经常需要从外部存储介质&#xff08;硬盘、光盘、U盘等&#xff09;读取数据&#xff0c;或者将…

微信小程序开发-目录结构介绍

文章目录 一&#xff0c;目录结构介绍1&#xff0c;主体文件2&#xff0c;页面文件3&#xff0c;修改页面渲染模式 二&#xff0c;新增页面1&#xff0c;右键“pages”-新建文件夹2&#xff0c;右键文件夹-新建page3&#xff0c;新建页面的快捷方式 四&#xff0c;基础库设置 一…

①EtherNet/IP转ModbusTCP, EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关https://item.taobao.com/item.htm?ftt&id822721028899 协议转换通信网关 EtherNet/IP 转 Modbus TCP GW型号系列 MS-GW25 概述 简介 MS-GW25 是 EtherNet/IP 和 Modbus TCP 协议转换网关&#xff0c;为…

C语言 | 第十一章 | static 日期函数 数学函数

P 100 变量作用域基本规则 2023/1/9 一、基本介绍 概念&#xff1a;所谓变量作用域&#xff08;Scope&#xff09;&#xff0c;就是指变量的有效范围。 函数内部声明/定义的局部变量&#xff0c;作用域仅限于函数内部。 #include<stdio.h> void sayHello() {char nam…

【C++】—— 继承(上)

【C】—— 继承&#xff08;上&#xff09; 1 继承的概念与定义1.1 继承的概念1.2 继承定义1.2.1 定义格式1.2.2 继承父类成员访问方式的变化 1.3 继承类模板 2 父类和子类对象赋值兼容转换3 继承中的作用域3.1 隐藏规则3.2 例题 4 子类的默认成员函数4.1 构造函数4.1.1 父类有…

稀缺是否意味着价值

省流版&#xff1a;物以稀为贵。 稀少并不等于需求。 更新为&#xff1a;物以希为贵。 有需求就意味着有价值。 不管是20&#xff1a;80中的20&#xff0c;还是10&#xff1a;90中的10&#xff0c;还是2&#xff1a;98中的2。 所以&#xff0c;这个模型里一定会出现1这类人&a…

MambaAD 实验部分讲解

4 实验 4.1 设置&#xff1a;数据集、指标和细节 数据集&#xff08;6个&#xff09; 1.MVTec-AD&#xff1a; 包含5种类型的纹理和10种类型的对象&#xff0c;总共5,354张高分辨率图像。 实验&#xff1a; 3,629张正常图像被指定为训练。 剩下的 1,725 张图像被保留用于测试…

AWS MySQL 升级(三)—— TAZ - 近0停机的小版本升级方案

与AWS交流了解到的新方案&#xff0c;没有实际试过&#xff0c;所以本篇主要是些原理 一、 TAZ的含义 TAZ实际上就是 3 AZ&#xff0c;扩展一些就是 Multi-AZ DB Cluster&#xff0c;即在3个可用区部署DB&#xff0c;具备两个只读备用实例。 二、 TAZ的主要用途 1. 近0停机的小…