二叉树顺序结构——堆的结构与实现

news2024/11/15 9:50:08

二叉树顺序结构——堆的结构与实现

  • 一、二叉树的顺序结构
  • 二、堆的概念及结构
  • 三、堆的实现
    • 堆向下调整算法
    • 堆的创建
    • 建堆时间复杂度
    • 堆的插入(堆向上调整算法)
    • 堆的删除
    • 堆的代码实现(使用VS2022的C语言)
      • 初始化、销毁
      • 构建、插入、删除
      • 返回堆顶元素、判空、返回有效元素个数
  • 四、完整 Heap.c 源代码

一、二叉树的顺序结构

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

二、堆的概念及结构

如果有一个关键码的集合K = { k0,k1 ,k2 ,…,k(n - 1) },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: Ki <= K(2 * i +1) 且 Ki <= K(2 * i + 2) (Ki >= K(2 * i + 1) 且 Ki >= K(2 * i + 2) ) ,i = 0,1,2…,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆
堆的性质:

  1. 堆中某个结点的值总是不大于或不小于其父结点的值;
  2. 总是一棵完全二叉树
    在这里插入图片描述
    在这里插入图片描述

三、堆的实现

注意:

  1. 对于堆中父亲节点下标 i ,它的左孩子总是 i * 2 + 1,右孩子总是 i * 2 + 2;
  2. 对于左右孩子节点 i ,由于整数相除会取整,则它们共同的父亲节点为:(i - 1) / 2;
/*
* leftChild = parent * 2 + 1;
* rightChild = parent * 2 + 2;
* parent = (leftChild - 1) / 2 = (rightChild - 1) / 2;
*/

堆向下调整算法

现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根结点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整
在这里插入图片描述
在这里插入图片描述

void swap(HPDataType* a, HPDataType* b)
{
	HPDataType temp = *a;
	*a = *b;
	*b = temp;
}

// 向下调整代码
void AdjustDown(HPDataType* arr, int n, int parent)
{
	assert(arr);
	
	// 先找到左孩子
	int child = parent * 2 + 1;
	while (child < n)						// 当child 超过范围退出
	{
		// 假设法
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			++child;
		}
		
		// 若不符合堆的性质,则调整,反之退出
		if (arr[parent] > arr[child])
		{
			swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;							// 满足堆的性质,直接退出
		}
	}
}

堆的创建

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。根结点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 堆的创建
void HeapCreate(pHeap ph, HPDataType* arr, int sz)
{
	assert(ph);
	assert(HeapEmpty(ph));		// 堆不为空则不能创建

	HPDataType* temp = (HPDataType*)realloc(ph->arr, sizeof(HPDataType) * sz);
	if (temp == NULL)
	{
		perror("realloc failed");
		return;
	}
	ph->arr = temp;
	ph->size = sz;
	ph->capacity = sz;

	memcpy(ph->arr, arr, sizeof(HPDataType) * sz);
	
	// 倒数的第一个非叶子结点的子树下标为: 
	// (总长 - 2) / 2 == 总长 / 2 - 1
	// 因为对于最后一个叶子节点,它的下标为:
	// 总长 - 1
	// 而我们知道非根节点无论是左右孩子,因为整数用除会取整,
	// 则它的父亲节点均为:
	// (child - 1) / 2
	// 即:
	// lastChild = 总长 - 1;
	// 倒数的第一个非叶子结点的子树下标 = (lastChild - 1) / 2;
	// 得 (总长 - 1 - 1) / 2 == 总长 / 2 - 1
	for (int i = sz / 2 - 1; i >= 0; --i)
	{
		AdjustDown(ph->arr, sz, i);
	}
}

建堆时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果):
在这里插入图片描述
因此:建堆的时间复杂度为O(N)

堆的插入(堆向上调整算法)

先插入一个 10 到数组的尾上,再进行向上调整算法,直到满足堆。
在这里插入图片描述

