【数据结构】堆,堆的实现,堆排序,TOP-K问题

大家好!今天我们来学习数据结构中的堆及其应用

目录

1. 堆的概念及结构

2. 堆的实现

2.1 初始化堆

2.2 销毁堆

2.3 打印堆

2.4 交换函数

2.5 堆的向上调整

2.6 堆的向下调整

2.7 堆的插入

2.8 堆的删除

2.9 取堆顶的数据

2.10 堆的数据个数

2.11 堆的判空

3. 堆的实现的完整代码

3.1 Heap.h

3.2 Heap.c

3.3 Test.c

4. 建堆的时间复杂度

4.1 向上调整建堆

4.2 向下调整建堆

5. 堆的应用

5.1 堆排序

5.2 TOP-K问题

6. 总结


1. 堆的概念及结构

堆(Heap)是计算机科学中中一类特殊的数据结构,是最高效的优先级队列,堆通常是一个可以被看作一棵完全二叉树数组对象

堆分为最小堆(Min Heap)和 最大堆(Max Heap)和最小堆(Min Heap)。对于最小堆父结点的值小于等于它的子结点的值。对于最大堆父结点的值大于等于它的子结点的值;

堆的性质:

1. 堆中某个结点的值总是不大于或不小于其父结点的值。

2. 堆总是一棵完全二叉树。

3. 小堆的根是整棵树的最小值,大堆的根是整棵树的最大值。

问题:

1. 小堆的底层数组是否升序?

2. 大堆的底层数组是否降序?

2. 堆的实现

我们这里以小根堆为例(大根堆可以在小根堆的基础上稍作修改),下面是要实现堆的接口函数:

//初始化堆
void HeapInit(HP* php);
//销毁堆
void HeapDestroy(HP* php);
//打印堆
void HeapPrint(HP* php);
//交换函数
void Swap(HPDataType* p1, HPDataType* p2);
//堆向上调整算法
void AdjustUp(HPDataType* a, int child);
//堆向下调整算法
void AdjustDown(HPDataType* a, int n, int parent);
//堆的插入
void HeapPush(HP* php, HPDataType x);
//堆的删除
void HeapPop(HP* php);
//取堆顶的数据
HPDataType HeapTop(HP* php);
//堆的数据个数
int HeapSize(HP* php);
//堆的判空
bool HeapEmpty(HP* php);

堆的定义:

typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;

一些简单的接口函数,和我们之前实现的顺序表,链表,栈等数据结构类似。我们在这里就不详细介绍了,这里我们主要介绍堆向上调整算法堆向下调整算法。这两个函数分别会在堆的插入和堆的删除中调用。

2.1 初始化堆

//初始化堆
void HeapInit(HP* php)
{assert(php);php->a = NULL;php->size = 0;php->capacity = 0;
}

2.2 销毁堆

//销毁堆
void HeapDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}

2.3 打印堆

//打印堆
void HeapPrint(HP* php)
{assert(php);for (int i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");
}

2.4 交换函数

//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}

2.5 堆的向上调整

向上调整前提前面的数据是堆

//堆向上调整算法
void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;while (child > 0){if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else {break;}	}
}

2.6 堆的向下调整

向下调整前提左右子树都是小堆或者都是大堆

//堆向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){if (child + 1 < n && a[child + 1] < a[child]) //找出小的那个孩子{++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child; child = parent * 2 + 1;}else{break;}}
}

2.7 堆的插入

向堆中插入一个元素,就是这个元素插入到堆的尾部,因为堆的实际存储结构是一个数组,我们可以将元素放到数组末尾。

但是如果仅仅是将元素插入到数组末尾的话,会破坏堆的结构,我们还需要调用一个向上调整函数,保持堆的结构。

注意:在插入之前,我们需要判断堆的容量是否足够,如果堆的容量已满,需要扩容,扩容时我们在原来的基础上扩2倍

如下图:

先插入一个10到数组的尾上,再进行向上调整算法,直到满足堆。

//堆的插入
void HeapPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc failed");exit(-1);}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}

2.8 堆的删除

删除堆删除堆顶的数据将堆顶的数据根最后一个数据一换然后删除数组最后一个数据,再进行向下调整算法。

//堆的删除
void HeapPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);--php->size;AdjustDown(php->a, php->size, 0);
}

2.9 取堆顶的数据

//取堆顶的数据
HPDataType HeapTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0];
}

2.10 堆的数据个数

//堆的数据个数
int HeapSize(HP* php) 
{assert(php);return php->size;
}

2.11 堆的判空

//堆的判空
bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}

3. 堆的实现的完整代码

