【数据结构】二叉树的顺序存储结构 —— 堆

news2025/2/28 20:12:33

文章目录

  • 前言
  • 二叉树的顺序存储
  • 堆的概念和结构
  • 堆的实现
    • 结构的定义
    • 接口总览
    • 初始化
    • 销毁
    • 插入
    • 向上调整
    • 删除
    • 向下调整
    • 取堆顶数据
    • 计算堆大小
    • 判空
    • 打印堆
  • 完整代码
    • Heap.h
    • Heap.c
    • test.c
  • 结语

前言

今天,我们开始二叉树的学习。本篇博客的内容为 介绍二叉树的顺序存储 和 堆的实现。今天的内容相对于之前的数据结构就多了一些 “科技与狠活” 了,不单单是看结构了,难度略微有些上升。所以做好准备,我们这就开始。

二叉树的顺序存储

二叉树的顺序结构存储是使用 数组存储

一般使用数组只适合表示 完全二叉树,因为完全二叉树最后一层连续且其它层均满,使用顺序存储不存在空间浪费

image-20221120204514765

二叉树顺序存储在 物理 上是一个 数组,在 逻辑 上是一棵 二叉树

我们这篇博客学习的堆就是使用 顺序存储 来实现。

堆的概念和结构

概念:如果有一个关键码的集合 K = {k0 , k1 , k2 , … , kn-1} ,把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中 ,并满足: Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >=K2i+2) i = 0 , 1 , 2… ,则称为小堆 ( 或大堆) 。(即双亲比孩子的数值小(大)——小(大)堆)将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆分为 大堆小堆

  • 大堆:树中所有父亲节点数据大于等于孩子节点数据
  • 小堆:树中所有父亲节点数据小于等于孩子节点数据

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆是一棵完全二叉树

说了这么多,其实判断是否为堆最好的方式就是 画图,画出堆构成的完全二叉树,看其是否符合性质。

image-20221120211655563

堆的实现

实现堆之前,我们需要了解一下概念:左孩子下标为奇数,右孩子下标为偶数

根据概念推导:

左孩子下标 = 2 * 双亲下标 + 1

右孩子下标 = 2 * 双亲下标 + 2

双亲下标 = (孩子下标 - 1) / 2 —— 这个式子是向下取整的,左右孩子都适用

结构的定义

堆是完全二叉树,其存储结构是顺序存储。那就和顺序表一样,将数据存在数组中,给定size 记录堆中元素个数,capacity 记录堆的最大容量。

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a; // 存储数据的空间
	int size; // 大小
	int capacity; // 容量
}HP;

本篇博客默认实现的是 小堆

接口总览

void HeapPrint(HP* php); // 打印
void HeapInit(HP* php); // 初始化
void HeapDestroy(HP* php); // 销毁
void HeapPush(HP* php, HPDataType x); // 堆尾插入数据
void HeapPop(HP* php); // 删除堆顶数据
HPDataType HeapTop(HP* php); // 取堆顶数据
int HeapSize(HP* hp); // 计算大小
bool HeapEmpty(HP* hp); // 判空
void AdjustUp(HPDataType* a, int child); // 向上调整
void AdjustDown(HPDataType* a, int n, int parent); // 向下调整

初始化

堆的初始化和顺序表是一样的,因为我们用的就是顺序存储:

void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;
}

销毁

堆的销毁只要释放空间,然后把 sizecapacity 置0就可以。

void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;

	php->size = php->capacity = 0;
}

插入

堆的插入就是在 数组尾部 的插入,就是 数组 的 尾插

堆插入数据只会在尾部,所以无需封装接口用来扩容,直接判断是否要扩容就可以。

堆在插入数据后,需要保持堆的结构,之前是小/大堆,在插入数据后也应该是小/大堆。当插入数据后,如果破坏了结构,就需要 向上调整

void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	// 检查容量
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	// 插入元素
	php->a[php->size++] = x;

	// 向上调整
	AdjustUp(php->a, php->size - 1);
}

向上调整

我们默认实现为 小堆,于是堆的插入就可能会造成 两种情况

  1. 插入数据 大于 它的 父亲 ,插入后,仍然为小堆,这种情况无需调整:

image-20221120221953463

  1. 插入数据 小于 它的 祖先(从根到该节点所经分支上的所有节点,就是它的父亲,爷爷等),插入后,不为小堆,此时需要将 插入数据需要向上调整,直到它为小堆:

image-20221120222748938

理清了这两个情况,再梳理一下细节:

向上调整,肯定是以 孩子为基准孩子调整到堆顶就代表着向上调整结束了。如果使用父亲为基准的话,是非正常结束的(孩子调整到0没有结束,而是通过比较值后,break退出的)。