// 向上调整
void AdjustUp(HPDataType* arr, int child)
{
	assert(arr);

	int parent = (child - 1) / 2;
	while (child > 0)				// 当 child 为根节点时退出
	{
		if (arr[child] < arr[parent])
		{
			swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;					// 当数据满足堆的性质时退出
		}
	}
}

堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
在这里插入图片描述

堆的代码实现(使用VS2022的C语言)

堆常用的接口包括:

  1. 初始化、销毁

  2. 构建、插入、删除

  3. 返回堆顶元素、判空、返回有效元素个数

在 Heap.h 中:

#pragma once

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

// 初始化容量
#define INIT_CAPACITY 4

// 增容倍率
#define EXPANSION_MULTIPLE 2

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* arr;
	int size;
	int capacity;
} Heap, * pHeap;

// 初始化、销毁
void HeapInit(pHeap ph);

void HeapDestroy(pHeap ph);

// 构建、插入、删除
void HeapCreate(pHeap ph, HPDataType* arr, int sz);

void HeapPush(pHeap ph, HPDataType x);

void HeapPop(pHeap ph);

// 返回堆顶元素、判空、返回有效元素个数
HPDataType HeapTop(pHeap ph);

bool HeapEmpty(pHeap ph);

int HeapSize(pHeap ph);

在 Heap.c 中:

初始化、销毁

void HeapInit(pHeap ph)
{
	assert(ph);

	ph->arr = NULL;
	ph->size = 0;
	ph->capacity = 0;
}

void HeapDestroy(pHeap ph)
{
	assert(ph);

	free(ph->arr);
	ph->size = 0;
	ph->capacity = 0;
}

构建、插入、删除

// 堆的创建
void HeapCreate(pHeap ph, HPDataType* arr, int sz)
{
	assert(ph);
	assert(HeapEmpty(ph));		// 堆不为空则不能创建

	HPDataType* temp = (HPDataType*)realloc(ph->arr, sizeof(HPDataType) * sz);
	if (temp == NULL)
	{
		perror("realloc failed");
		return;
	}
	ph->arr = temp;
	ph->size = sz;
	ph->capacity = sz;

	memcpy(ph->arr, arr, sizeof(HPDataType) * sz);

	// 倒数的第一个非叶子结点的子树下标为: 
	// (总长 - 2) / 2 == 总长 / 2 - 1
	// 因为对于最后一个叶子节点,它的下标为:
	// 总长 - 1
	// 而我们知道非根节点无论是左右孩子,因为整数用除会取整,
	// 则它的父亲节点均为:
	// (child - 1) / 2
	// 即:
	// lastChild = 总长 - 1;
	// 倒数的第一个非叶子结点的子树下标 = (lastChild - 1) / 2;
	// 得 (总长 - 1 - 1) / 2 == 总长 / 2 - 1
	for (int i = sz / 2 - 1; i >= 0; --i)
	{
		AdjustDown(ph->arr, sz, i);
	}
}

void HeapPush(pHeap ph, HPDataType x)
{
	assert(ph);
	
	// 先判断空间是否充足
	if (ph->size == ph->capacity)
	{
		int newCapacity = ph->capacity == 0 ? INIT_CAPACITY : ph->capacity * EXPANSION_MULTIPLE;
		HPDataType* temp = (HPDataType*)realloc(ph->arr, sizeof(HPDataType) * newCapacity);
		if (temp == NULL)
		{
			perror("realloc failed");
			return;
		}

		ph->arr = temp;
		ph->capacity = newCapacity;
	}

	ph->arr[ph->size++] = x;
	AdjustUp(ph->arr, ph->size - 1);
}

void HeapPop(pHeap ph)
{
	assert(ph);
	assert(!HeapEmpty(ph));

	--ph->size;
	swap(&ph->arr[0], &ph->arr[ph->size]);
	AdjustDown(ph->arr, ph->size, 0);
}

返回堆顶元素、判空、返回有效元素个数