3.1 Heap.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;//初始化堆
void HeapInit(HP* php);
//销毁堆
void HeapDestroy(HP* php);
//打印堆
void HeapPrint(HP* php);
//交换函数
void Swap(HPDataType* p1, HPDataType* p2);
//堆向上调整算法
void AdjustUp(HPDataType* a, int child);
//堆向下调整算法
void AdjustDown(HPDataType* a, int n, int parent);
//堆的插入
void HeapPush(HP* php, HPDataType x);
//堆的删除
void HeapPop(HP* php);
//取堆顶的数据
HPDataType HeapTop(HP* php);
//堆的数据个数
int HeapSize(HP* php);
//堆的判空
bool HeapEmpty(HP* php);

3.2 Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"//初始化堆
void HeapInit(HP* php)
{assert(php);php->a = NULL;php->size = 0;php->capacity = 0;
}//销毁堆
void HeapDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}//打印堆
void HeapPrint(HP* php)
{assert(php);for (int i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");
}//交换函数
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}//堆向上调整算法
void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;while (child > 0){if (a[child] < a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else {break;}	}
}//堆向下调整算法
void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){if (child + 1 < n && a[child + 1] < a[child]) //找出小的那个孩子{++child;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);parent = child; child = parent * 2 + 1;}else{break;}}
}//堆的插入
void HeapPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);if (tmp == NULL){perror("realloc failed");exit(-1);}php->a = tmp;php->capacity = newCapacity;}php->a[php->size] = x;php->size++;AdjustUp(php->a, php->size - 1);
}//堆的删除
void HeapPop(HP* php)
{assert(php);assert(php->size > 0);Swap(&php->a[0], &php->a[php->size - 1]);--php->size;AdjustDown(php->a, php->size, 0);
}//取堆顶的数据
HPDataType HeapTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0];
}//堆的数据个数
int HeapSize(HP* php) 
{assert(php);return php->size;
}//堆的判空
bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}

3.3 Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"int main()
{int a[] = { 2,3,5,7,4,6,8};HP hp;HeapInit(&hp);for (int i = 0; i < sizeof(a) / sizeof(int); i++){HeapPush(&hp, a[i]);}HeapPrint(&hp);int sz = HeapSize(&hp);printf("堆中元素个数为%d个\n", sz);while (!HeapEmpty(&hp)){printf("%d ", HeapTop(&hp));HeapPop(&hp);}HeapDestroy(&hp);return 0;
}

4. 建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果):

4.1 向上调整建堆

因此向上调整建堆的时间复杂度是O(N*logN)

4.2 向下调整建堆

因此,向下调整建堆的时间复杂度为O(N)

因为向下调整建堆的时间复杂度是O(N),小于向上调整建堆的时间复杂度O(N*logN),所以一般情况下,我们都采用向下调整建堆。

5. 堆的应用

5.1 堆排序

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

1. 建堆

升序:建大堆

降序:建小堆

2. 利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

这里我们以大堆为例,通过堆排序进行升序

我们也可以在堆的底层数组中进一步理解堆排序的过程

因为是大堆,所以我们要稍微修改向下调整部分的代码(我们在上面堆的实现中是以小堆为例)

