【数据结构和算法初阶(C语言)】二叉树的顺序结构--堆的实现/堆排序/topk问题详解---二叉树学习日记②1

 

目录

 ​编辑

1.二叉树的顺序结构及实现

1.1 二叉树的顺序结构

2 堆的概念及结构

3 堆的实现

3.1堆的代码定义

3.2堆插入数据

3.3打印堆数据

3.4堆的数据的删除

3.5获取根部数据

3.6判断堆是否为空

3.7 堆的销毁 

4.建堆以及堆排序 

4.1堆排序---是一种选择排序

4.2升序建大堆,降序建小堆

 4.3 建堆的时间复杂度

4.3.1向下调整建堆

4.3.2向上调整建堆

4.4 topk问题

5.结语


1.二叉树的顺序结构及实现

1.1 二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结 构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

左孩子的下标 = 父亲下标*2+1

右孩子下标 = 父亲节点下标*2+2

父亲节点下标 = (子节点下标-1)/2 

2 堆的概念及结构

堆是非线性结构,是完全二叉树

如果有一个值的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆的性质: 堆中某个节点的值总是不大于或不小于其父节点的值;

堆总是一棵完全二叉树。

通俗来说父节点小于等于子节点的完全二叉树就叫小根堆,或者小堆,根一定是整棵树最小的。

父节点值大于等于子节点的完全二叉树叫做大根堆。或者大堆,但是底层数组不一定降序。但是大堆的根是整棵树的最大值。

3 堆的实现

3.1堆的代码定义

底层是一个顺序表

typedef int HPDataType;typedef struct Heap
{//底层是一个顺序表,但是数据不是随便存储,逻辑结构是二叉树HPDataType * a;int size;int capacity;
}HP;

堆的初始化:

void HeapInit(HP* php)
{assert(php);HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * 2);//先为i堆空间申请两个节点if (tmp == NULL){perror("malloc");exit(-1);}php->a = tmp;php->capacity = 2;php->size = 0;
}

3.2堆插入数据

实现关键

实现原理图:向上调整:

(以大堆的实现方式举例)

首先我们从有限个数据的层面来实现一下堆的实现,后面堆排序再来看对于一堆数据怎么建堆。

对于一组少量数据比如一个数组:

首先将数据一个一个插入到堆里面,由于数据有限可以使用这种数据插入的方式建立堆这种数据结构;

void HeapPush(HP* php, HPDataType x)
{//尾插assert(php);//判断空间够不够if (php->capacity == php->size){HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(php->a) + sizeof(HPDataType) * 2);if (tmp == NULL){perror("realloc");exit(-1);}php->a = tmp;php->capacity += 2;}php->a[php->size] = x;php->size++;//调整数据,变成堆AdjustUp(php->a, php->size-1);}

 然后把这组数据调整成一个堆:

void Swap(HPDataType* child, HPDataType* parent)
{HPDataType tmp = 0;tmp = *child;*child = *parent;*parent = 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;//跳出循环}}}

3.3打印堆数据

为了看一下我们插入的效果我们来试一下插入一段数据 

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

 

 就建成了一个大堆。

3.4堆的数据的删除

堆这个数据结构有意义的一个点就是,大堆的根一定是这组数据中最大的值,小堆的根一定是这组数据中最小的值。所以如果我们能拿到这个根的数据,再删除就可以找到这堆数据中次小的数据了。那么删除根数据是这个结构比较有意义的。

想一个问题:根的删除能不能简单的数据覆盖?只是将后续的数据移动向前

答案是不能的,可以数据这样移动后续数据根本就不能成堆了。那么这里使用的方法是向下调整法

前提是左右子树是堆:

这里我们以小堆举例示范:

先删除

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);
}

在调整

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+1可能越界访问{child++;}if (a[child] < a[parent]){Swap(&a[child], &a[parent]);//继续向下调整parent = child;child = parent * 2 + 1;}else{break;}}}

调整是由于每次都是取两个子节点中的较小的值,所以先假设一个大,如果假设错了,就改变下标 

if (child+1<n&&a[child + 1] < a[child])//child+1可能越界访问
        {
            child++;
        }

对调整循环结束的判定所示孩子下标小于n

3.5获取根部数据

//获取根部数据
HPDataType HeapTop(HP* php)
{assert(php);assert(php->size > 0);return php->a[0];
}

3.6判断堆是否为空


//判断堆是否为空函数
bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;}

3.7 堆的销毁 

void HeapDestory(HP* php)
{assert(php);free(php->a);php->a = NULL;php->size = php->capacity = 0;
}

