【数据结构】堆的实现

news/2024/5/20 10:33:26 标签: 数据结构, 算法, c++,

  • 1.:一种二叉树
  • 2.的概念及结构
  • 3.的实现
    • 3.1 创建的结构
    • 3.2 的初始化
    • 3.3 的插入
    • 3.4 的向上调整法(up)
    • 3.5 打印的数据
    • 3.6 到这里就可以实现一个基本的
    • 3.7 向下调整法down(非常重要的一个方法)
    • 3.8 最优的建算法
    • 3.9的一些操作(pop,top,size,empty)
      • 3.9.1 的删除 pop
      • 3.9.2 取顶元素
      • 3.9.2 的大小
      • 3.9.3 判断是否为空
    • 3.10 的销毁
  • 4.总结

1.:一种二叉树

是一种完全二叉树,使用顺序结构的数组来存储。
不了解完全二叉树的小伙伴可以看看这篇文章,这里不再赘述
二叉树
使用完全二叉树可以避免空间的浪费,因为完全二叉树结点之间从逻辑图看一定是连续的
在这里插入图片描述
我们要做的是像最左边的逻辑图那样利用顺序结构实现

需要注意的是这里的和操作系统虚拟进程地址空间中的两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段

2.的概念及结构

在这里插入图片描述

的性质:
1.中某个结点的值总是不大于或不小于父节点的值
2.总是一棵完全二叉树

如图所示
在这里插入图片描述

3.的实现

3.1 创建的结构

typedef int data_type; //把int typedef这样可以方便以后随时更换数据类型

typedef struct heap // 这里默认是大根
{
	int* a;   // 数组模拟
	int size; // 元素数量
	int capacity; // 的当前容量
}heap; 

3.2 的初始化

void init(heap* h)
{
	assert(h); //断言一下,防止传入空
	h->a = NULL;
	h->size = 0;
	h->capacity = 4; // 容量初始化为 4 
	int* tmp = (heap*)malloc(sizeof(heap) * h -> capacity);
	//malloc动态开辟数组大小
	if (tmp == NULL)  //养成好习惯,malloc后判断是否开辟成功
	{
		perror("malloc");
		exit(-1);
	}
	h->a = tmp; // 开辟成功后,再把开辟的空间给我们的的数组
}

3.3 的插入

我们要往里面插入数据,最好的方法就是在数组的末尾插入,因为如果在数组前面插入的话,整个数组的其他元素还要往后挪,在中间插入也要挪,最好的方法就是往末尾插入,再去调整它,保持大根或者小根,我们这里默认实现大根

void push(heap* h, data_type x) // x 是我们要插入的数据
{
	assert(h);
	if (h -> size == h->capacity) //判断容量是否满了,满了就扩容
	{
		int* tmp = (heap*)realloc(h->a, sizeof(heap) * h->capacity * 2);//每次扩两倍
		if (tmp == NULL) //养成好习惯判断是否扩容成功
		{
			perror("malloc");
			exit(-1);
		}
		h->a = tmp; // 扩容成功再赋给的数组a
		h->capacity *= 2; //别忘了我们的结构体成员变量也要 *2
	}
	h->a[h->size] = x; //往size的位置插入x,每次都在末尾插入
	h->size++;//插入后每次size++,方便下次插入
	up(h -> a, h -> size - 1); // 然后使用向上调整法,我们下面讲
	//每插入一个数据就利用向上调整
}

3.4 的向上调整法(up)

向上调整法就是找到一个结点,然后拿这个结点与它的父亲去比较,
如果我们想实现大根,就判断这个结点是否比它的父亲大,满足条件就和这个的结点的父亲交换。
然后一直向上做判断,交换,做判断,交换。知道走到最顶层为止。
时间复杂度为 O(log2 n)

void up(data_type* a,int child)
{
	int parent = (child - 1) / 2; //利用孩子结点找父亲结点公式
	while (child > 0) // 走到最顶层为止
	{
		if (a[child] > a[parent])//如果孩子结点大于父亲
		{
			swap(&(a[child]), &(a[parent]));//交换
			child = parent; //把父亲结点变换成孩子结点,继续向上做判断,交换
			parent = (child - 1) / 2;//再一次找新孩子的父亲
		}
		else //如果孩子结点小于父亲,就不满足条件,break这次循环
		{
			break;
		}
	}
}

代码里的swap函数

