数据结构(C语言篇):(十三)堆的应用

目录

 

前言

一、堆排序

1.1  版本一:基于已有数组建堆、取栈顶元素完成排序

1.1.1  实现逻辑

1.1.2  底层原理

1.1.3  应用示例

1.1.4  执行流程

1.2  版本二:原地排序 —— 标准堆排序

1.2.1  实现逻辑

1.2.2  底层原理

1.2.3  时间复杂度计算

二、TOP-K问题

2.1  实现逻辑

2.2  底层原理

2.2.1  为何使用小堆而非大堆?

2.2.2  时间复杂度优化

2.2.3  空间复杂度优势

2.3  应用示例

2.4  执行流程


 

前言

        堆作为一种高效且灵活的树形结构,在算法设计与优化中扮演着重要角色。其独特的性质——完全二叉树形态与父子节点间的有序性,使得堆能够以对数级时间复杂度动态维护极值,从而为两类经典问题提供优雅解决方案:堆排序利用大顶堆或小顶堆实现无需递归的原地排序,将时间复杂度稳定控制在O(nlogn);而TOP-K问题则通过堆的极值筛选特性,在海量数据场景下以O(nlogk)的代价快速定位关键元素。无论是排序场景的性能平衡,还是大数据处理的效率优化,堆结构的应用都展现出算法设计与工程实践的深度结合。本文将系统解析这两类问题的技术实现与核心思想。下面就让我们正式开始吧!


一、堆排序

1.1  版本一:基于已有数组建堆、取堆顶元素完成排序

1.1.1  实现逻辑

  1. 创建并初始化堆:定义HP类型的堆变量hp,通过HPInit初始化。
  2. 构建堆:遍历数组arr,通过HPPush将每个元素插入堆中(插入过程会自动维护堆的性质)。
  3. 提取堆顶元素:循环从堆中获取堆顶元素(HPTop),依次存入原数组arr,然后通过HPPop删除堆顶元素。
  4. 销毁堆:排序完成后调用HPDesTroy释放堆占用的资源。

        完整代码如下所示:

//堆排序----这不是实际的堆排序
void HeapSort01(int* arr,int n)
{HP hp;//-----使用数据结构-堆HPInit(&hp);//调用push将数组中的数据放入到堆中for (int i = 0; i < n; i++){HPPush(&hp, arr[i]);}int i = 0;while (!HPEmpty(&hp)){int top = HPTop(&hp);arr[i++] = top;HPPop(&hp);}HPDesTroy(&hp);
}

1.1.2  底层原理

        排序过程的本质是这样的:

  • 若使用小堆:每次提取的堆顶是当前剩余元素的最小值,依次存入数组会得到升序结果
  • 若使用大堆:每次提取的堆顶是当前剩余元素的最大值,依次存入数组(需从后往前存)会得到升序结果

        这种堆排序方法的整体时间复杂度为,时间复杂度的计算主要需要考虑下面两种操作:

  • 构建堆:nHPPush,每次,总复杂度
  • 提取元素:nHPTop)和HPPop),总复杂度

1.1.3  应用示例

        假设堆为小堆(AdjustUpAdjustDown均按小堆逻辑实现),则应用示例如下所示:

// 假设已实现HPTop函数(获取堆顶元素)
HPDataType HPTop(HP* php) {assert(php && !HPEmpty(php));return php->arr[0];
}// 排序示例
int main() {int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};int n = sizeof(arr) / sizeof(arr[0]);HeapSort01(arr, n);// 输出排序结果(升序)for (int i = 0; i < n; i++) {printf("%d ", arr[i]); // 输出:1 1 2 3 4 5 6 9}return 0;
}

