堆的结构及函数接口、堆排序,TopK

news2024/11/24 14:24:14

本篇内容涉及到二叉树的概念及性质,可参考文章 树和二叉树的概念及性质

文章目录

  • 一、堆的概念
  • 二、堆的存储结构
  • 三、堆的函数接口
    • 1. 初始化及销毁
    • 2. 打印函数
    • 3. 堆的插入
    • 4. 堆的删除
    • 5. 取堆顶、判空、数据个数
  • 四、建堆算法和时间复杂度
    • 1. 向上调整建堆
    • 2. 向下调整建堆
  • 五、堆排序和 TopK 问题

一、堆的概念

堆是一颗 完全二叉树,并且数据满足如下性质

  • 如果树中 所有父节点 的值都 大于等于 子节点的值,称作 大堆(最大堆、大根堆)
  • 如果树中 所有父节点 的值都 小于等于 子节点的值,称作 小堆(最小堆、小根堆)

在这里插入图片描述

二、堆的存储结构

在上篇 树和二叉树的概念及性质 的最后,介绍了 完全二叉树的编号,以及 通过某个节点的编号可以轻松的找到该节点的父节点和孩子节点,因此可以 根据编号作为下标数组来存储堆

//堆的数据类型
typedef int HeapDataType;

//堆的结构
typedef struct Heap 
{
	HeapDataType* data;
	int size;	//存储的数据个数
	int capacity;	//当前的容量
}Heap;

在这里插入图片描述

三、堆的函数接口

1. 初始化及销毁

创建一个堆之后,堆结构中的成员变量存储的都是一些随机值,所以需要对其进行初始化,这里采用 初始化时不分配空间 的方式,也可以在初始化时就为其分配一些空间

初始化函数如下:

void HeapInit(Heap* pHp)
{
	//pHp 不能为空指针
  	assert(pHp);

	//初始化
	pHp->data = NULL;
	pHp->size = pHp->capacity = 0;
}

在堆中:存储数据的空间是动态开辟的,不使用时应手动释放

销毁函数如下:

void HeapDestroy(Heap* pHp)
{
	//pHp 不能为空指针
    assert(pHp);

    free(pHp->data);
    pHp->data = NULL;
    pHp->size = pHp->capacity = 0;
}

2. 打印函数

为了验证堆的插入、删除等得到的结果是否正确,提供打印堆的函数,这里数据类型以 int 为例,当读者采用的类型不同时,自行更改该函数即可

打印函数如下:

void HeapPrint(Heap* pHp)
{
    assert(pHp);

    for(int i = 0; i < pHp->size; ++i)
    {
        printf("%d ", pHp->data[i]);
    }
    printf("\n");
}

3. 堆的插入

由于 堆是一颗完全二叉树,因此只能在 最后一个编号之后插入数据,以大堆为例

插入的值 小于 父节点的值 时,插入之后的完全二叉树 还是大堆
在这里插入图片描述
插入的值 大于 父节点的值 时,插入之后的完全二叉树就 不是大堆 了,此时便 需要将结构调整为大堆
在这里插入图片描述
调整方法:将插入的结点值和父节点值交换,交换之后,如果该值还大于父节点的值,则 继续和父节点交换,直到 交换后的结点值小于等于父节点值该节点已经是根节点

调整过程中,所需要 判断的所有节点都是插入节点的祖先,因此 称作向上调整
在这里插入图片描述

插入函数如下:

