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

本文主要是介绍【数据结构和算法初阶(C语言)】二叉树的顺序结构--堆的实现/堆排序/topk问题详解---二叉树学习日记②12,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

目录

 ​编辑

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向下调整建堆

讨论最坏的时间复杂度

将下图进行建立一个大堆

假设有h层,这里测算的时间复杂度是最坏的情况,那么就相当于调整的是一个满二叉树的堆。

第h层  ,共有2^(h-1)个节点, 需要调整0次

第h-1层,共有2^(h-2)个节点 ,每个节点调整一次,调整:2^(h-2)*1次

第h-2层,共有2^(h-3)个节点,每个节点最坏向下调整两次,调整2^(h-3)*2次

        :

        :

        :

第3层,共有2^(2)个节点,每个节点向下调整h-3次,调整2^(2)*(h-3)次

第2层,共有2^(1)个节点,每个节点向下调整h-2次,调整2^(1)*(h-2)次

第1层,共有2^(0)个节点,每个节点向下调整h-1次,调整2^(0)*(h-1)次

时间复杂度为:

T(h) = 2^(0)*(h-1)+2^(1)*(h-2)+2^(2)*(h-3)+…………2^(h-3)*2+2^(h-2)*1①

2T(h) =  2^(1)*(h-1)+2^(2)*(h-2)+2^(3)*(h-3)+…………2^(h-2)*2+2^(h-1)*1

②-①得

T(h) = 2^(1)+2^(2)……+2^(h-2)+2^(h-1)+1-h

=2^0+2^(1)+2^(2)……+2^(h-2)+2^(h-1)-h

=2^h-1-h

由于h是树的层高,与节点个数的关系是:N = 2^h-1

h = log(n+1)

所以时间复杂度为:

O(N) = N-longN+1~O(N)

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++方向前行的奋斗者,数据结构内容持续更新中,感谢大家的关注与喜欢。

这篇关于【数据结构和算法初阶(C语言)】二叉树的顺序结构--堆的实现/堆排序/topk问题详解---二叉树学习日记②12的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/831884

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个