1.1.4  执行流程

        以数组[3, 1, 2]为例,执行流程如下所示:

        (1)初始化堆:HPInit(&hp)创建空堆。

        (2)元素入堆:

  • 插入 3:堆为[3];
  • 插入 1:经AdjustUp调整后堆为[1, 3];
  • 插入 2:经AdjustUp调整后堆为[1, 3, 2](小堆性质:1≤3 且 1≤2)。

        (3)元素出堆存回数组:

  • 第一次:HPTop获取 1,存入arr[0]HPPop后堆变为[2, 3];
  • 第二次:HPTop获取 2,存入arr[1]HPPop后堆变为[3];
  • 第三次:HPTop获取 3,存入arr[2]HPPop后堆为空。

        (4)销毁堆:HPDesTroy(&hp)释放资源,排序完成,数组变为[1, 2, 3]

        为什么说这种堆排序“不是实际的堆排序”呢?


        这种方法实现的堆排序与标准堆排序的核心区别在于空间效率:

        标准堆排序是直接在原数组上构建堆(原地建堆)的,通过 "堆顶与末尾元素交换 + 向下调整" 实现排序,不需要额外的堆结构,空间复杂度为

        而这种堆排序需要额外创建一个堆结构存储所有元素,空间复杂度为,本质是 "借助堆的特性实现排序",而非严格意义上的原地堆排序。

        除此之外,标准堆排序的建堆过程是通过AdjustDown从后往前调整(时间复杂度为),而该函数通过HPPush建堆(时间复杂度为),建堆效率是比较低的。

1.2  版本二:原地排序 —— 标准堆排序

        标准堆排序的核心思路在于:数组建堆,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据。

1.2.1  实现逻辑

        标准堆排序的核心操作主要分为两个阶段:

(1)建堆阶段:

        首先从最后一个非叶子节点(从索引(n-1-1)/ 2)开始,向前遍历至根节点;

        然后对于每个结点执行AdjustDown(向下调整),将无序数组转换为堆(默认实现为大堆,便于升序排序)。

(2)排序阶段:

  1. 定义end指向当前堆的最后一个元素(初始为n-1);
  2. 循环交换堆顶(索引0,当前最大值)与end位置的元素,将最大值 "沉" 到数组末尾;
  3. 对新堆顶(原end位置元素)执行AdjustDown,调整范围为[0, end)(排除已排好的末尾元素);
  4. 减小end,重复操作直到end0(所有元素排序完成)。

        完整代码的实现如下:

//堆排序————使用的是堆结构的思想 n * logn
void HeapSort(int* arr, int n)
{//向下调整算法——建堆nfor (int i = (n - 1 - 1)/2; i >= 0; i--){AdjustDown(arr, i, n);}////向上调整算法建堆n*logn//for (int i = 0; i < n; i++)//{//	AdjustUp(arr, i);//}//n*lognint end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);//lognend--;}
}

1.2.2  底层原理

  • 建堆的选择

        标准堆排序优先使用AdjustDown从后往前建堆,时间复杂度为

        上述代码中的AdjustUp从前往后建堆,时间复杂度为,因此前者更高效;

        建堆选择大堆的原因:大堆的堆顶是最大值,每次交换后可将最大值放到数组末尾,直接形成升序。

  • 原地排序的实现

        无需额外空间,直接在原数组上通过索引操作实现堆结构(利用完全二叉树的父子节点索引关系);

    end变量的作用是划分 "未排序的堆区域" 和 "已排序的末尾区域",随着循环逐渐缩小堆区域。

1.2.3  时间复杂度计算

        

        分析如下:

        第1层,有个结点,交换到根结点后,需要向下移动0层;

        第2层,有个结点,交换到根结点后,需要向下移动1层;

        第3层,有个结点,交换到根结点后,需要向下移动2层;

        ……

        第h层,有个结点,交换到根结点后,需要向下移动h-1层。

        由此我们可以发现,堆排序第二个循环中的向下调整与建堆中的向上调整算法的时间复杂度计算是一致的,故在这博主就不再过多赘述了。大家如果感兴趣的话可以去看看我上一期的博客。

        最终可以计算得到堆排序的时间复杂度为
 

二、TOP-K问题

        TOP-K问题就是求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都是比较大的。

        比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

        对于TOP-K问题,我们能够想到的最简单直接的方法就是进行排序。但是,如果数据量非常大时,排序的效率就会变得很低(可能出现数据不能一下全部加载到内存中的情况)。因此解决TOP-K问题的最佳方式是使用堆,基本思路如下:

(1)用数据集合中的前K个元素来建堆:

        如果用前K个最大的元素,则建小堆;如果用前K个最小的元素,则建大堆。

