数据结构C语言版 —— 二叉树的顺序存储堆的实现

news2025/1/23 14:55:24

二叉树顺序结构实现(堆)

1. 堆的概念

  • 堆在物理上是一个一维数组,在逻辑上是一颗完全二叉树
  • 满足父亲节点小于等于孩子节点的叫做小堆或者小根堆
  • 满足父亲节点大于等于孩子节点的叫做大堆或者大根堆

堆的孩子和父亲的下标关系

  1. 已知父亲(parent)的下标

    • 左孩子(left)下标等于 l e f t = 2 ∗ p a r e n t + 1 left = 2*parent+1 left=2parent+1
    • 右孩子(right)下标等于 r i g h t = 2 ∗ p a r e n t + 2 right = 2 * parent + 2 right=2parent+2
  2. 已知左孩子或右孩子下标(child)

    • 父亲节点下标等于 p a r e n t = ( c h i l d − 1 ) / 2 parent = (child-1)/2 parent=(child1)/2

在这里插入图片描述

2. 堆的基本操作

堆的向下调整算法

下面这个数组逻辑上可以看作是一棵完全二叉树,通过从根节点开的向下调整算法可以把它调整成一个堆(大堆或小堆),向下调整算法有以有一个前提:左右子树必须是一个堆,才能调整。我这里的是实现小堆的向下调整算法

建小堆的向下调整的基本思路就是:从堆顶开始,拿自己和较小的一个孩子进行比较大小,如果小就进行交换然后把交换的位置当作父节点继续向下调整,如果两个孩子都比自己小就停止调整,否则一直调整到叶子节点。

在这里插入图片描述