void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}void AdjustDown(HPDataType* a, int n, int parent)
{int child = parent * 2 + 1;while (child < n){if (child + 1 < n && a[child + 1] > a[child]) //找出大的那个孩子{++child;}if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child; child = parent * 2 + 1;}else{break;}}
}
void HeapSort(int* a, int n)
{//从倒数第一个非叶子结点开始调for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);  //向下调整建堆}int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);  //向下调整[0,end]的元素--end;}
}int main()
{int a[] = { 20,17,4,16,5,3 };HeapSort(a, sizeof(a) / sizeof(int));for (int i=0 ; i < sizeof(a) / sizeof(int); i++){printf("%d ", a[i]);}printf("\n");return 0;
}

5.2 TOP-K问题

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

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

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

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

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

实际应用:在1000000个随机数中找出前十个最大的数字

void PrintTopK(int* a, int n, int k)
{int* KMaxHeap = (int*)malloc(sizeof(int) * k);assert(KMaxHeap);for (int i = 0; i < k; i++){KMaxHeap[i] = a[i];}//建小根堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(KMaxHeap, k, i);}//依次比较a数组中剩余的元素for (int i = k; i < n; i++){if (a[i] > KMaxHeap[0]){KMaxHeap[0] = a[i];}AdjustDown(KMaxHeap, k, 0);}//打印for (int i = 0; i < k; i++){printf("%d ", KMaxHeap[i]);}printf("\n");
}void TestTopK()
{int n = 1000000;int* a = (int*)malloc(sizeof(int) * n);srand(time(0));for (int i = 0; i < n; i++){a[i] = rand() % n;}//手动设定10个最大的数a[5] = n + 1;a[1231] = n + 2;a[531] = n + 3;a[5121] = n + 4;a[115] = n + 5;a[2335] = n + 6;a[9999] = n + 7;a[76] = n + 8;a[423] = n + 9;a[3144] = n + 10;PrintTopK(a, n, 10);
}int main()
{TestTopK();return 0;
}

6. 总结

到这里,我们就用学习完了数据结构中堆的相关知识点。有什么问题欢迎在评论区讨论。如果觉得文章有什么不足之处,可以在评论区留言。如果喜欢我的文章,可以点赞收藏哦!

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

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

相关文章

C/C++学习 -- 分组密算法(3DES算法)

1. 3DES算法概述 3DES&#xff08;Triple Data Encryption Standard&#xff09;&#xff0c;又称为TDEA&#xff08;Triple Data Encryption Algorithm&#xff09;&#xff0c;是一种对称加密算法&#xff0c;是DES&#xff08;Data Encryption Standard&#xff09;的加强版…

GPT系列论文解读:GPT-1

GPT系列 GPT&#xff08;Generative Pre-trained Transformer&#xff09;是一系列基于Transformer架构的预训练语言模型&#xff0c;由OpenAI开发。以下是GPT系列的主要模型&#xff1a; GPT&#xff1a;GPT-1是于2018年发布的第一个版本&#xff0c;它使用了12个Transformer…

【STL】用哈希表(桶)封装出unordered_set和unordered_map

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C进阶 ⭐代码仓库&#xff1a;C进阶 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们的支持是我…

C++基础语法(多态)

多态的学习是建立在继承之上的&#xff0c;如果你没有事先了解学习过继承&#xff0c;请去看看笔者写的关于继承的文章&#xff0c;对继承有概念之后&#xff0c;再来学习多态。多态的坑是相当的多&#xff0c;如果未来就业&#xff0c;公司对多态的考察也是让人直呼&#xff1…

【网络模型】OSI七层网络模型、TCP/IP网络模型、键入网址到页面显示的过程、DNS是什么等重点知识汇总

目录 OSI 的七层模型 TCP/IP 网络模型 键入网址到网页显示发生了什么 你知道DNS是什么&#xff1f; OSI 的七层模型 简要概括 应用层&#xff1a;为用户的应用进程提供网络通信服务表示层&#xff1a;处理用户信息的表示问题&#xff0c;数据的编码&#xff0c;压缩和解压…

IDEA踩坑记录:查找用法 找到的不全怎么办

在我跟CC1链的时候&#xff0c;对InvokerTransformer类的transform()方法进行右键查找用法时&#xff0c;本来应该找到org.apache.commons.collections.map包中的TransformedMap类调用了此方法&#xff0c;但是结果确是没找到。 解决办法&#xff1a; 点击右上方的Maven选项&a…

【C++类和对象】:构造函数、析构函数、拷贝构造函数、赋值运算符重载

【C类和对象】&#xff1a;构造函数、析构函数、拷贝构造函数、赋值运算符重载 一、构造函数1.1 概念1.2 性质1.3 实例 二、析构函数2.1 概念2.2 性质2.3 实例 三、拷贝构造函数3.1 概念3.2 性质3.3 实例 四、赋值运算符重载4.1 运算符重载4.2 2 赋值运算符重载1. 赋值运算符重…

如何在 Elasticsearch 中使用 Openai Embedding 进行语义搜索

随着强大的 GPT 模型的出现&#xff0c;文本的语义提取得到了改进。 在本文中&#xff0c;我们将使用嵌入向量在文档中进行搜索&#xff0c;而不是使用关键字进行老式搜索。 什么是嵌入 - embedding&#xff1f; 在深度学习术语中&#xff0c;嵌入是文本或图像等内容的数字表示…

Cannot download sources:IDEA源码无法下载

问题 Swagger的相关包&#xff0c;无法看到注释&#xff1b; 在class文件的页面&#xff0c;点击下载源码&#xff0c;源码下载不了&#xff0c;IDEA报下面的错误。 报错 Cannot download sources Sources not found for: io.swagger.core.v3:swagger-annotations:2.2.9 解决…

(二) gitblit用户使用教程

(一)gitblit安装教程 (二) gitblit用户使用教程 (三) gitblit管理员手册 目录 网页访问git客户端设置推送错误配置查看当前配置 日常使用仓库分组my profile修改上传代码简洁 网页访问 点击Advanced... 点击Accept the Risk and Contiue 初始用户名和密码都是admin,点击login…

IDEA的Maven换源

前言 IDEA是个好东西&#xff0c;但是使用maven项目时可能会让人很难受&#xff0c;要么是非常慢&#xff0c;要么直接下载不了。所以我们需要给IDEA自带maven换源&#xff0c;保证我们的下载速度。 具体操作 打开IDEA安装路径&#xff0c;然后打开下面的文件夹 plugins\m…

【Spring MVC】MVC如何浏览器请求(service方法)

文章目录 1. DispatcherServlet 的 service 方法1.1. processRequest 方法1.2. doService 方法 背景&#xff1a;平时我们学习 MVC 重点关注的时DispatcherServlet 的 doDispatcher 方法&#xff0c;但是在 doDispatcher 方法之前 还有请求处理的前置过程&#xff0c;这个过程…