【数据结构与算法】堆 / 堆排序 / TopK问题(Heap)

文章目录

  • 1.堆
  • 2.C语言实现堆
    • 2.1 堆结构与基本操作
    • 2.2 其它辅助操作
    • 2.3 堆的基本操作
      • 2.3.1 插入
      • 2.3.2 删除
  • 3. 堆排序
  • 4. TopK
  • 5. 所有代码

1.堆

堆总是一棵完全二叉树,而完全二叉树更适合使用**顺序结构(数组)**存储。二叉树不适合用数组来存储,因为存在空间浪费。需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
在这里插入图片描述
使用数组存储二叉树,基于父子节点下标间的关系:leftchild = parent * 2 + 1,rightchild = parent * 2 + 2,parent = (child - 1) / 2,如果打破这种存储关系则数组无法表示二叉树,所以数组存储非完全二叉树注定要浪费空间。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。最大堆即任意一个父节点都大于等于孩子节点,最小堆即任意一个父节点都小于等于孩子节点。
在这里插入图片描述

2.C语言实现堆

2.1 堆结构与基本操作

堆结构看起来与顺序表无异,毕竟都是数组实现。不一样的是逻辑结构,顺序表是线性结构,堆是树形结构。堆的基本操作只有插入和删除,应用场景有堆排序和TopK(前k个最大或最小的数)。

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>// 堆结构
typedef int datatype;
typedef struct Heap {datatype* arr;int size;int capacity;
} Heap;// 其它辅助操作
void HeapInit(Heap* heap);
void HeapDestroy(Heap* heap);
datatype HeapTop(Heap* heap); // 取堆顶元素
size_t HeapSize(Heap* heap);
bool HeapEmpty(Heap* heap);// logn
void HeapPush(Heap* heap, datatype val);
void HeapPop(Heap* heap); // 删除堆顶元素
// 插入或删除时,堆向上、向下调整
void Swap(datatype* x, datatype* y);
void AdjustUp(datatype* arr, int child);
void AdjustDown(datatype* arr, int size, int parent);// 堆排序 nlogn、求TopK nlogk
void HeapSort(datatype* arr, int size);
void PrintTopK(const char* file, int k);

2.2 其它辅助操作

void HeapInit(Heap* heap) {assert(heap);heap->arr = NULL;heap->size = heap->capacity = 0;
}void HeapDestroy(Heap* heap) {assert(heap);free(heap->arr);heap->arr = NULL;heap->size = heap->capacity = 0;
}datatype HeapTop(Heap* heap) {assert(heap && heap->arr && heap->size > 0);return heap->arr[0];
}size_t HeapSize(Heap* heap) {assert(heap);return heap->size;
}bool HeapEmpty(Heap* heap) {assert(heap);return heap->size == 0;
}

2.3 堆的基本操作

2.3.1 插入

直接往数组插入数据,然后再向上调整即可。以构建最小堆举例,只要插入的数据比父节点小,就与父节点交换,重复这个操作直到不能再做交换。