那么如果现在我们每次拿到堆的元素在删除在获取,就可以得到一个有序的数据了:

4.建堆以及堆排序 

上面我们已经掌握了堆这个数据结构的一些方法,最后通过插入数据建堆。删除数据将数据排序。可是如果我有十亿个数据,想找出最大的十个数据,如果用堆得插入10亿次数据吗?那就失去了使用这个数据结构的意义,通常来说我们只用建立一个大堆模型,这个堆的前十个数据自然就是10亿个数据中的最大的一个。

4.1堆排序---是一种选择排序

使用堆结构对一组数据进行排序,方便对数据进行处理。粗暴办法就是将原数组数据插入堆,再取堆数据覆盖,这种方法首先得有堆结构,其次插入数据就要额外开辟空间。

最好的方式就是直接将原数组或者原来的这组数据变成堆。将原数组直接看成一颗完全二叉树,一般都不是堆。那么就要将原数据之间调成堆----建堆

建堆不是插入数据,只是使用向上调整的思想。在原有数据上进行更改,调换

int a[] = { 2,3,5,7,4,6,8,65,100,70,32,50,60 };
HeapSort(a, sizeof(a) / sizeof(a[0]));void HeapSort(int* a, int n)
{for (int i = 0; i < n; i++){AdjustUp(a, 1);}}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;//跳出循环}}}

4.2升序建大堆,降序建小堆

一般我们要利用堆结构将一组数据排成升序,就建立大堆

要利用堆结构将一组数据排成降序,就建立小堆。

排序和删除的原理是一样的,先找最大/最小然后次大/次小,每次选取数据后不会将后面数据覆盖上来,否则就会导致关系全乱,可能次大数据就要重新建堆,增加了工作量了。而是通过堆顶元素和最后一个数据交换位置过后,向下调整思想,将排除刚刚调整的尾部最大数据除外的剩下数据看成堆,循环排序。

最后发现:大堆这样处理的数据最大的数据在最后,最小的在最前,小堆相反。 

void HeapSort(int* a, int n)
{//对数据进行建堆for (int i = 0; i < n; i++){AdjustUp(a, 1);}//堆排序---向下调整的思想int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;//让n-1个数据调整成堆选出次小}}

 4.3 建堆的时间复杂度

4.3.1向下调整建堆

4.3.2向上调整建堆

4.4 topk问题

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

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。 对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,

基本思路如下:

1. 用数据集合中前K个元素来建堆 前k个最大的元素,则建小堆 ;找前k个最小的元素,则建大堆

2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

比如现在我们的磁盘中有十亿个数据,我们要在十亿数据中找到前100个最大的数

第一步:读取文件的前100个数据,在内存中建立一个小堆

第二步:再依次读取剩余数据与堆顶部元素进行比较,大于堆顶就替换进堆,向下调整,所有数据读完,堆里面就是最大的100个。

首先堆顶元素就是这100个数据中最小的,如果比这个堆顶数据小的肯定不是要找的前100个最大数中的一个,如果比堆顶元素大进替换堆顶元素进堆,向下调整后找到这100个数据中次二小的,最后遍历完就得到这100个最大的数据。堆顶元素就是第100大数据,如果建立大堆一轮遍历只能找到一个最大的值,就没有必要建堆了。

先向磁盘中写入1万个数据

void CreateNDate()
{int n = 10000;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() % 1000000;fprintf(fin, "%d\n", x);}fclose(fin);
}

将前k个数据从磁盘中读入内存,

// 1. 建堆--用a中前k个元素建堆
//首先读文件
FILE* fout = fopen(filename, "r");
if (fout == NULL)
{perror("fopen");return;
}
int* minhead = (int*)malloc(sizeof(int) * k);
if (minhead == NULL)
{perror("malloc fail");return;
}
for (int i = 0; i < k; i++)
{fscanf(fout, "%d", &minhead[i]);
}

利用向下调整的方式建堆,并且与磁盘中的元素进行一一比较