(2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素:

        将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

        要解决TOP-K问题,我们需要先利用C语言篇学到的文件操作的相关知识,来造一些数据:

void CreateNDate()
{// 造数据int n = 100000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (int i = 0; i < n; ++i){int x = (rand() + i) % 1000000;fprintf(fin, "%d\n", x);}fclose(fin);
}

        这样我们就生成了100000个随机数,并把它们存储在了“data.txt”文件中。

        下面我们就来正式尝试解决一下TOP-K问题:

2.1  实现逻辑

(1)获取用户输入K:通过scanf读取需要查找的前 K 个元素数量。

(2)文件操作初始化:打开数据文件data.txt,处理文件打开失败的异常。

(3)创建小堆:

  • 动态分配大小为 K 的数组minHeap(用于存储候选的前 K 个最大元素);
  • 从文件中读取前 K 个数据存入数组;
  • 通过AdjustDown将数组调整为小堆(堆顶为当前 K 个元素中的最小值)。

(4)筛选剩余数据:

  • 遍历文件中剩余的所有数据;
  • 若当前数据大于小堆的堆顶(说明该数据比当前候选的最小元素更大,有资格进入前 K),则替换堆顶并通过AdjustDown重新调整为小堆。

(5)输出结果:循环结束后,小堆中存储的就是整个文件中最大的前 K 个元素,打印输出。

(6)资源释放:关闭文件。

        完整代码如下所示:

void TopK()
{int k = 0;printf("请输入K:");scanf("%d", &k);const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen fail!");exit(1);}//申请空间大小为k的整型数组int* minHeap = (int*)malloc(sizeof(int) * k);if (minHeap == NULL){perror("malloc fail!");exit(2);}//读取文件中k个数据放到数组中for (int i = 0; i < k; i++){fscanf(fout, "%d", &minHeap[i]);}//数组调整建堆--向下调整建堆//找最大的前K个数,建小堆for (int i = (k-1-1)/2; i >= 0; i--){AdjustDown(minHeap, i, k);}//遍历剩下的n-k个数,跟堆顶比较,谁大谁入堆int data = 0;while (fscanf(fout,"%d",&data) != EOF){if (data > minHeap[0]){minHeap[0] = data;AdjustDown(minHeap, 0, k);}}//打印堆里的数据for (int i = 0; i < k; i++){printf("%d ", minHeap[i]);}printf("\n");fclose(fout);
}

2.2  底层原理

2.2.1  为何使用小堆而非大堆?

        我们的目标是找 “最大的前 K 个元素”,小堆的堆顶是当前候选集中的最小值。当新元素大于堆顶时,说明它比候选集中最小的元素更大,是有资格替换堆顶进入候选集的。

        若使用大堆,堆顶是候选集中的最大值,是无法通过简单比较判断新元素是否应进入候选集的(新元素可能比堆顶小但比其他元素大)。

2.2.2  时间复杂度优化

        在传统方法中,是对所有数据排序后取前 K 个,时间复杂度为(n 为数据总量)。

        在堆解法中,建堆时间为,遍历剩余数据并调整堆的时间为,整体复杂度为。当 n 极大(如百万 / 亿级)且 K 较小时(如 100),性能优势是十分显著的。

2.2.3  空间复杂度优势

        堆解法仅需要的空间存储小堆,适合处理无法一次性加载到内存的海量数据(如大文件)。

2.3  应用示例

        假设data.txt中存储以下数据(共 10 个元素):

5 9 3 12 7 15 2 8 11 6

        当用户输入k=3时,期望输出最大的 3 个元素:15、12、11。

        首先读取前 3 个数据[5, 9, 3],建小堆后为[3, 9, 5](堆顶 3 是当前最小值)

        接着遍历剩余数据:

                12 > 3 → 替换堆顶为 12,调整后小堆为[5, 9, 12];

                7 > 5 → 替换堆顶为 7,调整后小堆为[7, 9, 12];

                15 > 7 → 替换堆顶为 15,调整后小堆为[9, 15, 12];

                2 ≤ 9 → 不处理;

                8 ≤ 9 → 不处理;

                11 > 9 → 替换堆顶为 11,调整后小堆为[11, 15, 12];

                6 ≤ 11 → 不处理。

        最终小堆[11, 15, 12]中存储的就是最大的 3 个元素(输出顺序可能因堆结构而略有不同)。

2.4  执行流程

(1)输入与初始化:

  • 用户输入 K 值(如 3);
  • 打开data.txt,分配大小为 K 的数组minHeap。

(2)构建初始小堆:

  • 读取前 K 个数据到minHeap;
  • 从最后一个非叶子节点(k-1-1)/2开始,通过AdjustDown将数组调整为小堆(确保堆顶是当前最小值)。

(3)筛选剩余数据:

  • 循环读取文件中剩余的每个数据data
    • data > 堆顶(说明data比当前候选集中最小的元素大):
      • data替换堆顶元素;
      • 调用AdjustDown从堆顶开始调整,维持小堆性质。
    • 否则:跳过该数据。

(4)输出结果:

        小堆中保存的 K 个元素即为整个文件中最大的前 K 个,打印输出。

        最终我们可以得到TOP-K问题的时间复杂度为:

 

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

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

相关文章

4步OpenCV-----扫秒身份证号

这段代码用 OpenCV 做了一份“数字模板字典”&#xff0c;然后在银行卡/身份证照片里自动找到身份证号那一行&#xff0c;把每个数字切出来跟模板比对&#xff0c;最终输出并高亮显示出完整的身份证号码&#xff0c;下面是代码解释&#xff1a;模块 1 工具箱&#xff08;通用函…

冯诺依曼体系:现代计算机的基石与未来展望

冯诺依曼体系&#xff1a;现代计算机的基石与未来展望 引人入胜的开篇 当你用手机刷视频、用电脑办公时&#xff0c;是否想过这些设备背后共享的底层逻辑&#xff1f;从指尖轻滑切换APP&#xff0c;到电脑秒开文档&#xff0c;这种「无缝衔接」的体验&#xff0c;其实藏着一个改…

前端基础 —— C / JavaScript基础语法

以下是对《3.JavaScript(基础语法).pdf》的内容大纲总结&#xff1a;---&#x1f4d8; 一、JavaScript 简介 - 定义&#xff1a;脚本语言&#xff0c;最初用于表单验证&#xff0c;现为通用编程语言。 - 应用&#xff1a;网页开发、游戏、服务器&#xff08;Node.js&#xff09…

springboot 二手物品交易系统设计与实现

springboot 二手物品交易系统设计与实现 目录 【SpringBoot二手交易系统全解析】从0到1搭建你的专属平台&#xff01; &#x1f50d; 需求确认&#xff1a;沟通对接 &#x1f5e3; &#x1f4ca; 系统功能结构&#xff1a;附思维导图 ☆开发技术&#xff1a; &#x1f6e…

【Android】可折叠式标题栏

在 Android 应用开发中&#xff0c;精美的用户界面可以显著提升应用品质和用户体验。Material Design 组件中的 CollapsingToolbarLayout 能够为应用添加动态、流畅的折叠效果&#xff0c;让标题栏不再是静态的元素。本文将深入探讨如何使用 CollapsingToolbarLayout 创建令人惊…

Debian13下使用 Vim + Vimspector + ST-LINK v2.1 调试 STM32F103 指南

1. 硬件准备与连接 1.1 所需硬件 STM32F103C8T6 最小系统板ST-LINK v2.1 调试器连接线&#xff08;杜邦线&#xff09; 1.2 硬件连接 ST-LINK v2.1 ↔ STM32F103C8T6 连接方式&#xff1a;ST-LINK v2.1 引脚STM32F103C8T6 引脚功能说明SWDIOPA13数据线SWCLKPA14时钟线GNDGND共地…

第21课:成本优化与资源管理

第21课:成本优化与资源管理 课程目标 掌握计算资源优化 学习成本控制策略 了解资源调度算法 实践实现成本优化系统 课程内容 21.1 成本分析框架 成本分析系统 class CostAnalysisFramework {constructor(config) {this.config

SAP HANA Scale-out 04:CalculationView优化

CV执行过程计算视图激活时&#xff0c;生成Stored ModelSELECT查询时&#xff1a;首先将Stored Model实例化为runtime Model 计算引擎执行优化&#xff0c;将runtime Model转换为Optimized Runtime ModelOptimized Runtime Model通过SQL Optimizer进行优化计算引擎优化特性说明…

鸿蒙审核问题——Scroll中嵌套了List/Grid时滑动问题

文章目录背景原因解决办法1、借鉴Flutter中的解决方式&#xff0c;如下图2、鸿蒙Next中对应的解决方式&#xff0c;如下图3、官方文档回访背景 来源一次审核被拒的情况。也是出于粗心导致的。之前在flutter项目中也是遇到过这种问题的。其实就是滚动视图内嵌滚动视图造成的&am…

测试电商购物车功能,设计测试case

在电商场景中&#xff0c;购物车是连接商品浏览与下单支付的关键环节&#xff0c;需要从功能、性能、兼容性、安全性等多维度进行测试。以下是购物车功能的测试用例设计&#xff1a; 一、功能测试 1. 商品添加到购物车 - 未登录状态下&#xff0c;添加商品到购物车&#xff08;…

Linux --- 常见的基本指令

一. 前言本篇博客使用的 Linux 操作系统是 centos &#xff0c;用来学习Linux 的 Linux 系统的内核版本和系统架构信息版本如下所示&#xff1a;上图的主要结构为&#xff1a;主版本号-次版本号 修正次数&#xff0c;3.10.0 是操作系统的主版本号&#xff1b;当我们在维护一段L…

微信小程序 -开发邮箱注册验证功能

一、前端验证&#xff1a;正则表达式与插件结合正则表达式设计 使用通用邮箱格式校验正则&#xff0c;并允许中文域名&#xff08;如.中国&#xff09;&#xff1a; const emailReg /^[a-zA-Z0-9._%-][a-zA-Z0-9-](?:\.[a-zA-Z0-9-])*\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2})?$/i;…

docker 部署 code-server

docker 部署 code-servercode-serverError response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headersdocker 配置正确步骤 阿里云源permission de…

网络编程专题:从源码解析网络编程常用方法(基于6.16.3内核)

前言 本文是因为作者在研究下面这个代码时发现的问题&#xff1a; int main() {// 1. 创建 IPv4 专用地址结构体 sockaddr_instruct sockaddr_in ipv4_addr;memset(&ipv4_addr, 0, sizeof(ipv4_addr)); // 初始化清零// 2. 填充 IPv4 专属信息ipv4_addr.sin_family AF_IN…

2025年数字公共治理专业重点学什么内容?(详细指南)

数字公共治理作为一个新兴的跨学科领域&#xff0c;近年来受到越来越多高校和学生的关注。这个专业融合了多个学科的知识体系&#xff0c;旨在培养掌握现代治理理念和技术应用能力的复合型人才。对于在校大学生而言&#xff0c;了解这一专业的学习内容和发展方向&#xff0c;有…

一招解决 win 下 终端打印中文乱码问题

适合所有终端 cmd powershell git bash&#xff0c; 原理&#xff1a;修改电脑的区域设置&#xff0c;勾选使用 UTF-8 1.电脑搜索 区域&#xff0c; 打开区域设置2. 打开相关设置3. 点击更改 日期、时间或数字格式4. 选则管理-点击更改系统区域设置&#xff0c;在弹出框中勾选 …

Elasticsearch面试精讲 Day 13:索引生命周期管理ILM

【Elasticsearch面试精讲 Day 13】索引生命周期管理ILM 在“Elasticsearch面试精讲”系列的第13天&#xff0c;我们将深入探讨 索引生命周期管理&#xff08;Index Lifecycle Management, ILM&#xff09; 这一核心运维机制。作为大规模日志、监控和时序数据场景下的必备功能&…

Python快速入门专业版(二十八):函数参数进阶:默认参数与可变参数(*args/**kwargs)

目录引一、默认参数&#xff1a;给函数参数设置“默认值”1. 基本语法与使用示例示例1&#xff1a;带默认参数的乘法函数2. 默认参数的核心规则&#xff1a;必须放在非默认参数之后示例2&#xff1a;默认参数位置错误&#xff08;报错&#xff09;3. 默认参数的“可变对象陷阱”…

FreeRTOS 知识点

一、配置过程二、基本知识点2.1 抢占优先级和响应优先级在 FreeRTOS 中&#xff0c;任务的调度方式主要有 ​​抢占式&#xff08;Preemptive&#xff09;​​ 和 ​​协作式&#xff08;Cooperative&#xff09;​​ 两种模式&#xff0c;它们的核心区别在于 ​​任务如何释放…

SQL注入漏洞手动测试详细过程

这是一次详细的、基于真实手动测试思维的SQL注入漏洞测试过程记录。我们将以一个假设的Web应用程序为例&#xff0c;进行逐步探测和利用。测试目标假设我们正在测试一个名为 example.com 的电商网站&#xff0c;其有一个查看商品详情的页面&#xff0c;URL 为&#xff1a; http…