void HeapPush(Heap* heap, datatype val) {assert(heap);if (heap->size == heap->capacity) { // 空间不够时扩容heap->capacity = heap->capacity == 0 ? 10 : heap->capacity * 2;datatype* tmp = realloc(heap->arr, sizeof(datatype) * heap->capacity);if (tmp == NULL) {perror("HeapPush malloc failed.");exit(-1);}heap->arr = tmp;}// 插入heap->arr[heap->size++] = val;AdjustUp(heap->arr, heap->size - 1);
}
void Swap(datatype* x, datatype* y) {int tmp = *x;*x = *y;*y = tmp;
}
// logn
void AdjustUp(datatype* arr, int child) {int parent = (child - 1) / 2;while (child > 0 && arr[child] < arr[parent]) {Swap(&arr[child], &arr[parent]); // upchild = parent;parent = (child - 1) / 2;}
}

如果将while循环的条件arr[child] < arr[parent]改成大于arr[child] > arr[parent],则是调整构建最大堆。

Heap heap;
HeapInit(&heap);
// 建堆 nlogn
HeapPush(&heap, 67864);
HeapPush(&heap, 7432);
HeapPush(&heap, 854312);
HeapPush(&heap, 909876);
HeapPush(&heap, 8765);
HeapPush(&heap, 2345678);
HeapPush(&heap, 2563);
HeapPush(&heap, 12676);
HeapPush(&heap, 6543);
HeapPush(&heap, 2167);
for (int i = 0; i < heap.size; i++) {printf("%d ", heap.arr[i]);
}
HeapDestroy(&heap);

在这里插入图片描述

2.3.2 删除

删除堆元素,只能删除堆顶元素,这是合理的规定,其它诸如任意删除、删除最后一个元素的操作对堆而言都是没有意义的。

如果是直接删除堆顶元素,数组剩下的元素不再构成堆,所以不能这么做。还是以最小堆为例,(1)标准的实现是:将堆顶元素与数组最后一个元素进行交换,即最小的数与较大的数交换,接着删除最后一个元素,然后再向下调整,目的是将堆顶元素往下沉,重新构建最小堆;(2)向下调整的思路:从左右孩子节点中选出最小的,这个孩子节点比父节点小就做交换,重复这个操作。

void HeapPop(Heap* heap) {assert(heap && heap->arr && heap->size > 0);Swap(&heap->arr[0], &heap->arr[--heap->size]);AdjustDown(heap->arr, heap->size, 0);
}
void Swap(datatype* x, datatype* y) {int tmp = *x;*x = *y;*y = tmp;
}
// logn
void AdjustDown(datatype* arr, int size, int parent) {int child = parent * 2 + 1; // left or right child = child + 1 < size && arr[child + 1] < arr[child]? ++child : chi// child<parent调整为小堆,child>parent调整为大堆 while (child < size && arr[child] < arr[parent]) {Swap(&arr[child], &arr[parent]); // downparent = child;child = parent * 2 + 1; // left or rightchild = child+1 < size && arr[child+1] < arr[child]? ++child : child;}
}

如果将arr[child + 1] < arr[child]改成arr[child + 1] > arr[child],以及while循环的条件arr[child] < arr[parent]改成大于arr[child] > arr[parent],则是调整构建最大堆。

Heap heap;
HeapInit(&heap);
// 建堆 nlogn
HeapPush(&heap, 67864);
HeapPush(&heap, 7432);
HeapPush(&heap, 854312);
HeapPush(&heap, 909876);
HeapPush(&heap, 8765);
HeapPush(&heap, 2345678);
HeapPush(&heap, 2563);
HeapPush(&heap, 12676);
HeapPush(&heap, 6543);
HeapPush(&heap, 2167);
while (!HeapEmpty(&heap)) {printf("%d ", HeapTop(&heap));HeapPop(&heap);
}
HeapDestroy(&heap);

在这里插入图片描述
这其实相当于排了个序,时间复杂度为nlogn。不过由于还有插入操作的时间复杂度nlogn,所以整体时间复杂度为2n*2logn。

另外这也能解决TopK问题:

int k = 3; 
while (k--) {printf("%d ", HeapTop(&heap));HeapPop(&heap);
}

3. 堆排序

前面借助堆基本操作Top和Pop也能做到堆排序,不过却比较麻烦,因为需要实现堆的基本操作。这里所指的堆排序是指,直接将数组构建成堆然后排序,不包含建堆的时间则堆排序的时间复杂度为nlogn。
在这里插入图片描述

// 排升序则构建大堆,排降序则构建小堆
void HeapSort(datatype* arr, int size) {// 建堆 O(nlogn) //for (int i = 1; i < size; ++i) {//	AdjustUp(arr, i);  //}// 建堆 O(n) 并非是nlognfor (int i = (size - 1 - 1) / 2; i >= 0; --i) {AdjustDown(arr, size, i);}// 排序 O(nlogn)for (int end = size - 1; end > 0; --end) {Swap(&arr[0], &arr[end]);AdjustDown(arr, end, 0);}
}

利用向下调整建堆的时间复杂度为O(n)的原因是,最后一层不需要向下调整,直接从倒数第二层开始向下调整,这节省了很多时间复杂度,毕竟最后一层的节点占了堆节点总数一半。每一层的节点数量越多,向下调整次数越少;每一层的节点数量越少,向下调整的次数才越多。

而利用向上调整构建堆,从最后一层的节点往上,则会耗费较多时间复杂度,因为最后一层也需要向上调整。

4. TopK

如果数据量太大,比如一千万个数据,以int来算则是四千万字节,差不多相当于40G内存,如果还是按以前那样将所有数据插入堆中求TopK显然是不可能的。

void PrintTopK(const char* file, int k) {// 文件FILE* fr = fopen(file, "r");if (fr == NULL) {perror("PrintTopK fopen failed.");exit(-1);}// 大小为k的堆datatype* minheap = (datatype*)malloc(sizeof(datatype) * k);if (minheap == NULL) {perror("PrintTopK malloc failed.");exit(-1);}// 从文件读取前k个建小堆for (int i = 0; i < k; i++) {fscanf(fr, "%d", &minheap[i]);AdjustUp(minheap, i); // child<parent}// 从文件挨个读取,寻找TopKint val = 0;while (fscanf(fr, "%d", &val) != EOF) {if (val > *minheap) {*minheap = val; // 大的往下沉 child<parentAdjustDown(minheap, k, 0); }}// 打印TopKfor (int i = 0; i < k; i++) {printf("%d ", minheap[i]);}printf("\n");free(minheap);minheap = NULL;fclose(fr);
}

5. 所有代码

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>// 堆结构
typedef int datatype;
typedef struct Heap {datatype* arr;int size;int capacity;
} Heap;void HeapInit(Heap* heap);
void HeapDestroy(Heap* heap);void HeapPush(Heap* heap, datatype val);
void HeapPop(Heap* heap);
// 插入或删除时,堆向上、向下调整
void Swap(datatype* x, datatype* y);
void AdjustUp(datatype* arr, int child);
void AdjustDown(datatype* arr, int size, int parent);datatype HeapTop(Heap* heap);
size_t HeapSize(Heap* heap);
bool HeapEmpty(Heap* heap);// 堆排序、求TopK
void HeapSort(datatype* arr, int size);
void PrintTopK(const char* file, int k);
#define _CRT_SECURE_NO_WARNINGS 1#include "Heap.h"void HeapInit(Heap* heap) {assert(heap);heap->arr = NULL;heap->size = heap->capacity = 0;
}void HeapDestroy(Heap* heap) {assert(heap);free(heap->arr);heap->arr = NULL;heap->size = heap->capacity = 0;
}void HeapPush(Heap* heap, datatype val) {assert(heap);if (heap->size == heap->capacity) {heap->capacity = heap->capacity == 0 ? 10 : heap->capacity * 2;datatype* tmp = realloc(heap->arr, sizeof(datatype) * heap->capacity);if (tmp == NULL) {perror("HeapPush malloc failed.");exit(-1);}heap->arr = tmp;}heap->arr[heap->size++] = val;AdjustUp(heap->arr, heap->size - 1);
}void HeapPop(Heap* heap) {assert(heap && heap->arr && heap->size > 0);Swap(&heap->arr[0], &heap->arr[--heap->size]);AdjustDown(heap->arr, heap->size, 0);
}datatype HeapTop(Heap* heap) {assert(heap && heap->arr && heap->size > 0);return heap->arr[0];
}size_t HeapSize(Heap* heap) {assert(heap);return heap->size;
}bool HeapEmpty(Heap* heap) {assert(heap);return heap->size == 0;
}void Swap(datatype* x, datatype* y) {int tmp = *x;*x = *y;*y = tmp;
}void AdjustUp(datatype* arr, int child) {int parent = (child - 1) / 2;while (child > 0 && arr[child] < arr[parent]) {Swap(&arr[child], &arr[parent]); // upchild = parent;parent = (child - 1) / 2;}
}void AdjustDown(datatype* arr, int size, int parent) {int child = parent * 2 + 1; //child+1为右孩子节点child = child + 1 < size && arr[child + 1] < arr[child]? ++child : child;// child<parent调整为小堆,child>parent调整为大堆 while (child < size && arr[child] < arr[parent]) {Swap(&arr[child], &arr[parent]); // downparent = child;child = parent * 2 + 1; // left or rightchild = child+1 < size && arr[child+1] < arr[child]? ++child : child;}
}// 升序构建大堆,降序构建小堆
void HeapSort(datatype* arr, int size) {// 建堆 O(nlogn) //for (int i = 1; i < size; ++i) {//	AdjustUp(arr, i);  //}// 建堆 O(n) for (int i = (size - 1 - 1) / 2; i >= 0; --i) {AdjustDown(arr, size, i);}// 排序 O(nlogn)for (int end = size - 1; end > 0; --end) {Swap(&arr[0], &arr[end]);AdjustDown(arr, end, 0);}
}void PrintTopK(const char* file, int k) {// 文件FILE* fr = fopen(file, "r");if (fr == NULL) {perror("PrintTopK fopen failed.");exit(-1);}// 大小为k的堆datatype* minheap = (datatype*)malloc(sizeof(datatype) * k);if (minheap == NULL) {perror("PrintTopK malloc failed.");exit(-1);}// 从文件读取前k个建小堆for (int i = 0; i < k; i++) {fscanf(fr, "%d", &minheap[i]);AdjustUp(minheap, i); // child<parent}// 从文件挨个读取,寻找TopKint val = 0;while (fscanf(fr, "%d", &val) != EOF) {if (val > *minheap) {*minheap = val; // 大的往下沉 child<parentAdjustDown(minheap, k, 0); }}// 打印TopKfor (int i = 0; i < k; i++) {printf("%d ", minheap[i]);}printf("\n");free(minheap);minheap = NULL;fclose(fr);
}
#define _CRT_SECURE_NO_WARNINGS #include "Heap.h"
#include <time.h>static void CreateNData();int main() {//Heap heap;//HeapInit(&heap); 建堆 nlogn//HeapPush(&heap, 67864);//HeapPush(&heap, 7432);//HeapPush(&heap, 854312);//HeapPush(&heap, 909876);//HeapPush(&heap, 8765);//HeapPush(&heap, 2345678);//HeapPush(&heap, 2563);//HeapPush(&heap, 12676);//HeapPush(&heap, 6543);//HeapPush(&heap, 2167);//printf("%zd\n", HeapSize(&heap));//for (int i = 0; i < heap.size; i++) {//	printf("%d ", heap.arr[i]);//}//printf("\n");// top k//int k = 3; //while (k--) {//	printf("%d ", HeapTop(&heap));//	HeapPop(&heap);//}//printf("\n");// Push nlogn + 排序 nlogn =  O(2nlogn)//while (!HeapEmpty(&heap)) {//	printf("%d ", HeapTop(&heap));//	HeapPop(&heap);//}//HeapDestroy(&heap);//printf("\n");// 堆排序//int arr[] = { 67864,7432,854312,909876,8765,2345678,2563,12676,6543,2167 };//HeapSort(arr, sizeof arr / sizeof arr[0]);//for (int i = 0; i < sizeof arr / sizeof arr[0]; i++) {//	printf("%d ", arr[i]);//}// 大量数据下的TopK//CreateNData(); PrintTopK("data.txt", 6);return 0;
}static void CreateNData() {int n = 100000;srand((unsigned int)time(0));FILE* fw = fopen("data.txt", "w");if (fw == NULL) {perror("CreateNData fopen failed.");exit(-1);}for (int i = 0; i < n; i++) {fprintf(fw, "%d\n", rand() % n);}fclose(fw);
}

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

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

相关文章

微信自动预约小程序开发指南:从小白到专家

在数字化时代&#xff0c;预约小程序已成为各类服务行业的必备工具。本文将指导你从零开始&#xff0c;通过第三方小程序制作平台&#xff0c;顺利开发出一款具有预约功能的实用小程序。 第一步&#xff1a;注册登录第三方小程序制作平台 首先&#xff0c;你需要选择一个适合你…

python flask 魔术方法

魔术方法作用_init_对象的初始化方法_class_返回对象所属的类_module_返回类所在的模块_mro_返回类的调用顺序&#xff0c;可以找到其父类&#xff08;用于找父类&#xff09;_base_获取类的直接父类&#xff08;用于找父类&#xff09;_bases_获取父类的元组&#xff0c;按它们…

FINN: 使用神经网络对网络流进行指纹识别

文章信息 论文题目&#xff1a;FINN: Fingerprinting Network Flows using Neural Networks 期刊&#xff08;会议&#xff09;&#xff1a;Annual Computer Security Applications Conference 时间&#xff1a;2021 级别&#xff1a;CCF B 文章链接&#xff1a;https://dl.ac…

可解释性AI(XAI)的主要实现方法和研究方向

文章目录 每日一句正能量前言主要实现方法可解释模型模型可解释技术 未来研究方向后记 每日一句正能量 当你还不能对自己说今天学到了什么东西时&#xff0c;你就不要去睡觉。 前言 随着人工智能的迅速发展&#xff0c;越来越多的决策和任务交给了AI系统来完成。然而&#xff…

【开源】SpringBoot框架开发高校学生管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 学生管理模块2.2 学院课程模块2.3 学生选课模块2.4 成绩管理模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 学生表3.2.2 学院课程表3.2.3 学生选课表3.2.4 学生成绩表 四、系统展示五、核心代码5.1 查询课程5.2 新…

如何正确理解和获取S参数

S参数是网络参数&#xff0c;定义了反射波和入射波之间的关系&#xff0c;给定频率的S参数矩阵指定端口反射波b的矢量相对于端口入射波a的矢量&#xff0c;如下所示&#xff1a; bS∙a 在此基础上&#xff0c;如下图所示&#xff0c;为一个常见的双端口网络拓扑图&#xff1a;…

部署篇 | MatrixOne与MySQL全面对比

MatrixOne是一款高度兼容MySQL语法的HTAP数据库&#xff0c;在大部分场景下可以直接实现对MySQL的替换。 作为一款开源数据库&#xff0c;MatrixOne 选用对开发者友好的 Apache-2.0 License&#xff0c;支持在主流的 Linux 和 MacOS 系统中直接进行物理部署。在部署方式上&…

JVM 性能调优 - 参数基础(2)

查看 JDK 版本 $ java -version java version "1.8.0_151" Java(TM) SE Runtime Environment (build 1.8.0_151-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode) 查看 Java 帮助文档 $ java -help 用法: java [-options] class [args...] …

Redis 双写一致性

问题&#xff1a;redis 作为缓存&#xff0c;mysql 的数据如何与 redis 进行同步呢&#xff1f;&#xff08;双写一致性&#xff09; 双写一致性是指当修改了数据库的数据也要同时更新缓存的数据&#xff0c;缓存和数据库的数据要保持一致。 读操作&#xff1a;缓存命中&…

鸿蒙内核框架

1 内核概述 内核简介 用户最常见到并与之交互的操作系统界面&#xff0c;其实只是操作系统最外面的一层。操作系统最重要的任务&#xff0c;包括管理硬件设备&#xff0c;分配系统资源等&#xff0c;我们称之为操作系统内在最重要的核心功能。而实现这些核心功能的操作系统模…

【51单片机】LED的三个基本项目(LED点亮&LED闪烁&LED流水灯)(3)

前言 大家好吖&#xff0c;欢迎来到 YY 滴单片机系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过单片机的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的…

大型装备制造企业案例分享——通过CRM系统管理全球业务

本期&#xff0c;小Z为大家带来的CRM管理系统客户案例是某大型装备制造企业运用Zoho CRM管理全球业务的过程分享。该企业是创业板上市公司&#xff0c;业务遍及100多个国家和地区&#xff0c;合作伙伴超百位&#xff0c;拥有覆盖全球的销售和服务网络。截止目前&#xff0c;相继…