// 向下调整(小堆)
void AdjustDown(HPDataType* arr, int n, int index)
{
	int parent = index;
	int child = 2 * parent;
	while (parent < n)
	{
		
		//找出两个孩子里的较小的
		if (child < n && child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		// 拿较小的孩子比较和父亲比价大小
		if (child < n && arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent;
		}
		else
		{
			//说明无需调整
			break;
		}
	}
}

堆的向下调整每次调整的一个节点,假设树的高度为 h h h最坏情况下调整的次数就是 h − 1 h-1 h1,所以向下调整的时间复杂度就是树的深度 l o g 2 ( n − 1 ) log_{2}(n-1) log2(n1),最后得出 l o g 2 n log_{2}n log2n

堆的创建

我们知道堆的向下调整算法必须满足左右子树都是一个堆,那有的时候是一个普通的数组,也就是一颗普通的完全二叉树,所以要通过建堆来让一个数组变成堆。

建堆的实现思路:从最后一个节点的父节点,也就是第一个非叶子节点的父亲开始不断向下调整,直到整课树都被调整成一个堆。

在这里插入图片描述

//向下调整建堆
int i = 0;
//从倒数第一个非叶子节点开始向下调整
for (i = (n - 2) / 2; i >= 0; --i)//n为数组元素个数
{
    AdjustDown(arr,n ,i);
}

建堆的时间复杂度

我们知道时间复杂度就是计算最坏的时间复杂度,实际上就是计算一个满二叉树,这样每一棵树都会进行调整。

假设这一棵树的高度是 h h h

  1. 第一层的节点个数就是 2 0 2^{0} 20、第二层 2 1 2^{1} 21、第三层 2 2 2^{2} 22,第 n n n层就有 2 n − 1 2^{n-1} 2n1个,那么最后一层就有 2 h − 1 2^{h-1} 2h1个节点
  2. 每一层调整的高度:第一层 h − 1 h-1 h1、第二层 h − 2 h-2 h2、…、1

那么假设时间复杂度为 T n T_{n} Tn,时间复杂度就是从第一层到倒数第二层每个节点的调整次数之和

  • 时间复杂度: T ( n ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + 2 3 ∗ ( h − 4 ) + . . . + 2 h − 3 ∗ 2 + 2 h − 2 ∗ 1 T(n) = 2^{0}*(h-1)+2^{1}*(h-2)+2^{2}*(h-3)+2^{3}*(h-4)+...+2^{h-3}*2+2^{h-2}*1 T(n)=20(h1)+21(h2)+22(h3)+23(h4)+...+2h32+2h21
  • 等式两边同时乘2: 2 ∗ T ( n ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + 2 3 ∗ ( h − 3 ) + 2 4 ∗ ( h − 4 ) + . . . + 2 h − 2 ∗ 2 + 2 h − 1 ∗ 1 2*T(n) = 2^{1}*(h-1)+2^{2}*(h-2)+2^{3}*(h-3)+2^{4}*(h-4)+...+2^{h-2}*2+2^{h-1}*1 2T(n)=21(h1)+22(h2)+23(h3)+24(h4)+...+2h22+2h11
  • 使用错位相减法(将上面两个等式进行相减): T ( n ) = 2 1 + 2 2 + 2 3 + 2 4 + . . . + 2 h − 2 + 2 h − 1 − h + 1 T(n) = 2^{1}+2^{2}+2^{3}+2^{4}+...+2^{h-2}+2^{h-1}-h+1 T(n)=21+22+23+24+...+2h2+2h1h+1
  • 错位相减后得到一个等比数列: T ( n ) = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + . . . + 2 h − 2 + 2 h − 1 − h T(n) = 2^{0}+2^{1}+2^{2}+2^{3}+2^{4}+...+2^{h-2}+2^{h-1}-h T(n)=20+21+22+23+24+...+2h2+2h1h
  • 通过等比数列公式$S_{n} = \frac{a_{1}(1-q^{n})}{1-q} $
  • 1 − 2 ( h − 1 ) ∗ 2 1 − 2 \frac{1-2^{(h-1)}*2}{1-2} 1212(h1)2
  • T ( n ) = 2 h − 1 − h T(n) = 2^{h}-1-h T(n)=2h1h;( h h h是错位相减得到的)
  • 假设有 N N N个节点,于是就推出 N = 2 h − 1 N = 2^{h}-1 N=2h1,即 h = l o g 2 ( N + 1 ) h =log_{2}(N+1) h=log2(N+1)(一棵高度为 h h h的满二叉树的节点个数等于 2 h − 1 2^{h}-1 2h1
  • 把上面两个公式带入 T ( n ) = 2 h − 1 − h T(n) = 2^{h}-1-h T(n)=2h1h得出,得到 T ( n ) = N − l o g 2 ( N + 1 ) T(n) = N - log_{2}(N+1) T(n)=Nlog2(N+1)
  • 通多大O渐近法表示得到最后的时间复杂度 O ( N ) O(N) O(N)

所以建堆的时间复杂度就是 O ( N ) O(N) O(N),因为当 N N N足够大时,对数的大小就根本不值得一提了。

堆的向上调整算法

堆的向上调整算法是用一个堆中,当我们要在堆的末尾插入一个新元素

将堆顶元素和最后一个元素进行交换,然后将最后一个位置的元素进行向上调整。

如果是建小堆,拿最后一个元素和父节点进行比较,如果父节点大于自己就进行交换,接着以父节点的位置继续开始向上调整,如果不小于父节点就停止向上调整(说明此时已经满足小堆的条件了)。

在这里插入图片描述

// 交换函数
void Swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}
// 向上调整(建小堆)
void AdjustUp(HPDataType* arr, int index)
{
	int child = index;
	int parent = child / 2;//获取父节点下标
	while (parent >= 0)
	{
		if (arr[parent] > arr[child])//如果节点如果大于孩子就交换
		{
			Swap(&arr[parent], &arr[child]);
			child = parent;
			parent = child / 2;
		}
		else
		{
			//说明无需调整
			break;
		}
	}
}

3. 堆的实现

通过一维数组来实现一个逻辑上的完全二叉树,需要定义以下接口

堆的结构体

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* arr;//数组
	int size;//堆中元素个数
	int capacity;//堆的容量
}Heap;
// 交换函数
void Swap(HPDataType* x, HPDataType* y);
// 堆的创建
Heap* HeapCreate(HPDataType* arr, int n);
// 向下调整
void AdjustDown(HPDataType* arr, int n, int index);
// 向上调整
void AdjustUp(HPDataType* arr, int index);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType data);
// 堆的删除
void HeapPop(Heap* hp);
// 获取堆顶元素
HPDataType HeapTop(Heap* hp);
// 获取堆的元素个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

堆的创建

首先先通过malloc开辟空间

如果一个数组不是堆,在创建的时候就需要通过向下调整算法,从最后一个叶子节点的父亲开始调整,把它调整成一个小堆

// 交换函数
void Swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}
// 堆的创建
Heap* HeapCreate(HPDataType* arr, int n)
{
	assert(arr);
	Heap* heap = (Heap*)(malloc(sizeof(Heap)));
	if (heap == NULL)
	{
		printf("malloc erro!\n");
		exit(-1);
	}
	heap->arr = (HPDataType*)(malloc(sizeof(HPDataType) * n));
	heap->size = n;
	heap->capacity = n;
	memcpy(heap->arr, arr, sizeof(HPDataType) * n);
	//向下调整建堆
	int i = 0;
	//从倒数第一个非叶子节点开始向下调整
	for (i = (n - 2) / 2; i >= 0; --i)
	{
		AdjustDown(heap->arr,heap->capacity ,i);
	}

	return heap;
}
// 向下调整
void AdjustDown(HPDataType* arr, int n, int index)
{
	int parent = index;
	int child = 2 * parent;
	while (parent < n)
	{
		
		//找出两个孩子里的较小的
		if (child < n && child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		// 拿较小的孩子比较和父亲比价大小
		if (child < n && arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent;
		}
		else
		{
			//说明无需调整
			break;
		}
	}
}

向堆中插入元素

  • 堆的插入需要判断扩容,如果堆满了就进行二倍扩容。
  • 每次默认在堆的末尾插入一个元素,再拿这个元素进行向上调整
// 堆的插入
void HeapPush(Heap* hp, HPDataType data)
{
	assert(hp);
	//扩容
	if (hp->size == hp->capacity)
	{
		// 二倍扩容
		HPDataType* ptr = (HPDataType*)(realloc(hp->arr, sizeof(HPDataType)*hp->capacity * 2));
		if (ptr == NULL)
		{
			printf("扩容失败\n %s", strerror(errno));
			exit(-1);
		}
		hp->arr = ptr;
		hp->capacity = 2 * hp->capacity;
	}

	hp->arr[hp->size] = data;
	//向上调整
	AdjustUp(hp->arr,hp->size);
	hp->size++;
}

删除堆顶元素

删堆顶元素实现思路

  • 拿堆顶元素和数组最后一个元素进行交换
  • 在把堆中元素个数减一
  • 再从堆顶进行向下调整
// 堆的删除
void HeapPop(Heap* hp)
{
	//堆中没有元素
	assert(hp && hp->size != 0);

	//拿堆顶元素和数组最后一个元素交换
	Swap(&(hp->arr[0]), &(hp->arr[hp->size - 1]));
	hp->size--;
	//向下调整
	AdjustDown(hp->arr, hp->size, 0);
	
}

获取堆顶元素

这个比价简单,就会返回数组 第一个元素就好

// 获取堆顶元素
HPDataType HeapTop(Heap* hp)
{
	assert(hp && hp->size != 0);

	return hp->arr[0];
}

获取堆中元素个数

// 获取堆的元素个数
int HeapSize(Heap* hp)
{
	assert(hp);

	return hp->size;
}

判断堆是否为空

// 堆的判空
int HeapEmpty(Heap* hp)
{
	assert(hp);

	return hp->size == 0;
}

堆的销毁

// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->arr);
	hp->size = 0;
	hp->capacity = 0;
	hp->arr = NULL;
	free(hp);
}