而中间的过程就是判断孩子是否小于父亲,如果小于就交换它们的值,然后将孩子迭代为父亲,再重新计算父亲,继续调整上方;如果孩子大于等于父亲,就退出,无需调整。

通过不断向上调整元素,就可以构建出来 小堆

void Swap(HPDataType* p1, HPDataType* p2)
{
	assert(p1 && p2);

	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustUp(HPDataType* a, int child)
{
	assert(a);

	// 求父亲
	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;
		}
	}
}

建大堆 只要修改一下条件:

if (a[child]) > a[parent]

删除

堆的 删除 为删除 堆顶的数据

对于删除来说,有两个方案:

  1. 直接头删
  2. 交换 堆顶堆底 元素,尾删堆底元素,将堆顶元素 向下调整。(堆底元素就是数组尾部的元素)。

我们先看看 方案一 可不可行:

首先,由于堆是顺序存储的,那么 头删就要挪动数据,时间复杂度就为O(N)。其次,这样会 完全打乱关系

举个例子,假设 15 和 18 在第二层原本是兄弟,但是由于头删,15到了堆顶,变成了 18 的父亲。关系就乱了,感情也就淡了(doge)。18 表示 我拿你当兄弟,你却想当我父亲。但是就这一对的话,还能忍忍,但是全部的父子关系都被破坏了,所以肯定不可行。

所以,方案一就被否决了,那就只能使用 方案二 了:

方案二的话就很好,删除元素前,交换了堆顶和堆底的元素,然后将堆底尾删,尾删的时间复杂度只有O(1)。通过向下调整对堆顶元素 下调 时,也不会破坏过多的关系。

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); // 向下调整
}

向下调整

向下调整的步骤为:

  1. 找到左右孩子中的 小孩子
  2. 判断 父亲 是否大于 小孩子,如果是则交换,不是则退出
  3. 交换后将 父亲迭代到大孩子的位置,重新计算孩子。

注意找最大孩子的时候,大孩子必须存在,小心越界。

向下调整的 循环条件孩子下标 < 堆的大小,如果继续调整就越界了。

image-20221120231641781

void Swap(HPDataType* p1, HPDataType* p2)
{
	assert(p1 && p2);

	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustDown(HPDataType* a, int n, int parent)
{
	// 假设最小孩子
	int minchild = 2 * parent + 1;

	while (minchild < n)
	{
		// 找最小孩子
		if (minchild + 1 < n && a[minchild + 1] < a[minchild])
		{
			minchild++;
		}
        // 如果父亲大于孩子,调整
		if (a[parent] > a[minchild])
		{
			Swap(&a[parent], &a[minchild]); // 交换
			parent = minchild; // 迭代
			minchild = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

调大堆 只要改变两个条件:

if (minchild + 1 < n && a[minchild + 1] > a[minchild]) // 找大孩子
if (a[parent] < a[minchild]) // 如果父亲小于孩子,则交换

取堆顶数据

若堆非空,则取0下标位置数据:

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

计算堆大小

这就更简单了,直接返回 size

int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}

判空

只要 size == 0 ,堆就为空:

bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

打印堆

void HeapPrint(HP* php)
{
	assert(php);

	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

完整代码

Heap.h

#pragma once

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

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

// 堆的构建
void HeapCreate(HP* hp, HPDataType* a, int n);

void HeapPrint(HP* php);
void HeapInit(HP* php);
void HeapDestroy(HP* php);

// 保持他继续是一个堆 O(logN)
void HeapPush(HP* php, HPDataType x);

// 删除堆顶的数据,并且保持他继续是一个堆 O(logN)
void HeapPop(HP* php);

HPDataType HeapTop(HP* php);

int HeapSize(HP* hp);
// 堆的判空
bool HeapEmpty(HP* hp);

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1 

#include "Heap.h"

void HeapPrint(HP* php)
{
	assert(php);

	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

// 初始化 不开空间
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->size = php->capacity = 0;
}

// 销毁
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;

	php->size = php->capacity = 0;
}

void Swap(HPDataType* p1, HPDataType* p2)
{
	assert(p1 && p2);

	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

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

	// 算父亲
	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;
		}
	}
	
}

// 保持他继续是一个堆 O(logN)
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	// 检查容量
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	// 插入元素
	php->a[php->size++] = x;

	// 向上调整
	AdjustUp(php->a, php->size - 1);
}

// 向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
	// 假设最小孩子
	int minchild = 2 * parent + 1;

	while (minchild < n)
	{
		// 找最小孩子
		if (minchild + 1 < n && a[minchild + 1] < a[minchild])
		{
			minchild++;
		}
		if (a[parent] > a[minchild])
		{
			Swap(&a[parent], &a[minchild]);
			parent = minchild;
			minchild = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

// 删除堆顶的数据,并且保持他继续是一个堆 O(logN)
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);
}

HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}
// 堆的判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1 

#include "Heap.h"

void TestHp1()
{
	HP hp;
	HeapInit(&hp);
	
	int arr[] = { 27,15,19,18,28,34,65,49,25,37 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	
	for (int i = 0; i < sz; i++)
	{
		HeapPush(&hp, arr[i]);
	}

	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

    // 取五个最小数据
	/*int k = 5;
	while (k--)
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}*/

	HeapDestroy(&hp);
}

void TestHp2()
{
	int array[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < sizeof(array) / sizeof(int); ++i)
	{
		HeapPush(&hp, array[i]);
	}

    // 排序
	while (!HeapEmpty(&hp))
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
	}

	HeapDestroy(&hp);
}

int main()
{
	TestHp1();
	//TestHp2();
	
	return 0;
}

结语

到这里,本篇博客就到此结束了。看到这儿,相信大家也对堆有了一定的了解。

今天的内容在二叉树一块还是较简单的。下篇博客我会讲解由堆引申出的两个堆的应用 —— 堆排序 和 TopK问题。所以今天的内容还是很重要的,只有看懂下篇博客理解起来才比较清晰。

如果觉得anduin写的还不错的话,还请一键三连!如有错误,还请指正!

我是anduin,一名C语言初学者,我们下期见!

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

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

相关文章

【滤波跟踪】基于matlab不变扩展卡尔曼滤波器对装有惯性导航系统和全球定位系统IMU+GPS进行滤波跟踪【含Matlab源码 2232期】

⛄一、简介 针对室内定位中的非视距&#xff08;Non-Line-of-Sight,NLOS&#xff09;现象,提出一个新型算法进行识别,同时有效缓解其影响.主要通过超宽带&#xff08;Ultra-Wideband,UWB&#xff09;定位系统与惯性导航系统&#xff08;Inertial Navigation System,INS&#x…

酒店管理系统的设计与实现

Word下载链接如下&#xff1a; https://download.csdn.net/download/yw1990128/87096359 一 设计背景 1.1 课题现状 随着国家社会经济水平的提升&#xff0c;各酒店的发展速度越来越快&#xff0c;入住人员也越来越多。酒店房间的管理要求也愈来愈大&#xff0c;所以很多酒店正…

45.Django模板

1.django模板配置 1.1 Django模板概述 作为一个Web框架&#xff0c;Django需要一种方便的方式来动态生成HTML。最常用的方法依赖于模板。模板包含所需HTML输出的静态部分以及描述如何插入动态内容的特殊语法。 ​ 对模板引擎的一般支持和Django模板语言的实现都存在于 djang…

Linux下的NFS服务(包含windows10下的nfs搭建)

目录 1.NFS服务介绍 2.Linux下搭建NFS服务 &#xff08;1&#xff09;下载NFS服务端 &#xff08;2&#xff09;新建一个共享文件 &#xff08;3&#xff09;修改NFS服务配置文件 &#xff08;4&#xff09;重新启动NFS服务 &#xff08;5&#xff09;显示查看共享的文件…

38、常用类之String类

1、基本介绍&#xff1a; String s5new String(byte[] b)&#xff1b; &#xff08;5&#xff09;String实现了Serializable&#xff0c;说明String可以串行化&#xff0c;即可以网络传输 String实现了Comparable&#xff0c;说明String对象可以比较 &#xff08;6&#xff0…

JavaScript基础(13)_原型、原型对象

上一章构造函数确实简化了多个对象创建的麻烦问题&#xff0c;但是&#xff1a;构造函数每创建一个实例&#xff0c;构造函数就会执行一次&#xff0c;将属性和方法添加到该对象&#xff0c;每个对象实例化后地址互不相同&#xff0c;即使它们的方法所实现的逻辑和功能一样&…

pytorch初学笔记(八):神经网络之卷积操作

目录 一、卷积操作 二、二维卷积操作 2.1 torch.nn.functional 2.2 conv2d方法介绍 2.2.1 使用该方法需要引入的参数 2.2.2 常用参数 2.2.3 关于对input和weight的shape详解 三、代码实战 3.1 练习要求 3.2 tensor的reshape操作 3.3 不同stride的对比 3.4 不同pad…

Docker面试

1. Docker和虚拟机的区别&#xff1f; 虚拟机Virtual Machine与容器化技术&#xff08;代表Docker&#xff09;都是虚拟化技术&#xff0c;两者的区别在于虚拟化的程度不同。 隔离性 由于vm对操作系统也进行了虚拟化&#xff0c;隔离的更加彻底。而Docker共享宿主机的操作系统…

