【数据结构初阶】之堆(C语言实现)

数据结构初阶之堆(C语言实现)

  • 🌏 堆的概念
  • 🌏 堆的模拟实现
    • 🐓 堆的结构和方法接口
    • 🐓 堆的方法的模拟实现
      • 🙊 堆的初始化
      • 🙊 堆的构建
      • 🙊 堆的插入
      • 🙊 向上调整
      • 🙊 堆的删除
      • 🙊 向下调整
      • 🙊 堆的数据的个数
      • 🙊 堆的判空
      • 🙊 堆顶元素
      • 🙊 堆的销毁
      • 🙊 堆排序
        • 🌺 版本1 创建堆,通过push、pop进行操作 (异地操作)
        • 🌺 版本2 向上调整建堆(原地操作)
        • 🌺 版本3----向下调整建堆(原地操作)
          • 🍀 向下调整建堆的时间复杂度分析
      • 🙊 topK问题
        • 🌺 topK问题的分析
        • 🌺 topK问题的代码实现
        • 🌺 top问题的时间复杂度和空间复杂度分析
  • 🌏 测试程序
    • 🐓 打印堆
    • 🐓 验证是否是一个堆
    • 🐓 测试程序

前言:在二叉树基础篇我们提到了二叉树的顺序实现,今天让我们来学习一下特殊的二叉树———堆的相关知识。

📃博客主页: 小镇敲码人
💞热门专栏:数据结构与算法
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞

🌏 堆的概念

堆是一种完全二叉树,但是其节点值满足一些特定的规则,又分为小堆和大堆,小堆是任意根节点的值都小于等于它子树的节点值,大堆是任意根节点的值都大于等于它子树的值。

小堆:

在这里插入图片描述

大堆:

在这里插入图片描述

注意:不管是大堆还是小堆,其同一层的节点大小可任意。

🌏 堆的模拟实现

🐓 堆的结构和方法接口

在二叉树部分我们曾经提到,堆是一种完全二叉树,所以其使用顺序存储是不会浪费空间的,而顺序存储就是我们常说的动态顺序表。

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdbool.h>
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>typedef int HPDataType;
typedef struct Heap
{HPDataType* _a;int _size;int _capacity;
}Heap;//堆的初始化
void HeapInit(Heap* hp);
// 堆的构建(直接给一个数组)
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
//HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//向上调整建堆
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int parent, int n);
//两数交换
void Swap(HPDataType* hp1, HPDataType* hp2);
//打印堆
void HeapPrint(Heap* hp);
//堆排序
void Heapsort(HPDataType* a, int n);

🐓 堆的方法的模拟实现

下面我们的方法我们都默认建一个小堆。

🙊 堆的初始化

堆的初始化很简单,我们将其对应的指针置空,元素个数和空间大小置0即可。

//堆的初始化
void HeapInit(Heap* hp)
{assert(hp);//hp不为空hp->_a = NULL;//指针置为空hp->_capacity = hp->_size = 0;//元素和空间大小置为0
}

🙊 堆的构建

给你一个数组,你应该如何建一个小堆呢?我们来画图分析:

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{assert(hp);//断言,防止hp为空hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);//为存节点的数组申请空间hp->_size = hp->_capacity = n;//更新空间大小和元素大小memcpy(hp->_a, a, sizeof(HPDataType) * n);//将数组里的值copy到我们的数据域里面//向上调整建一个小堆for (int i = 1; i < n; i++){AdjustUp(hp->_a,i);}
}

图解代码:
在这里插入图片描述

向下调整建堆的时间复杂度分析:

在这里插入图片描述

🙊 堆的插入

堆的插入应该如何实现呢?我们直接插入到线性表尾部就行,由于插入前是堆,所以我们走一个向上调整就可以将其调整为堆。

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{assert(hp);//断言,防止hp为空if (hp->_size == hp->_capacity)//扩容{hp->_capacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;HPDataType* tmp = (HPDataType*)realloc(hp->_a,sizeof(HPDataType) * hp->_capacity);if (tmp == NULL){perror("realloc failed\n");exit(-1);}hp->_a = tmp;}hp->_a[hp->_size] = x;//插入hp->_size++;//向上调整AdjustUp(hp->_a,hp->_size - 1);//在插入位置走一个向上调整
}