4. TopK问题

Topk问题:给你一个组数据找出前k大的数

思路:对数组排序,取出前k个

size_t IntCmp(const void* x, const void* y)
{
	return *((int*)y) - *((int*)x);
}
void Test(int* arr, int n, int k)
{
	qsort(arr, n, sizeof(arr[0]), IntCmp);
	int i = 0;
	for (i = 0; i < k; i++)
	{
		printf("%d ", arr[i]);
    }
}	

qsort底层是通过快排实现的,而快排的时间复杂度为 n ∗ l o g 2 n n*log_{2}n nlog2n

问题升级:能不能让时间复杂度在降低一点

此时就可以通过堆来解决这个问题

  • 找前k个大的建小堆
  • 找前k个小的建大堆

假设前面的找前k个大的数,建个小堆,因为小堆的堆顶一定是是一组数里最小的一个数字,如果来了一个数字比最小的数还要大,那么它肯定是要先如堆的。

于是写出代码

// 堆的创建
Heap* HeapCreate(HPDataType* arr, int n)
{
	assert(arr);
	Heap* heap = (Heap*)(malloc(sizeof(Heap)));
	if (heap == NULL)
	{
		printf("malloc erro!\n");
		exit(-1);
	}
	heap->arr = (HPDataType*)(malloc(sizeof(HPDataType) * n));
	heap->size = n;
	heap->capacity = n;
	memcpy(heap->arr, arr, sizeof(HPDataType) * n);
	//向下调整建堆
	int i = 0;
	//从倒数第一个非叶子节点开始向下调整
	for (i = (n - 2) / 2; i >= 0; --i)
	{
		AdjustDown(heap->arr,heap->capacity ,i);
	}

	return heap;
}
// 向下调整
void AdjustDown(HPDataType* arr, int n, int index)
{
	int parent = index;
	int child = 2 * parent;
	while (parent < n)
	{
		
		//找出两个孩子里的较小的
		if (child < n && child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		// 拿较小的孩子比较和父亲比价大小
		if (child < n && arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = 2 * parent;
		}
		else
		{
			//说明无需调整
			break;
		}
	}
}
// 堆的删除
void HeapPop(Heap* hp)
{
	//堆中没有元素
	assert(hp && hp->size != 0);

	//拿堆顶元素和数组最后一个元素交换
	Swap(&(hp->arr[0]), &(hp->arr[hp->size - 1]));
	hp->size--;
	//向下调整
	AdjustDown(hp->arr, hp->size, 0);
	
}

// 获取堆顶元素
HPDataType HeapTop(Heap* hp)
{
	assert(hp && hp->size != 0);

	return hp->arr[0];
}

然后不断获取堆顶元素,不断删除堆顶元素,就能得到前K个小的数。于是 O ( n ) O(n) O(n)的时间复杂就解决了问题

问题继续升级:假设有100亿个整数,从中找出前10大的数。

此时用单纯用堆肯定行不通的,因为一个整形4个字节,那么100亿个整形就是400亿个字节,那么这就是将近40G的数据,如果单纯用堆肯定是不行的。

思路:建一个大小为10的小堆,不断往堆中插入元素,如果元素满了,就和堆顶比较,如果小就删除堆顶元素,然后再进行插入,直到遍历完整个数组。

那么此时的时间复杂度为 O ( n ) O(n) O(n),而空间复杂度则是 O ( k ) O(k) O(k)


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

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

相关文章

基于昇思MindSpore实现使用胶囊网络的图像描述生成算法

基于昇思MindSpore实现使用胶囊网络的图像描述生成算法 项目链接 https://github.com/Liu-Yuanqiu/acn_mindspore 01 项目描述 1.1 图像描述生成算法 人类可以轻易的使用语言来描述所看到的场景&#xff0c;但是计算机却很难做到&#xff0c;图像描述生成任务的目的就是教…

昇思MindSpore动静结合中list和dict方法实现

01 概述 静态图和动态图是神经学习框架中的重要概念&#xff0c;昇思MindSpore同时支持动态图和静态图两种模式&#xff0c;在动态图与静态图的结合方面做了很多工作。本文以昇思MindSpore框架中图模式下list和dict的实现方式为例&#xff0c;介绍昇思MindSpore框架中的动静结…

C与C++如何互相调用

个人主页&#xff1a;董哥聊技术我是董哥&#xff0c;嵌入式领域新星创作者创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01;文章目录1、为什么会有差异&#xff1f;2、extern "C"3、C调用C正确方式4、C调用C5、总结在项目开发过…

[第十二届蓝桥杯/java/算法]C——卡片

&#x1f9d1;‍&#x1f393;个人介绍&#xff1a;大二软件生&#xff0c;现学JAVA、Linux、MySQL、算法 &#x1f4bb;博客主页&#xff1a;渡过晚枫渡过晚枫 &#x1f453;系列专栏&#xff1a;[编程神域 C语言]&#xff0c;[java/初学者]&#xff0c;[蓝桥杯] &#x1f4d6…

中外法律文献查找下载常用数据库大盘点

中外法律文献查找下载常用数据库有&#xff1a; 一、Westlaw&#xff08;法律全文数据库&#xff09; 是法律出版集团Thomson Legal and Regulator’s于1975年开发的&#xff0c;为国际法律专业人员提供的互联网的搜索工具。 Westlaw International其丰富的资源来自法律、法规…

图(Graph)详解 - 数据结构

文章目录&#xff1a;图的基本概念图的存储结构邻接矩阵邻接矩阵的实现邻接表邻接表实现图的遍历图的广度优先搜索&#xff08;BFS&#xff09;图的深度优先搜索&#xff08;DFS&#xff09;最小生成树Kruskal算法Prim算法最短路径单源最短路径 - Dijkstra算法单源最短路径 - B…

Linux学习-91-Discuz论坛安装

17.22 Discuz论坛安装 通过 Discuz! 搭建社区论坛、知识付费网站、视频直播点播站、企业网站、同城社区、小程序、APP、图片素材站&#xff0c;游戏交流站&#xff0c;电商购物站、小说阅读、博客、拼车系统、房产信息、求职招聘、婚恋交友等等绝大多数类型的网站。Discuz!自2…

《教养的迷思》

在读《穷查理宝典》时&#xff0c;查理芒格在有一讲&#xff0c;专门谈及《教养的迷思》一书&#xff0c;说到作者朱迪斯哈里斯。查理芒格认为哈里斯在探求真理的道路上走得很顺利&#xff0c;取得成功的因素之一就是她热衷于摧毁自己的观念。 朱迪斯在书的开端首先严肃地纠正了…

【案例教程】无人机生态环境监测、图像处理与GIS数据分析综合实践

【查看原文】无人机生态环境监测、图像处理与GIS数据分析综合实践技术应用 构建“天空地”一体化监测体系是新形势下生态、环境、水文、农业、林业、气象等资源环境领域的重大需求&#xff0c;无人机生态环境监测在一体化监测体系中扮演着极其重要的角色。通过无人机航空遥感技…

Fabric系列 - 多通道技术(Muti-channel)

可在节点&#xff0c;通道和联盟级别上配置。 一个Fabric网络中能够运行多个账本&#xff0c;每个通道间的逻辑相互隔离不受影响&#xff0c;如下图所示&#xff0c;每种颜色的线条代表一个逻辑上的通道&#xff0c;每个Peer节点可以加入不同的通道&#xff0c;每个通道都拥有…

AI编译器XLA调研

文章目录一、XLA简介二、XLA在TensorFlow中的应用2.1 XLA是什么&#xff1f;&#xff08;tensorflow\compiler\xla&#xff09;2.2 TensorFlow怎样转化为XLA &#xff08;tensorflow\compiler\tf2xla&#xff09;2.3 JIT(just in time) 即时编译 &#xff08;tensorflow\compil…

【大数据技术Hadoop+Spark】Flume、Kafka的简介及安装(图文解释 超详细)

Flume简介 Flume是Cloudera提供的一个高可用、高可靠、分布式的海量日志采集、聚合和传输的系统&#xff0c;Flume支持在日志系统中定制各类数据发送方&#xff0c;用于收集数据&#xff1b;同时&#xff0c;Flume提供对数据进行简单处理&#xff0c;并写到各种数据接受方&…

NLP学习笔记(四) Seq2Seq基本介绍

大家好&#xff0c;我是半虹&#xff0c;这篇文章来讲序列到序列模型 (Sequence To Sequence, Seq2Seq) 本文写作思路如下&#xff1a; 从循环神经网络的应用场景引入&#xff0c;介绍循环神经网络作为编码器和解码器使用&#xff0c;最后是序列到序列模型 在之前的文章中&am…

微信消息收发与微信内部emoji表情转义

微信消息收发与微信内部emoji表情转义 目录 微信内部emoji表情转义与消息收发 一、概述 二、常用标准emoji表情字符、微信内部转义符、unicode对照表 1、比如 2、微信聊天窗口emoji表情字符 2.1、PC端表情选择&#xff0c;01~03排&#xff1a; 2.2、PC端表情选择&#…

华为IMC培训——通信基础

目录 一、华为设备图标 二、数据的传递 三、专业术语 四、网络设备及相关知识 五、OSI七层模型 六、TCP和UDP数据报格式 七、TCP的三次握手 八、 TCP窗口滑动机制 一、华为设备图标 AP&#xff1a;相当于家用路由器一般配和AC使用。 AC和AP的区别_wangzhibo_csdn的博客…

创意被盗用,这3个加水印方法,让照片刻上我们专属印记

一般我们为了保护自己的图片不被别人盗用&#xff0c;都会选择在图片上刻上专属印记。那么便是加水印方法&#xff0c;它包含两种&#xff1a;文字水印和图片水印。想知道怎么给图片添加水印吗&#xff1f;其实有很多种法子可以做到&#xff0c;下面就由我来分享这3个简单好用的…

代码随想录刷题记录 day48 两个字符串的删除操作+编辑距离

代码随想录刷题记录 day48 两个字符串的删除操作编辑距离 583. 两个字符串的删除操作 思想 两个元素都能删除了&#xff0c;还是考虑第i-1个字符和第j-1个字符是不是相同的&#xff0c;不相同的话考虑三种情况&#xff0c;删除i-1&#xff1b;删除j-1&#xff0c;同时删除 1…

css实现鼠标禁用(鼠标滑过显示红色禁止符号)

css实现鼠标禁用&#xff08;鼠标滑过显示红色禁止符号&#xff09;创作背景css鼠标禁用创作背景 从本文开始&#xff0c;将会用三篇文章来一步一步实现vueantdts实战后台管理系统中table表格的不可控操作。中间会补充两篇css知识文章&#xff0c;方便后续功能的实现。实现表格…

非零基础自学Golang 第14章 反射 14.2 基本用法 14.2.2 获取类型的值 14.2.3 使用反射调用函数

非零基础自学Golang 文章目录非零基础自学Golang第14章 反射14.2 基本用法14.2.2 获取类型的值14.2.3 使用反射调用函数第14章 反射 14.2 基本用法 14.2.2 获取类型的值 Go语言使用reflect.TypeOf来获取类型信息&#xff0c;使用reflect.ValueOf来获取变量值的信息。 refle…

云原生|kubernetes|CKA真题解析-------(6-10题)

第六题&#xff1a; service配置 解析&#xff1a; 考察两个知识点&#xff1a; deployment控制器内的port命名 暴露一个pod内的端口到新建的服务内的 这里有一个需要注意的地方&#xff0c;没有告诉你deployment控制器在哪个namespace。假设这个front-end这个pod是在A这个…