数字化转型总体需求

基于“两型三化九力”对企业数字化的要求&#xff0c;以建设产品全生命周期管理平台为手段和途径&#xff0c;打通设计、工艺、制造及交付服务的全生命周期的数字线&#xff0c;实现数字化设计、数字化仿真、数字化制造、数字化服务及数字化管理&#xff0c;未来以此为基础实现…

【计算机毕业设计】11.毕业生信息管理系统+vue

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 毕业生信息招聘平台&#xff0c;主要的模块包括查看管理员&#xff1b;首页、个…

zk常用命令ls、ls2、get、stat,参数意思(重补早期学习记录)

前言:补学习记录,几年前写一半丢草稿箱,突然看到,有强迫症所以补完 1.连接zk客户端(进入zk后台) ./zkCli.sh 连接成功 使用help查看有哪些命令可以使用 试试ls和ls2的区别 ls显示指定路径下的目录 ls2不仅可以 显示指定路径下的目录,还可以显示该节点的相关状态信息…

OpenGL 单色

目录 一.OpenGL 单色图 1.IOS Object-C 版本1.Windows OpenGL ES 版本2.Windows OpenGL 版本 二.OpenGL 单色 GLSL Shader三.猜你喜欢 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础 零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >…

非关系型数据库MongoDB是什么/SpringBoot如何使用或整合MongoDB

写在前面&#xff1a; 继续记录自己的SpringBoot学习之旅&#xff0c;这次是SpringBoot应用相关知识学习记录。若看不懂则建议先看前几篇博客&#xff0c;详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用&#xff01; 3.4.3.3 Mongodb 3.4.3.3.1 介绍 MongoDB是一个开…

【Tomcat专题】Tomcat如何打破双亲委派机制?

文章目录类加载器双亲委派机制双亲委派的好处Tomcat的类加载器loadClass总体加载步骤&#xff1a;类加载器 三种JDK内部的类加载器 启动类加载器&#xff08;BootStrap ClassLoader&#xff09; 负责加载JRE\lib下的rt.jar、resources.jar、charsets.jar包中的class。 扩展…

一文带你搞懂sklearn.metrics混淆矩阵

一般的二分类任务需要的评价指标有4个 accuracyprecisionrecallf1-score 四个指标的计算公式如下 计算这些指标要涉及到下面这四个概念&#xff0c;而它们又构成了混淆矩阵 TP (True Positive)FP (False Positive)TN (True Negative)FN (False Negative) 混淆矩阵实际值01预测…

周杰伦腾格尔晚上八点同时开线上演唱会,究竟是巧合还是刻意安排

从日历上面看&#xff0c;2022年11月19日&#xff0c;是一个再平凡不过的日子&#xff0c;不过有了周杰伦和腾格尔的加持&#xff0c;这个平凡的日子也变得不平凡了。根据腾格尔老师本人透露&#xff0c;他准备在11月19日&#xff0c;在某音平台开启线上演唱会&#xff0c;为歌…

智慧实验室解决方案-最新全套文件

智慧实验室解决方案-最新全套文件一、建设背景二、建设架构智慧实验室建设核心目标三、建设方案四、获取 - 智慧实验室全套最新解决方案合集一、建设背景 当前高校和中小学的智慧校园建设正如火如荼地进行中&#xff0c;智慧实验室建设属于“智慧校园”建设的重要组成部分之一…

ctf_BUUCTF_web(1)

文章目录BUUCTF_webSQL注入1. [极客大挑战 2019]EasySQL2. [SUCTF 2019]EasySQL3.[强网杯 2019]随便注4.[极客大挑战 2019]BabySQL5.[BJDCTF2020]Easy MD56.[极客大挑战 2019]HardSQL7.[GXYCTF2019]BabySQli8.[GYCTF2020]Blacklist9.[CISCN2019 华北赛区 Day2 Web1]Hack World1…

面试:HTTP 的长连接和短连接

https://www.cloudflare.com/zh-cn/learning/ddos/syn-flood-ddos-attack/ 一文搞懂 HTTP 的长连接和短连接_文晓武的博客-CSDN博客 1、HTTP 协议与 TCP/IP 协议的关系 HTTP 的长连接和短连接本质上是 TCP 长连接和短连接。HTTP 属于应用层协议&#xff0c;在传输层使用 TCP…

区块链交易明细中各字段的含义

Transaction Hash&#xff1a;标识本次交易的 hashStatus&#xff1a;交易状态Block&#xff1a;7768188 表示本次块高&#xff0c;217034 表示在 7768188 后面又新挖的区块数量&#xff0c;该数值会随着新区块增加而不断增长Timestamp&#xff1a;交易成功的时间戳From&#x…