HPDataType HeapTop(pHeap ph)
{
	assert(ph);
	assert(!HeapEmpty(ph));

	return ph->arr[0];
}

bool HeapEmpty(pHeap ph)
{
	assert(ph);

	return ph->size == 0;
}

int HeapSize(pHeap ph)
{
	assert(ph);

	return ph->size;
}

四、完整 Heap.c 源代码

#include "Heap.h"

void HeapInit(pHeap ph)
{
	assert(ph);

	ph->arr = NULL;
	ph->size = 0;
	ph->capacity = 0;
}

void HeapDestroy(pHeap ph)
{
	assert(ph);

	free(ph->arr);
	ph->size = 0;
	ph->capacity = 0;
}

void swap(HPDataType* a, HPDataType* b)
{
	HPDataType temp = *a;
	*a = *b;
	*b = temp;
}

// 向下调整代码
void AdjustDown(HPDataType* arr, int n, int parent)
{
	assert(arr);

	// 先找到左孩子
	int child = parent * 2 + 1;
	while (child < n)						// 当child 超过范围退出
	{
		// 假设法
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			++child;
		}

		// 若不符合堆的性质,则调整,反之退出
		if (arr[parent] > arr[child])
		{
			swap(&arr[parent], &arr[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;							// 满足堆的性质,直接退出
		}
	}
}

// 向上调整
void AdjustUp(HPDataType* arr, int child)
{
	assert(arr);

	int parent = (child - 1) / 2;
	while (child > 0)				// 当 child 为根节点时退出
	{
		if (arr[child] < arr[parent])
		{
			swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;					// 当数据满足堆的性质时退出
		}
	}
}

// 堆的创建
void HeapCreate(pHeap ph, HPDataType* arr, int sz)
{
	assert(ph);
	assert(HeapEmpty(ph));		// 堆不为空则不能创建

	HPDataType* temp = (HPDataType*)realloc(ph->arr, sizeof(HPDataType) * sz);
	if (temp == NULL)
	{
		perror("realloc failed");
		return;
	}
	ph->arr = temp;
	ph->size = sz;
	ph->capacity = sz;

	memcpy(ph->arr, arr, sizeof(HPDataType) * sz);

	// 倒数的第一个非叶子结点的子树下标为: 
	// (总长 - 2) / 2 == 总长 / 2 - 1
	// 因为对于最后一个叶子节点,它的下标为:
	// 总长 - 1
	// 而我们知道非根节点无论是左右孩子,因为整数用除会取整,
	// 则它的父亲节点均为:
	// (child - 1) / 2
	// 即:
	// lastChild = 总长 - 1;
	// 倒数的第一个非叶子结点的子树下标 = (lastChild - 1) / 2;
	// 得 (总长 - 1 - 1) / 2 == 总长 / 2 - 1
	for (int i = sz / 2 - 1; i >= 0; --i)
	{
		AdjustDown(ph->arr, sz, i);
	}
}

void HeapPush(pHeap ph, HPDataType x)
{
	assert(ph);

	// 先判断空间是否充足
	if (ph->size == ph->capacity)
	{
		int newCapacity = ph->capacity == 0 ? INIT_CAPACITY : ph->capacity * EXPANSION_MULTIPLE;
		HPDataType* temp = (HPDataType*)realloc(ph->arr, sizeof(HPDataType) * newCapacity);
		if (temp == NULL)
		{
			perror("realloc failed");
			return;
		}

		ph->arr = temp;
		ph->capacity = newCapacity;
	}

	ph->arr[ph->size++] = x;
	AdjustUp(ph->arr, ph->size - 1);
}

void HeapPop(pHeap ph)
{
	assert(ph);
	assert(!HeapEmpty(ph));

	--ph->size;
	swap(&ph->arr[0], &ph->arr[ph->size]);
	AdjustDown(ph->arr, ph->size, 0);
}

HPDataType HeapTop(pHeap ph)
{
	assert(ph);
	assert(!HeapEmpty(ph));

	return ph->arr[0];
}

bool HeapEmpty(pHeap ph)
{
	assert(ph);

	return ph->size == 0;
}

int HeapSize(pHeap ph)
{
	assert(ph);

	return ph->size;
}

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

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

相关文章

Python深度学习之PyTorch基础教程

⛄前言 PyTorch是一个基于Torch的Python开源机器学习(深度学习)框架&#xff0c;由Facebook的人工智能研究院开发&#xff0c;不仅能够实现强大的GPU加速&#xff0c;还支持动态神经网络&#xff0c;使得研究人员和开发人员能够轻松构建和训练复杂的深度学习模型。与TensorFlo…

贪心算法 -- 组合一组数字获得最大数

贪心算法 – 组合一组数字获得最大数 文章目录 贪心算法 -- 组合一组数字获得最大数题目重现读懂题目贪心场景代码示例 题目重现 题目链接&#xff1a;最大数 - 力扣 给定一组非负整数 nums&#xff0c;重新排列每个数的顺序&#xff08;每个数不可拆分&#xff09;使之组成一…

error 12154 received logging on to the standby报错处理

错误 处理方法 该参数不是主库的servicename &#xff08;低级错误&#xff09; SQL> alter system set log_archive_dest_2 SERVICEstandby ASYNC VALID_FOR(ONLINE_LOGFILES,PRIMARY_ROLE) DB_UNIQUE_NAMEstandby; System altered. 观察主库日志: 备库日志: 该问题会影…

Python集合的基本概念和使用方法

目录 集合&#xff08;Set&#xff09; 基本概念 基本特性 基本操作 集合运算 成员测试 高级操作 集合推导式 总结 集合&#xff08;Set&#xff09; Python集合&#xff08;Set&#xff09;是Python语言中一个非常实用且强大的数据结构&#xff0c;它用于存储多个不…

day27回溯算法part03| 39. 组合总和 40.组合总和II 131.分割回文串

39. 组合总和 题目链接/文章讲解 | 视频讲解 本题是 集合里元素可以用无数次&#xff0c;那么和组合问题的差别 其实仅在于 startIndex上的控制 class Solution { public:int sum;vector<int> path;vector<vector<int>> result;void backtracking(vector<…

爬虫工具yt-dlp

yt-dlp是youtube-dlp的一个fork&#xff0c;youtube-dlp曾经也较为活跃&#xff0c;但后来被众多网站屏蔽&#xff0c;于是大家转而在其基础上开发yt-dlp。yt-dlp的github项目地址为&#xff1a;GitHub - yt-dlp/yt-dlp: A feature-rich command-line audio/video downloaderA …

Java学习【深入探索包装类和泛型】

Java学习【深入探索包装类和泛型】 &#x1f680;包装类获取包装类对象的方式使用valueOf()创建直接赋值 Integer成员方法 &#x1f680;泛型引出泛型泛型类泛型方法泛型接口泛型的继承和通配符泛型的上界 在Java的学习中&#xff0c;包装类和泛型是两个重要的概念&#xff0c;…

分布式事务AP控制方案(上)

分布式事务控制方案 本篇文章给出一种要求高可用性&#xff08;AP思想&#xff09;的分布式事务控制方案 下篇新鲜出炉&#xff1a;点我查看 分布式事务控制方案1、业务背景2、本地消息表的设计3、对消息表的操作4、任务调度5、任务流程控制的抽象类6、课程发布的实现类7、总…

我的创作纪念日--我和CSDN一起走过的1825天

机缘 第一次在CSDN写文章&#xff0c;是自己在记录学习Java8新特性中Lambda表达式的内容过程中收获的学习心得。之前也有记录工作和生活中的心得体会、难点的解决办法、bug的排查处理过程等等。一直都用的有道笔记&#xff0c;没有去和大家区分享的想法&#xff0c;是一起的朋…

《Brave New Words 》2.4 与历史对话

Part II: Giving Voice to the Social Sciences 第二部分&#xff1a;为社会科学发声 Conversing with History 与历史对话 Good history and civics teachers make the past interesting. Great history and civics teachers make the past come alive. When history and civi…

作业07 递推算法2

作业&#xff1a; #include <iostream> using namespace std; int main(){int a[110][110]{0},b[110][110]{0},n;cin>>n;for(int i1;i<n;i){for(int j1;j<i;j){cin>>a[i][j];}}for(int in-1;i>1;i--){for(int j1;j<i;j){a[i][j]a[i][j]max(a[i1]…

离散数学--连通性和矩阵

目录 0.关系的运算和性质 1.通路和回路 2.连通关系 3.割点&#xff08;边&#xff09;和点&#xff08;边&#xff09;割集 4.强&#xff08;弱&#xff09;连通&单向连通 0.关系的运算和性质 &#xff08;1&#xff09;这个运算包括了矩阵的运算&#xff0c;包括这个…

汽车数据应用构想(三)

上期说的&#xff0c;用数据去拟合停车信息的应用&#xff0c;那么类似的POI信息相关的场景其实都可以实现。今天讲讲用户使用频率也很高的加油/充电场景。 实际应用中&#xff0c;在加油场景中用户关心的通常还是价格。无论是导航还是各种加油APP/小程序&#xff0c;都已经很…

了解常用智能指针

智能指针 1、概念 C中引入智能指针的主要目的是为了解决内存管理的问题&#xff0c;传统的指针&#xff08;裸指针&#xff09;在使用时需要手动分配和释放内存&#xff0c;容易出现内存泄漏和悬挂指针等问题。智能指针通过封装裸指针&#xff0c;并提供自动内存管理功能&…

端午安康,最真挚的祝福送最“粽”要的人

端午节&#xff0c;又称端阳节、龙舟节、重五节、天中节等&#xff0c;是集拜神祭祖、祈福辟邪、欢庆娱乐及饮食为一体的民俗大节&#xff0c;与春节、清明节、中秋节并称为中国四大传统节日&#xff0c;2008年被列为国家法定节假日&#xff0c;2009年9月端午节成为中国首个入选…

笔记 | 软件工程04:软件项目管理

1 软件项目及其特点 1.1 什么是项目 1.2 项目特点 1.3 影响项目成功的因素 1.4 什么是软件项目 针对软件这一特定产品和服务的项目努力开展“软件开发活动",&#xff08;理解&#xff1a;软件项目是一种活动&#xff09; 1.5 软件项目的特点 1.6 军用软件项目的特点 2 …

有点好玩的python运维脚本

python运维脚本 1. 常用端口扫描2. 文件整理 1. 常用端口扫描 在计算机网络中&#xff0c;端口是一个通信端点&#xff0c;允许不同的进程或服务通过网络连接和交换数据。端口通过数值来标识&#xff0c;并与特定的协议相关联。未采取适当安全措施而保持端口开放&#xff0c;可…

ICRA 2024:北京工业大学马楠教授联合中科原动力公司推出番茄采摘自主机器人AHPPEBot,实现32.46秒快速准确采摘

当前&#xff0c;农业生产正深受劳动力短缺困扰&#xff0c;这一现状对生产规模的进一步拓展构成了严重制约。为了突破这一瓶颈&#xff0c;实施自动化已成为提升农业生产力的关键途径&#xff0c;这也使得机器人采收技术备受关注。 现今的机器人采收系统普遍采用先进感知方法&…

linux 网桥学习

前言&#xff1a; 本文来学习一下linux网桥概念和网桥配置 1. linux网桥概念 网桥&#xff0c;类似于中继器&#xff0c;连接局域网中两个或者多个网段。它与中继器的不同之处就在于它能够解析它收发的数据&#xff0c;读取目标地址信息&#xff08;MAC&#xff09;&#xff…