void swap(data_type * n1, data_type* n2)
{
	int tmp = *n1;
	*n1 = *n2;
	*n2 = tmp;
}

3.5 打印的数据

我们先实现的打印,然后就可以查看里的数据了。

void print(heap* h)
{
	assert(h);
	for (int i = 0; i < h->size; i++)
	{
		printf("%d ", h->a[i]);
	}
	printf("\n");
}

3.6 到这里就可以实现一个基本的

void test1() // 测试
{
	heap h1;
	init(&h1);
	push(&h1, 50); 
	push(&h1, 25); 
	push(&h1, 90);
	push(&h1, 33);
	push(&h1, 12);
	print(&h1); // 结果是 90 33 50 25 12 
    //可以手动按顺序画个逻辑图,会发现确实是大根
}

我们模拟一下这个过程

一开始插入50,h1->a[0] = 50,h1 -> size++ ,此时size = 1
然后每插入一个数向上调整
up(h1->a,h1->size - 1 ) size - 1 是因为前面提前加了1
因为一开始只有一个数,所以up函数实际没用。
在这里插入图片描述
插入25后,调用up,发现25 < 50, 我们这里实现的是大根,所以不做交换。
接下来插入 90
在这里插入图片描述
调用一次up ,发现这个结点大于我的父结点,交换。
在这里插入图片描述

在这里插入图片描述
剩下的数据同理,大家可以自行模拟感受一下。

3.7 向下调整法down(非常重要的一个方法)

向下调整法就是找到该节点的孩子,判断我的两个孩子谁比我大或者谁比我小,实现大根或者小根
比如我们想实现大根,我们找到一个结点的两个左右孩子,如果孩子比父亲大,就交换。
注意,这里可以两个孩子都比父结点大,所以我们要找到最大的孩子交换。
然后一直往下判断,交换,判断,交换,达到最底层为止。

已知根结点求孩子的方法

left_child = (parent * 2) + 1 //左孩子
right_child = (parent * 2) + 2// 右孩子

向下调整法代码如下:

void down(data_type* a, int len, int parent)
{
	int child = (parent * 2) + 1; // 先假设左孩子最大
	while (child < len) //直到达到最底层就停止
	{
		//这里加1可能会有越界问题,可能会访问到随机值,导致出错,判断一下
		if (child + 1 < len && a[child + 1] > a[child]) // 如果右孩子大的话就 child++
		{
			child++;
		}
		if (a[parent] < a[child])//如果父结点小于孩子结点就交换
		{
			swap(&a[parent], &a[child]);
			parent = child;
			child = (parent * 2) + 1;
		}
		else
		{
			break;//否则break
		}
	}
}

3.8 最优的建算法

向下调整法是 建算法一个非常重要的算法
注意这里的建不同于上面的插入建,而是随便给你一个数组,要求你在最优的时间复杂度,把这个数组建立成,也就是大根或者小根
如果我们一个一个插入去实现,时间复杂度就是O(n * log2n)
或者利用向上调整法,从第二层开始逐层向上调整,时间复杂度也是O(n* log2n)
而如果利用我们的向下调整法,从倒数第二层开始向下调整,然后倒数第三层向下调整,直到最顶层,是可以做到时间复杂度为O(n)的。
最简单的一个优化理解就是,二叉树中一般最后一层占的结点最多,一般占了总结点的一半以上,我们直接省去了最后一层的计算,所以优化成了O(n)

void creat_heap(heap* h, data_type *arr, int n)  //建算法:从倒数第二层开始向下调整
{//这里的建是指传入一个数组,把它建成大或者小
	//时间复杂度O(n)
	assert(h);
	//malloc一个的新数组
	h->a = (heap*)malloc(sizeof(heap) * n);
	if (h -> a == NULL)
	{
		perror("malloc:");
		exit(-1);
	}
	//把原来数组的内容复制到新数组
	memcpy(h -> a, arr, sizeof(data_type) * n);
	h->size = h->capacity = n;//大小和容量也给过去
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
	// (n - 1 - 1) / 2 求倒数第一个非叶结点的位置
	//从倒数的第一个非叶子节点的子树开始调整
		down(h -> a, n, i);
	}
}

3.8.1 建算法时间复杂度分析

void test2()
{
	heap h2;
	int arr[] = { 1,5,3,8,7,6 };
	int n = sizeof(arr) / sizeof(arr[0]);
	creat_heap(&h2, arr,n);
	print(&h2); // 8 7 6 5 1 3
}