🙊 向上调整

上面已经分析过了。

//向上调整建小堆
void AdjustUp(HPDataType* a, int child)
{assert(a);//a不为空int parent = (child - 1) / 2;//找到child的父亲节点,和其比较看是否需要调整while (child > 0){if (a[child] < a[parent])//建一个小堆,如果孩子节点比父亲节点的值小,需要调整{Swap(&a[child], &a[parent]);//交换child = parent;//将父亲节点的值给孩子,继续在这条路径上调整parent = (parent - 1) / 2;//更新父亲节点的下标}else{break;//孩子节点的值已经大于等于父亲节点的值了,不需要再继续调整}}
}

🙊 堆的删除

堆的删除我们是删除堆顶的数据,因为这个数据最值得去删,什么意思呢?举个例子1.因为删除最后一个节点不会改变堆的结构没什么意义,2.堆顶数据要么是最大值,要么是最小值,很特殊。

那我们应该如何实现堆的删除呢?我们还是画图来分析:

在这里插入图片描述
代码实现:

// 堆的删除
void HeapPop(Heap* hp)
{assert(hp);//hp不为空assert(hp->_size > 0);//堆的元素个数要大于0Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);//交换堆顶元素和最后一个元素的值hp->_size--;//_size--//向下调整AdjustDown(hp->_a, 0, hp->_size);//根节点左树和右树为小堆,可以向下调整
}

🙊 向下调整

这个我们上面也已经分析了,直接给代码:

//向下调整(小堆)
void AdjustDown(HPDataType* a, int parent, int n)
{assert(a);//a不为空int child = parent * 2 + 1;//先假设最小的孩子是左孩子while (child < n){//找出最小的孩子if (child+1 < n && a[child] > a[child + 1])//如果右孩子存在且右孩子的值比左孩子更小{child = child + 1;}if (a[child] < a[parent])//最小的孩子比parent的值小,就交换{Swap(&a[child], &a[parent]);parent = child;//更新parentchild = parent * 2 + 1;//更新child,也假设最小的孩子是左孩子}else//最小的孩子比父亲节点的值大,已经不需要调整,是小堆了。{break;}}
}

🙊 堆的数据的个数

直接返回_size的值。

// 堆的数据个数
int HeapSize(Heap* hp)
{assert(hp);//hp不为空return hp->_size;
}

🙊 堆的判空

如果_size不为0,堆就不为空。

// 堆的判空
int HeapEmpty(Heap* hp)
{assert(hp);if (hp->_size == 0)//为空返回1return 1;else//不为空返回0return 0;
}

🙊 堆顶元素

堆顶元素在下标为0的位置。

// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{assert(hp);assert(hp->_size > 0);return hp->_a[0];
}

🙊 堆的销毁

堆的销毁实际上主要要做的就是回收在堆上开的空间,将相应的指针置为空,空间和元素大小置为0。

// 堆的销毁
void HeapDestory(Heap* hp)
{assert(hp);//hp不为空assert(hp->_size > 0);//元素个数要大于0free(hp->_a);//释放空间hp->_a = NULL;//将动态数组的指针置为空hp->_capacity = hp->_size = 0;//将空间和元素大小置为0
}

🙊 堆排序

由于小堆和大堆的堆顶元素都是最小或者最大的,所以我们pop堆顶元素,然后再剩下的元素调整,就能得到一个有序的序列。

🌺 版本1 创建堆,通过push、pop进行操作 (异地操作)

那我们该如何实现这个堆排序呢?下面的代码可以吗为什么?

void Heapsort(HPDataType* a, int n)
{Heap hp1;//假设我们建一个小堆HeapInit(&hp1);for (int i = 0; i < n; ++i){HeapPush(&hp1, a[i]);//插入对应元素}for (int i = 0; i < n; ++i){a[i] = HeapTop(&hp1);//将最小的元素依次给数组HeapPop(&hp1);//pop堆顶元素}
}