//交换值
void Swap(HeapDataType* p1, HeapDataType* p2)
{
    HeapDataType tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

//向上调整,调整时需要存储堆的数组、调整对象编号
void AdjustUp(HeapDataType* array, int child)
{
    assert(array);

    //计算孩子的父节点
    int parent = (child - 1) / 2;

    //交换到根节点后,便停止交换
    while(child > 0)
    {
        //如果孩子节点值大于父节点值,则需要交换父子节点的值,否则调整完成
        //将这里的 大于 改成 小于,就是小堆的的向上调整
        if(array[child] > array[parent]) 
        {
            Swap(&array[child], &array[parent]);
            child = parent;	//父子节点的值已经交换,需要更新孩子的指向
            parent = (child - 1) / 2;	//计算孩子的父节点
        }
        else 
        {
            break;
        }
    }
}

//插入
void HeapPush(Heap* pHp, HeapDataType x)
{
    assert(pHp);

    //扩容
    if(pHp->size == pHp->capacity)
    {
        int newCapacity = pHp->capacity == 0 ? 4 : 2 * pHp->capacity;
        HeapDataType* tmp = (HeapDataType*)realloc(pHp->data, sizeof(HeapDataType) * newCapacity);
        if(tmp == NULL)
        {
            perror("realloc");
            exit(-1);
        }

        pHp->data = tmp;
        pHp->capacity = newCapacity;
    }

    //插入数据
    pHp->data[pHp->size] = x;
    pHp->size++;

    //向上调整
    AdjustUp(pHp->data, pHp->size - 1);
}

4. 堆的删除

堆只会 删除堆顶的数据,删除其他位置的数据意义不大,以大堆为例

由于 数组尾删的效率很高,因此 为了较易删除堆顶的数据,分三步进行

第一步:将 堆顶的数据最后一个数据 交换
在这里插入图片描述
第二步:删除最后一个数据
在这里插入图片描述
第三步:将删除后的完全二叉树 调整为大堆
在这里插入图片描述

调整方法:将较大的孩子结点的值和堆顶节点值交换,交换之后,如果左右孩子中的较大值还大于该值,则 继续将较大的孩子结点的值和该节点值交换,直到 左右孩子的值小于等于交换后的结点值该节点已经是叶节点

调整过程中,所需要 判断的所有节点都是堆顶节点的子孙,因此 称作向下调整
在这里插入图片描述
删除函数如下:

//向下调整,调整时需要存储堆的数组、调整对象编号、堆的数据个数
void AdjustDown(HeapDataType* array, int parent, int n)
{
    assert(array);

    //假设左孩子为需要交换的孩子
    int child = parent * 2 + 1;

	//交换到叶节点,便停止交换
	//完全二叉树中,左孩子不存在,右孩子也就不存在了
    while(child < n)
    {
        //如果假设错误,则需要更新 child 为右孩子
        //需要注意:右孩子可能不存在
        //将这里和下面 if 语句的 大于 改成 小于,就是小堆的向下调整
        if(child + 1 < n && array[child + 1] > array[child]) 
        {
            ++child;
        }

        //如果较大的子节点的值大于父节点的值,则需要交换,否则调整完成
        //将这里和上面 if 语句中的 大于 改成 小于,就是小堆的向下调整
        if(array[child] > array[parent])
        {
            Swap(&array[child], &array[parent]);
            parent = child;	//父子节点的值已经交换,需要更新双亲的指向
            child = parent * 2 + 1;	//假设左孩子为需要交换的孩子
        }
        else 
        {
            break;
        }
    }
}

void HeapPop(Heap* pHp)
{
    assert(pHp);
    assert(!HeapEmpty(pHp));

    //第一步:交换堆顶和最后一个数据
    Swap(&pHp->data[0], &pHp->data[pHp->size - 1]);

    //第二步删除最后一个数据
    pHp->size--;

    //第三步:向下调整
    AdjustDown(pHp->data, 0, pHp->size);
}

5. 取堆顶、判空、数据个数

这些函数较为简单,就不做分析了

函数如下:

//取堆顶
HeapDataType HeapTop(Heap* pHp)
{
    assert(pHp);
    assert(!HeapEmpty(pHp));

    return pHp->data[0];
}

//判空
bool HeapEmpty(Heap* pHp)
{
    assert(pHp);

    return pHp->size == 0;
}

//数据个数
size_t HeapSize(Heap* pHp)
{
    assert(pHp);

    return pHp->size;
}

四、建堆算法和时间复杂度

数组 array { 25, 15, 51, 30, 20, 19 },交换数组元素使之变为堆,要求空间复杂度为 O(1)

1. 向上调整建堆

将数组的元素看做一棵完全二叉树
在这里插入图片描述
在堆的插入中,插入数据之前,数组本身是堆,当插入的数据大于父节点时,通过向上调整,便可以将数组调整为堆

为了可以使用向上调整算法,需要满足调整之前数组本来就是堆

当数组只有一个元素时,可以将其看做一个堆,于是便可以不断的对新数据进行向上调整,最终就可以将整个数组调整为堆
在这里插入图片描述

调整结果:
在这里插入图片描述

向上调整函数 已经在 堆的插入给出建堆循环 如下:

//数组和数组大小
int array[] = { 25, 15, 51, 30, 20, 19 };
int len = sizeof(array)/sizeof(array[0]);

//向上调整建堆
//时间复杂度:O(N * logN)
for(int i = 1; i < len; ++i)
{
	AdjustUp(array, i);	//对新数据进行向上调整
}

向上调整建堆的时间复杂度:
在这里插入图片描述
对于高度为 h 的堆,总节点数 N = 2h - 1,总调整次数:

F(h) = 21 * 1 + 22 * 2 + … + 2h - 1 * (h - 1)
小于 2 * 2h - 1 * (h - 1) = 2h * (h - 1) = (N + 1) * (log2(N + 1) - 1)

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

2. 向下调整建堆

将数组的数据看做一棵完全二叉树:
在这里插入图片描述
在堆的删除中:先将堆顶和最后一个数据交换,然后删除最后一个数据,此时堆顶的左子树和右子树均是堆,通过向下调整,便可以将数组调整为堆

为了可以使用向下调整算法,需要满足调整的节点的左子树和右子树均是堆

显然数组的第一个数据的左子树和右子树不是堆,此时并不能从第一个开始向下调整,而是需要从最后一个节点开始,从后往前对每一个节点向下调整

由于数组只有一个元素时,可以将其看做一个堆,因此可以 从最后一个分支节点开始,在完全二叉树中,最后一个分支节点就是最后一个节点的父节点,最后便可以将数组调整为堆
在这里插入图片描述

调整结果:
在这里插入图片描述

向下调整函数 已经在 堆的删除给出建堆循环 如下:

//数组和数组大小
int array[] = { 25, 15, 51, 30, 20, 19 };
int len = sizeof(array)/sizeof(array[0]);

//向下调整建堆
//时间复杂度O(N)
//从最后一个分支节点开始,从后往前
for(int i = (len - 1 - 1) / 2; i >= 0; --i)
{
	AdjustDown(array, i, len);	//向下调整为堆,为后续向下调整做准备
}

时间复杂度
在这里插入图片描述
对于高度为 h 的堆,总节点数 N = 2h - 1,总调整次数:

F(h) = 20 * (h - 1) + 21 * (h - 2) + … + 2h - 3 * 2 + 2h - 2 * 1
2 * F(h) = 21 * (h - 1) + 22 * (h - 2) + … + 2h - 2 * 2 + 2h - 1 * 1

2 * F(h) - F(h) 错位相减得:
F(h) = - 20 * (h - 1) + 21 * 1 + … + 2h - 3 * 1 + 2h - 2 * 1 + 2h - 1 * 1
F(h) = 2h - 2 - (h - 1) = 2h - 1 - h = N - log2N

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

五、堆排序和 TopK 问题

对数组 array { 25, 15, 51, 30, 20, 19 } 进行原地排序,升序建大堆,降序建小堆

在这里插入图片描述
堆排序时间复杂度:O(N * log2N),计算方法和向上调整建堆相似

堆排序函数如下:

void HeapSort(int* array, int arrayLen)
{
    //升序建大堆
    for(int i = (arrayLen - 1 - 1) / 2; i >= 0; --i)
    {
        AdjustDown(array, i, arrayLen);
    }

	//每一次将堆顶和最后一个数据交换,并且不将最后一个数据看做堆的数据,进行向下调整为堆
	//如果升序建小堆,出的数据没有存放的地方
    int end = arrayLen - 1;
    while(end > 0)
    {
        Swap(&array[0], &array[end]);
        AdjustDown(array, 0, end);
        --end;
    }
}

排序过程:
在这里插入图片描述
选取数据中前 K 个最大数据或最小数据,一般数据量都很大,无法存储在内存中

选取前 K 个最大数据,建 K 个数据的小堆,选取前 K 个最小数据,建 K 个数据的大堆

时间复杂度:K + (N - K) * log2K -> O(N * log2K)
空间复杂度:O(K)

//用于测试
void arrayPrint(int* array, int arrayLen)
{
    for(int i = 0; i < arrayLen; ++i)
    {
        printf("%d ", array[i]);
    }
    printf("\n");
}

void HeapTest5()
{
    int n = 10000;  //数据个数
    int k = 5;  //选取的 K 个数

    //设置随机数种子
    srand((unsigned)time(NULL));

    FILE* fin = fopen("data.txt", "w");
    if(fin == NULL)
    {
        perror("fopen");
        exit(-1);
    }

    //制造数据
    for(int i = 1; i <= n; ++i)
    {
        int val = rand() % 100;
        fprintf(fin, "%d ", val);

        //制造 2k 个较大的数
        if(i % (n / k / 2) == 0)
        {
            fprintf(fin, "%d ", i * 100 + val);
        }
    }

    fclose(fin);

	//创建 k 个空间,用来存储堆
    int* array = (int*)malloc(sizeof(int) * k);
    if(array == NULL)
    {
        perror("malloc");
        exit(-1);
    }

    FILE* fout = fopen("data.txt", "r");
    if(fout == NULL)
    {
        perror("fopen");
        exit(-1);
    }

    //读取前 k 个数据
    for(int i = 0; i < k; ++i)
    {
        fscanf(fout, "%d", &array[i]);
    }

    //arrayPrint(array, k);

    //建小堆
    for(int i = (k - 1 - 1) / 2; i >= 0; --i)
    {
        AdjustDown(array, i, k); 
    }

    //arrayPrint(array, k);
    
    //遍历数据
    int val = 0;
    while(fscanf(fout, "%d", &val) != EOF)
    {
    	//比堆顶大就替换堆顶,然后调整为小堆
        if(array[0] < val)
        {
            array[0] = val;
            AdjustDown(array, 0, k);
        }
    }

    arrayPrint(array, k);

    fclose(fout);
}

int main()
{
    HeapTest5();

    return 0;
}

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

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

相关文章

CTFshow--web--红包题第二弹

查看源代码&#xff0c;按注释提示&#xff0c;构造参数试试?cmdaa<?php #error_reporting(0); ?> <html lang"zh-CN"><head><meta http-equiv"Content-Type" content"text/html; charsetUTF-8" /><meta name&quo…

MATLAB绘制爱心曲线并导出

MATLAB绘制爱心曲线并导出 爱心曲线的表达式&#xff1a; f(x)x2/3e3(π−x2)1/2sin(aπx)f(x)x^{2/3}\frac e 3(\pi-x^2)^{1/2}sin(a\pi x) f(x)x2/33e​(π−x2)1/2sin(aπx) f (x,a)x.^2.^(1/3)exp(1)/3*(pi-x.^2).^(1/2).*sin(a*pi*x); h figure(color,[1 1 1]); set(g…

应用系统与钉钉集成案例及操作步骤

1、准备钉钉应用 1.1、注册钉钉账号 作为钉钉的企业管理员&#xff0c;首先登录钉钉官网&#xff0c;注册一个钉钉账号。 如果已经有账号&#xff0c;则直接使用即可。 钉钉官网&#xff1a;https://www.dingtalk.com/ 1.2、开通企业团队 企业管理员使用账号登录钉钉。 如…

如何限制docker容器使用内存大小

本文介绍如何通过docker运行参数配置限制docker容器可以使用的内存上限。docker容器默认可以使用全部宿主机的所有内存和 swap 分区&#xff0c;比如宿主机的内存是32G&#xff0c;则运行一个docker容器最多可以分配到32G内存&#xff0c;如果启用了多个docker容器&#xff0c;…

CSS实现文本显示两行

效果图 text-overflow: ellipsis;display: -webkit-box;-webkit-line-clamp: 2;-webkit-box-orient: vertical;display: -moz-box;-moz-line-clamp: 2;-moz-box-orient: vertical;overflow-wrap: break-word;word-break: break-all;white-space: normal;overflow: hidden;text-…

SAP ADM100-2.2 SAP系统开启过程中的配置文件

SAP系统的每个实例需要的数据都在文件系统中,包括所有实例都需要访问的全局数据(Globally)和个别实例需要访问的数据。在文件系统汇总,实例需要的数据被包含在usr/sap目录,在这里被进一步组织到子目录。 【注意】:业务数据和相关数据被存储在数据库中,数据库根据不同的制…

【GD32F427开发板试用】三、USB转CAN功能开发与试用总结

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;chenjie 【GD32F427开发板试用】一、环境搭建与freertos移植 【GD32F427开发板试用】二、USB库移植与双USB CDC-ACM功能开发 【GD32F427开发板…

【C++】IO流

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《吃透西嘎嘎》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;C语言的输…

04 微服务调用组件Feign

JAVA 项目中如何实现接口调用&#xff1f; 1&#xff09;Httpclient HttpClient 是 Apache Jakarta Common 下的子项目&#xff0c;用来提供高效的、最新的、功能丰富的支持 Http 协议的客户端编程工具包&#xff0c;并且它支持 HTTP 协议最新版本和建议。HttpClient 相比传统…

linux下Jenkins的安装、部署、启动(完整教程)

linux下Jenkins的安装、部署、启动(完整教程) 一、安装环境 Linux系统Centos 7 二、安装步骤 1、安装jdk8 2、安装jenkins 首先依次如下三个命令&#xff1a; 2.1 导入镜像 sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.…

内网渗透之中间人欺骗攻击

ARP攻击协议简介ARP全称为Address Resolution Protocol&#xff0c;即地址解析协议&#xff0c;它是一个根据IP地址获取物理地址的TCP/IP协议&#xff0c;主机发送信息时将包含目标IP地址的ARP请求广播到网络上的所有主机&#xff0c;并接收返回消息&#xff0c;以此确定目标的…

手把手教你QT打包(Windows)

第一步、切换工程版本 在这一步可以直接在QT的项目页面就可以完成 在这里切换完成之后建议先运行一下&#xff0c;确保没有问题我们再进行下一步 第二步、找到对应的文件夹和编译工具进行编译 我们在使用的过成功&#xff0c;会发现点击工程中的运行就可以执行&#xff0c;但是…

MySQL详解(二)——基础 2.0

5. 完整性约束 完整性约束是为了表的数据的正确性&#xff01;如果数据不正确&#xff0c;那么一开始就不能添加到表中。 5.1 主键 当某一列添加了主键约束后&#xff0c;那么这一列的数据就不能重复出现。这样每行记录中其主键列的值就是这一行的唯一标识。例如学生的学号可…

【Linux】基础IO --- 内核级和用户级缓冲区、磁盘结构、磁盘的分治管理、block group块组剖析…

出身寒微&#xff0c;不是耻辱。能屈能伸&#xff0c;方为丈夫。 文章目录一、缓冲区&#xff08;语言级&#xff1a;IO流缓冲&#xff0c;内核级&#xff1a;块缓冲&#xff09;1.观察一个现象2.理解缓冲区存在的意义&#xff08;节省进程IO数据的时间&#xff09;3.语言级缓冲…

commonjs vs ES module in Node.js

在现代软件开发中&#xff0c;模块将软件代码组织成独立的块&#xff0c;这些块共同构成了更大、更复杂的应用程序。 在浏览器 JavaScript 生态系统中&#xff0c;JavaScript 模块的使用依赖于import和export语句&#xff1b;这些语句分别加载和导出 EMCAScript 模块&#xff…

Java SSM (springboot+mybatis)美食菜谱分享平台系统设计和实现以及论文报告

Java SSM (springbootmybatis)美食菜谱分享平台系统设计和实现以及论文报告 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收…

DNS的解析,查询,调度原理是什么?什么是DNS劫持,污染?如何监控?

DNS的核心工作就是将域名翻译成计算机IP地址, 它是基于UDP协议实现的&#xff0c;本文将具体阐述DNS相关的概念&#xff0c;解析&#xff0c;调度原理&#xff08;负载均衡和区域调度&#xff09;等DNS相关的所有知识点DNS简介域名系统并不像电话号码通讯录那么简单&#xff0c…

LeetCode 91. 解码方法 120. 三角形最小路径和 97. 交错字符串 131. 分割回文串 132. 分割回文串 II

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛岛屿&#xff0c;本文带来的是LeetCode 91. 解码方法 120. 三角形最小路径和 97. 交错字符串 131. 分割回文串 132. 分割回文串 II &#x1f332;&#x1f332;&#x1f434;&#x1f434; 91. 解码方法…

意图识别和文本分类(六)

一、分类的目的和分类的方法 目标 能够说出项目中进行文本的目的能够说出意图识别的方法能够说出常见的分类的方法 1.1 文本分类的目的 回顾之前的流程&#xff0c;我们可以发现文本分类的目的就是为了进行意图识别 在当前我们的项目的下&#xff0c;我们只有两种意图需要被…

算法刷题打卡第75天:合并两个链表

合并两个链表 难度&#xff1a;中等 给你两个链表 list1 和 list2 &#xff0c;它们包含的元素分别为 n 个和 m 个。 请你将 list1 中下标从 a 到 b 的全部节点都删除&#xff0c;并将 list2 接在被删除节点的位置。 下图中蓝色边和节点展示了操作后的结果&#xff1a; 请你…