在这里插入图片描述
在这里插入图片描述

其实最简单的理解方式还是上面说的,最后一层的结点几乎占了总结点的一半,我们的向下调整法从倒数第一个非叶子开始,就省去了最后一层结点的计算,所以才能得到优化。

3.9的一些操作(pop,top,size,empty)

3.9.1 的删除 pop

的删除指的是删除顶的元素。

如果是直接删除顶元素,那我这个数组还要往前挪动,再向下调整
时间复杂度就是
O(n + log2n)
这种方法不怎么好。

我们这里的删除操作是用顶元素与最后的元素交换,然后size–
交换后,顶元素再向下调整,重新调整
时间复杂度为 O(log2n)

void pop(heap* h)
{
	assert(h);
	assert(h->size > 0);
	swap(&h -> a[0], &h -> a[h -> size - 1]);
	h->size--;
	down(h->a, h->size, 0);
}

3.9.2 取顶元素

data_type top(heap* h)
{
	assert(h);
	return h->a[0];
}

3.9.2 的大小

int heap_size(heap* h)
{
	assert(h);
	return h->size;
}

3.9.3 判断是否为空

bool heap_empty(heap* h)
{
	assert(h);
	return h->size == 0;
}

3.10 的销毁

void destroy(heap* h)
{
	assert(h);
	free(h->a);
	h->a = NULL;
	h->size = h->capacity = 0;
}

4.总结

以上就是的实现
重点是建算法
在后续文章中我们会讲到排序,和利用解决topK问题(前k大或前k小的数)

其实在C++中已经实现好了,我们直接用就好了,但是理解底层的实现原理,才能让我们在编程这条路上走的更远。


http://www.niftyadmin.cn/n/11264.html

相关文章

Python基础(二):不同系统安装Python3

​​​​​​​ 文章目录 不同系统安装Python3 一、Anaconda安装与使用 1、下载 2、安装 3、配置 4、使用 5、常用命令 二、原生Python安装 三、在Linux环境Python安装 不同系统安装Python3 14天学习训练营导师课程&#xff1a;杨鑫《Python 自学编程基础》杨鑫《 P…

kubelet源码 删除pod(三)

kubelet源码 删除pod&#xff08;三&#xff09; 上篇中UpdatePod函数结束&#xff0c;然后进入managePodLoop函数。 UpdatePod负责对pod的状态流程进行更新&#xff0c;对pod状态的标记 managePodLoop函数主要负责就是上述标记后处理这些pod了 1.managePodLoop函数里整个都是…

题目0155-特异性双端队列

特异性双端队列 题目描述 有一个特异性的双端队列,该队列可以从头部到尾部添加数据,但是只能从头部移除数据。 小A一次执行 2 n 2n 2n 个指令往队列中添加数据和移除数据, 其中 n n n 个指令是添加数据(可能从头部也可以从尾部添加) 依次添加

这便是智能

🍿*★,*:.☆欢迎您/$:*.★* 🍿

【蓝桥杯web】第十四届蓝桥杯(Web应用开发)模拟赛1期-大学组

数据类型检测 请看这篇数据类型检测 渐变色背景生成器 html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-equiv"X-UA-Compatible" content"IEedge" /><meta name&…

爬虫基础知识

一、Scrapy 1、cookie设置 目前cookie的设置不支持在headers进行设置&#xff0c; 需要通过以下三种方式进行设置: 第一种&#xff1a;setting文件中设置cookie 当COOKIES_ENABLED是注释的时候&#xff0c;scrapy默认没有开启cookie。当COOKIES_ENABLED没有注释设置为False的…

ASCII码与字符对照表(附转换代码)

目录 ASCII基本字符 ASCII控制字符 ASCII可显示字符 ASCII扩展字符 转换代码 ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符。标准 ASCII 码也叫基础ASCII码&#xff0c;使用 7 位二进制数来表示所有的大写和小写字母、数字 0 到 9、标点符号…

ShareSDK for Flutter

前言 这是一个基于ShareSDK功能的扩展的Flutter插件。使用此插件能够帮助您在使用Flutter开发应用时,快速地实现社会化功能,例如第三方授权登录,获取用户信息以及社交平台的分享等功能。 Demo例子&#xff1a;https://github.com/MobClub/ShareSDK-For-Flutter 开始集成 引入…