//建堆,也可以向上插入建堆
for (int i = (k - 2) / 2; i >= 0; i--)
{AdjustDown(minhead, k ,i);
}// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
int x = 0;
while (fscanf(fout, "%d", &x) != EOF)
{if (x > minhead[0]){minhead[0] = x;//替换进堆AdjustDown(minhead, k, 0);}
}
//打印一下前100个最大的数据
for (int i = 0; i < k; i++)
{printf("%d ", minhead[i]);
}
printf("\n");
fclose(fout);

然后手动修改十个·最大的值在磁盘里检测结果:

 

在初始堆的时候就可以直接为这段数据开辟固定空间,然后初始化堆的时候就可以直接建堆;

void HeapInitArray(HP* php, int* a, int n)
{assert(php);assert(a);php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);//之间开辟好对应数据大小的空间if (php->a == NULL){perror("malloc fail");exit(-1);}php->size = n;php->capacity = n;memcpy(php->a, a, sizeof(HPDataType) * n);//将数据拷贝到空间中for (int i = 1; i < n; i++){AdjustUp(php->a, i);//向上调整成堆}
}

5.结语

以上就是本期的所有内容,知识含量蛮多,大家可以配合解释和原码运行理解。创作不易,大家如果觉得还可以的话,欢迎大家三连,有问题的地方欢迎大家指正,一起交流学习,一起成长,我是Nicn,正在c++方向前行的奋斗者,数据结构内容持续更新中,感谢大家的关注与喜欢。

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

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

相关文章

鸿蒙实战开发:【浏览器制作】

浏览器 介绍 本示例使用[ohos.systemparameter]接口和[Web组件]展示了一个浏览器的基本功能,展示网页&#xff0c;根据页面历史栈前进回退等。 效果预览 首页打开网址 使用说明: 连接Wifi&#xff0c;启动应用&#xff0c;展示默认页面内容;点击默认页面的图标跳转到对应…

C语言经典算法-7

文章目录 其他经典例题跳转链接36.排序法 - 改良的选择排序37.快速排序法&#xff08;一&#xff09;38.快速排序法&#xff08;二&#xff09;39.快速排序法&#xff08;三&#xff09;40.合并排序法 其他经典例题跳转链接 C语言经典算法-1 1.汉若塔 2. 费式数列 3. 巴斯卡三…

AnyGo for Mac最新激活版:位置模拟软件打破地域限制

AnyGo for Mac&#xff0c;一款专为Mac用户打造的位置模拟软件&#xff0c;让您能够轻松打破地域限制&#xff0c;畅享无限可能。 软件下载&#xff1a;AnyGo for Mac v7.0.0最新激活版 通过AnyGo&#xff0c;您可以随时随地模拟出任何地理位置&#xff0c;无论是国内热门景点还…

(三)pulsar可视化消息管理工具

官网&#xff1a;https://pulsar.apache.org/docs/3.2.x/administration-pulsar-manager/ 版本: 3.2.x 安装和配置 拉取容器 docker pull apachepulsar/pulsar-manager:v0.3.0运行容器&#xff1a; # pulsar消息管理工具 CURRENT_DIR$(cd dirname $0; pwd) BASE_DIR$(cd $(…

【07】进阶html5

HTML5 包含两个部分的更新,分别是文档和web api 文档 HTML5 元素表 元素语义化 元素语义化是指每个 HTML 元素都代表着某种含义,在开发中应该根据元素含义选择元素 元素语义化的好处: 利于 SEO(搜索引擎优化)利于无障碍访问利于浏览器的插件分析网页新增元素 多媒体…

手撕算法-判断是不是完全二叉树

描述&#xff1a;思路&#xff1a;采用层序遍历&#xff0c;找到一个为空的标记&#xff0c;如果后面还有值&#xff0c;就代表不是完全二叉树。代码&#xff1a; public boolean isCompleteTree (TreeNode root) {// write code hereif(root null) return true;Queue<Tree…

Go语言学习13-常见软件架构的实现

Go语言学习13-常见软件架构的实现 架构模式 An architectural pattern is a general, reusable solution to a commonly occurring problem in software architectural within a given context. ——wikipedia Pipe-Filter 架构 Pipe-Filter 模式 非常适合于数据处理及数据分…

[Qt学习笔记]Qt下使用Halcon实现采图时自动对焦的功能(Brenner梯度法)

目录 1、介绍2、实现方法2.1 算法实现过程2.2 模拟采集流程 3、总结4、代码展示 1、介绍 在机器视觉的开发中&#xff0c;现在有很多通过电机去做相机的聚焦调节&#xff0c;对比手工调节&#xff0c;自动调节效果更好&#xff0c;而且其也能满足设备自动的需求&#xff0c;尤…

HCIA ——VLAN实验

一 、 实验需求 1.PC1和PC3所在接口为access接口&#xff1b;属于vlan 2 PC2-4-5-6处于同一网段&#xff1b;其中PC2可以访问PC4-5-6 PC4可以访问PC5不能访问PC6 PC5不能访问PC6 3.PC1-PC3与PC2-4-5-6不在同一个网段 4.所有PC均使用DHCP获取IP地址&#xff0c;且PC1可以正常访问…