效果演示:

在这里插入图片描述

这种方法是不太好的,因为先暂且不谈时间复杂度的问题,我们为了去给数组排一个序,而建了一个堆,这个空间开销是没必要的,其实我们可以对数组原地建堆。

🌺 版本2 向上调整建堆(原地操作)
//堆排序,向上调整
void Heapsort2(HPDataType* a, int n)
{assert(a);for (int i = 1; i < n; i++)//我们向上调整,原地建一个小堆{AdjustUp(a,i);}for (int end = n - 1; end >= 0; --end){Swap(&a[0], &a[end]);//将堆顶元素放到堆最后面去AdjustDown(a, 0, end);//此时的end就代表我们的元素个数}
}

原地建堆,如果你想排降序,就需要建一个小堆,因为小堆的堆顶元素是最小的,我们原地操作,就可以将最小的和最后一个元素交换,这样没有破坏原来的结构,走一个向下调整就可以恢复堆结构。

在这里插入图片描述

时间复杂度:NlogN,向上调整建堆:NlogN,向下调整调整一次logN(一共调整了N次),所以总体的时间复杂度也是O(N*logN)。

🌺 版本3----向下调整建堆(原地操作)

但是这样就结束了吗,我们发现如果使用上面这种方法,既要写一个向上调整,也要写一个向下调整,我只是想排个序,需要这么复杂吗,难道就不能直接向下调整建堆吗?这样我就不用写两个了呀,调整那块肯定要用向下调整的,不然就破坏堆的结构了。

下面我们来介绍向下调整建堆

在这里插入图片描述
代码实现:

//堆排序,向下调整
void Heapsort3(HPDataType* a, int n)
{assert(a);for (int i = (n-1-1)/2; i >= 0; i--)//我们向上调整,原地建一个小堆{AdjustDown(a,i,n);}for (int end = n - 1; end >= 0; --end){Swap(&a[0], &a[end]);//将堆顶元素放到堆最后面去AdjustDown(a, 0, end);//此时的end就代表我们的元素个数}
}

运行结果:

在这里插入图片描述

🍀 向下调整建堆的时间复杂度分析

在这里插入图片描述

整体这种堆排序的时间复杂度也是O(N*logN),建堆的消耗是O(N),但是每一次调整的消耗还是logN,调整了次,量级还是和第二种一样,但是向下调整建堆只需要写一个向下调整,且向下调整建堆比向上调整建堆要更快。

🙊 topK问题

topK问题指的是让我们求第K大,第K小等问题,思考一下,在一个乱序的数组里面,如果我们想要这样来做,我们常规的解决办法是什么?排序!!!这样来做的时间复杂度是N*logN,我们只是求第K大或者第K小的数,有必要把这些数全部排一遍顺序吗,答案是不用,下面让我们来学习一下使用堆这种数据结构来解决经典的topK问题。

🌺 topK问题的分析

下面我们画图来分析一下topk问题具体的解决之道。

在这里插入图片描述

🌺 topK问题的代码实现

我们使用C语言的文件操作,造了100w个小于100w的数据(随机值),然后我们求前5个大的数,我们这样来验证,把文件里任意5个数改为大于100w的数,如果最后打印出来是我们改的5个数,证明我们的topk问题得到解决。

在这里插入图片描述

可以看到我们改了最后5个数,修改之后把创建数据的程序注释掉,防止它重新生成,我们的修改就没意义了。

代码实现:

void CreateNDate()
{// 造数据int n = 1000000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (size_t i = 0; i < n; ++i){int x = rand() % 1000000;//造随机数据,可以让我们的测试程序得到更好的检查fprintf(fin, "%d\n", x);//将数据放入文件}fclose(fin);
}void PrintTopK(int k)
{const char* file = "data.txt";//定义文件FILE* flout = fopen(file, "r");if (flout == NULL){perror("fopen failed");exit(-1);}//开一个数组存放k个数据,开一个小堆HPDataType* minheap = (HPDataType*)malloc(sizeof(HPDataType) * k);if (minheap == NULL){perror("malloc failed");exit(-1);}for (int i = 0; i < k; i++)//将k个数放入数组中{fscanf(flout, "%d", &minheap[i]);}//向下调整建堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(minheap, i, k);}//遍历,如果大于小堆堆顶的数据的话,就和它换,并向下调整int x = 0;while (fscanf(flout, "%d", &x) != EOF){if (x > minheap[0]){Swap(&x, &minheap[0]);AdjustDown(minheap, 0,k);}}for (int i = 0; i < k; i++)//打印前k大的数{printf("%d ", minheap[i]);}fclose(flout);//关闭文件
}
int main()
{CreateNDate();//造10w个数据PrintTopK(5);return 0;
}

运行结果:

在这里插入图片描述

可以看到预期结果和我们修改的数是一致的,说明我们的代码应该没啥问题。

🌺 top问题的时间复杂度和空间复杂度分析

在这里插入图片描述

🌏 测试程序

下面我们来测试一下我们写的上述方法的正确性如何。

🐓 打印堆

我们可能需要把堆打印出来看结果。

//打印堆
void HeapPrint(Heap* hp)
{assert(hp);assert(hp->_size > 0);for (int i = 0; i < hp->_size; i++){printf("%d ", hp->_a[i]);}printf("\n");
}

🐓 验证是否是一个堆

我们应该如何验证我们的数据是否是一个小堆呢?其实很简单,数组是顺序存储的,我们的数据一定是完全二叉树,这个不用验证,关键是验证我们的数据是否都满足小堆的定义,即所有的根节点的值都比它的左子树的和右子树的根节点的值要小,我们可以走一个前序遍历来验证。

//验证是否是一个小堆
int is_Heap(HPDataType* a, int n,int root)
{if (root >= n)return 1;int leftchild = root * 2 + 1;//左孩子的下标int rightchild = root * 2 + 2;//右孩子的下标if (leftchild < n && a[root] > a[leftchild])//左孩子存在且小于根,返回0,并打印相关信息{printf("%d\n位置不满足小堆", root);return 0;}if (rightchild < n && a[root] > a[rightchild])//右孩子存在且小于根,返回0,并打印相关信息{printf("%d\n位置不满足小堆", root);return 0;}return is_Heap(a, n, leftchild) && is_Heap(a, n, rightchild);//左树和右树都满足小堆就返回1
}

🐓 测试程序

void Test2()
{Heap hp1;//建堆HeapInit(&hp1);//初始化堆//造10w个数据进堆int N = 100000;srand(time(NULL));//通过时间来初始化随机数种子for (int i = 0; i < N; ++i){HeapPush(&hp1, rand() % (10000000));//将数据存入堆里面}int i = is_Heap(hp1._a,N,0);//验证是否为堆if (i)//打印提示信息printf("是堆\n");elseprintf("不是堆\n");printf("堆的元素个数为%d\n", HeapSize(&hp1));//验证元素个数方法是否正确printf("堆顶元素为:%d\n", HeapTop(&hp1));//验证堆顶元素是否正确HeapPop(&hp1);//验证pop函数是否正确printf("堆的元素个数为%d\n", HeapSize(&hp1));printf("堆顶元素为:%d\n", HeapTop(&hp1));hp1._a[3] = -1;//破坏堆的结构i = is_Heap(hp1._a, N, 0);if (i)printf("是堆\n");elseprintf("不是堆\n");HeapDestory(&hp1);if (hp1._a == NULL)printf("销毁成功\n");
}

运行结果:

在这里插入图片描述

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

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

相关文章

政安晨:【深度学习实践】【使用 TensorFlow 和 Keras 为结构化数据构建和训练神经网络】(二)—— 深度神经网络

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras实战演绎 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 概述 深度神经网络&#xff08;Deep Neural Network…

探索 Flutter 中的 NavigationRail:使用详解

1. 介绍 在 Flutter 中&#xff0c;NavigationRail 是一个垂直的导航栏组件&#xff0c;用于在应用程序中提供导航功能。它通常用于更大屏幕空间的设备&#xff0c;如平板电脑和桌面应用程序。NavigationRail 提供了一种直观的方式来浏览应用程序的不同部分&#xff0c;并允许…

5.windows Ubuntu 子系统,测序数据质量检测。

我们在得到一组或几组测序数据后&#xff0c;比如从测序公司拿到的测序数据为fastq.gz格式&#xff0c;首先我们需要对它们进行MD5检验&#xff0c;确保数据没有问题后才可进入以后的程序。&#xff08;MD5可以进行测序数据完整性验证&#xff1a;MD5可以用于验证数据在传输或存…

python基础——数据容器总结、通用方法和相互转换

&#x1f4dd;前言&#xff1a; 在前段时间我们已经把python中常见的五大数据容器&#xff1a;列表、元组、字符串、集合、字典学习完了。 这篇文章主要对刚学习过的五大数据容器的特点做个总结&#xff0c;以及再介绍几个通用方法和它们之间相互转化的方法&#xff1a; 1&…

Python Qt Designer 初探

代码下载在最下面 #开发环境安装# 本示例在Windows11下, 使用VSCode开发, Python 3.12.2, Qt Designer 5.11 VSCode插件Python、Python Debugger、PYQT Integration、Pylance (准备) VSCode自行官网下载 Visual Studio Code - Code Editing. Redefined (准备) Python 直接…

体育竞赛成绩管理系统设计与实现|jsp+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java&#xff0c;…

NLP 笔记:Latent Dirichlet Allocation (介绍篇)

1 问题介绍 假设我们有一堆新闻&#xff0c;每个新闻都有≥1个主题 我们现在只知道新闻的内容&#xff0c;我们希望一个算法&#xff0c;帮我们把这些新闻分类成主题人类可以根据每个每个文章里面的单词判断主题&#xff0c;那计算机怎么做呢&#xff1f; ——>LDA(Latent D…

如何申请香港信用卡?

第一种方法就是申请香港的虚拟卡 在线上申请&#xff0c;方便快捷&#xff0c;下卡快&#xff0c;可以绑定香港apple id&#xff0c;香港paypal等等 点击获取线上香港信用卡 第二种线下办理方法 如何申请香港个人卡 香港个人银行卡(等同于大陆的借记卡9)&#xff0c;开户限制…

K8S--SpringCloud应用整合Nacos实战

原文网址&#xff1a;K8S--SpringCloud应用整合Nacos实战-CSDN博客 简介 本文介绍K8S部署SpringCloud应用整合Nacos实战。 本文是将原来的SpringCloud项目&#xff08;闪速优选&#xff09;迁移到K8S上&#xff0c;一行代码都不需要改动。用K8S运行Nacos、Gateway、SpringCl…

Stable Diffusion 本地训练端口与云端训练端口冲突解决办法

方法之一&#xff0c;修改本地训练所用的端口 1 首先&#xff0c;进入脚本训练器的根目录 例如&#xff1a;C:\MarkDeng\lora-scripts-v1.7.3 找到gui.py 2 修改端口号 因为云端训练器也是占用28000和6006端口 那么本地改成27999和6007也是可以的 保存退出&#xff0c;运行启动…

阿里的库存秒杀是如何实现的?

一、阿里的库存秒杀的实现 阿里有很多业务&#xff0c;几十上百个业务线&#xff0c;各自都有一些需要做抢购、秒杀、热点扣将的场景。他们都用哪些方案呢? 我看了很多资料&#xff0c;也找了很多人做交流&#xff0c;最终得到的结论是啥都有&#xff0c;主要总结几个主流的&…

React系列 之 React进阶 含源码解读 (一)事件合成、state原理

资料来源&#xff1a;掘金课程 https://juejin.cn/book/6945998773818490884?enter_fromcourse_center&utm_sourcecourse_center 记录一些笔记 事件合成 React的事件其实是React重新实现的一套事件系统。目标是统一管理事件&#xff0c;提供一种跨浏览器一致性的事件处…