mysql之基本概念与安装

一 数据库的基本概念 1.1 数据 记录个体的信息 1.2 表 存放信息的集合&#xff0c;行于与列 1.3 数据库 数据库就是表的集合。它是以一定的组织方式存储的相互有关的数据集合 1.4 数据库管理系统 数据库管理系统&#xff08;DatabaseManagementSystem&#xff0c;DBMS&…

Flutter 初始WidgetState 简单应用案例分析

本系列文章主要整理Flutter的知识汇总&#xff0c;由浅入深&#xff0c;从Widget的搭建到其中的原理。本文还是围绕Widget在开发中应用和理解。 关于Flutter环境配置和首次创建可以参考前面文章。链接如下&#xff1a; Flutter 安装部署与认识Dart语言 Flutter 使用AndroidS…

MySQL 搭建双主复制服务 并 通过 HAProxy 负载均衡

一、MySQL 搭建双主复制高可用服务 在数据库管理中&#xff0c;数据的备份和同步是至关重要的环节&#xff0c;而双主复制&#xff08;Dual Master Replication&#xff09;作为一种高可用性和数据同步的解决方案&#xff0c;通过让两个数据库实例同时充当主服务器和从服务器&…

Tomcat(Win+Linux)安装教程

Windows环境安装 Tomcat安装及配置教程主要分为四步&#xff1a; 步骤一&#xff1a;确认自己是否已 安装JDK&#x1f50d; 步骤二&#xff1a;下载安装Tomcat 步骤三&#xff1a;Tomcat配置环境变量 步骤四&#xff1a;验证Tomcat配置是否成功 OK&#xff0c;我们开始&#x…

python 中 float 和 decimal 的区别

decimal --- 十进制定点和浮点运算 — Python 3.11.8 文档请参考官方说明文档&#xff1a; decimal --- 十进制定点和浮点运算 — Python 3.11.8 文档 举例&#xff1a; # 使用 Decimal 类型进行计算 from decimal import Decimaltotal_float 0.1 0.2 total_decimal Decim…

CMU 10-414/714: Deep Learning Systems --hw3

实现功能 在ndarray.py文件中完成一些python array操作 我们实现的NDArray底层存储就是一个一维向量&#xff0c;只不过会有一些额外的属性&#xff08;如shape、strides&#xff09;来表明这个flat array在维度上的分布。底层运算&#xff08;如加法、矩阵乘法&#xff09;都…

[LeetCode][LCR170]交易逆序对的总数

题目 LCR 170. 交易逆序对的总数 在股票交易中&#xff0c;如果前一天的股价高于后一天的股价&#xff0c;则可以认为存在一个「交易逆序对」。请设计一个程序&#xff0c;输入一段时间内的股票交易记录 record&#xff0c;返回其中存在的「交易逆序对」总数。 示例 1&#xf…

【VUE】前端阿里云OSS断点续传,分片上传

什么是OSS&#xff1a; 数据以对象&#xff08;Object&#xff09;的形式存储在OSS的存储空间&#xff08;Bucket &#xff09;中。如果要使用OSS存储数据&#xff0c;您需要先创建Bucket&#xff0c;并指定Bucket的地域、访问权限、存储类型等属性。创建Bucket后&#xff0c;您…

React - 实现菜单栏滚动

简介 本文将会基于react实现滚动菜单栏功能。 技术实现 实现效果 点击菜单&#xff0c;内容区域会自动滚动到对应卡片。内容区域滑动&#xff0c;指定菜单栏会被选中。 ScrollMenu.js import {useRef, useState} from "react"; import ./ScrollMenu.css;export co…

线程和进程的区别和联系

一、什么是进程 进程(Process), 是一个具有独立功能的程序关于某个数据集合的一次运行活动&#xff0c;是系统进行 【资源分配和调度】 的一个独立单位。 进程是【程序】的【一次执行】(是计算机中程序的执行过程&#xff0c;而不是计算机中的程序)进程是系统进行【资源分配和…

[LeetBook]【学习日记】排序算法——归并排序

主要思想 归并排序是一种分治算法&#xff0c;其排序过程包括分和治分是指将要排序的序列一分为二、二分为四&#xff0c;直到单个序列中只有一个数治是指在分完后&#xff0c;将每两个元素重新组合&#xff0c;四合为二、二合为一&#xff0c;最终完成排序 图片作者&#xf…