【初阶数据结构】森林里的树影 “堆” 光:堆

ops/2025/2/27 4:35:43/

文章目录

  • 1.的概念及结构
  • 2.的接口实现
    • 2.1 的初始化
    • 2.2 的销毁
    • 2.3 的交换
    • 2.4 的向上调整
    • 2.5 的插入
    • 2.6 的向下调整
    • 2.7 的删除
    • 2.8 顶获取
    • 2.9 的判空
    • 2.10 的节点个数
    • 2.11 的打印
    • 2.12 的排序(向上建
    • 2.13 的排序(向下建
  • 3.排序效率比较
  • 4.代码展示
    • 4.1 Heap.h
    • 4.2 Heap.c
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力!

初步了解了关于型结构的知识与结构后,的功能实现能帮我们学会一种排序——排序,二叉也是很重要的一种文件式的结构

在学习本专题前,请详细学习有关的知识与结构

传送门:【初阶数据结构型数据的勘探:(暂未出版)

1.的概念及结构

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

的性质:

🚩中某个结点的值总是不大于不小于父结点的值

🚩总是一棵完全二叉

的结构:

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

的本质就是一个动态数组,把数组以的形式呈现出来而已,所以在实现的功能时要多画图来帮助理解

2.的接口实现

2.1 的初始化

void HeapInit(HP* php)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;
}

熟悉的初始化配方:

  1. 判断是否为空指针
  2. 创建空间,注意是否会因为开辟失败造成空指针
  3. 初始化变量 sizecapacity

🔥值得注意的是: 参数的指针浅拷贝,指向同一块空间,能够改变该空间里的内容,不涉及改变原空间的地址,所以传一级指针即可

2.2 的销毁

void HeapDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = php->size = 0;
}

2.3 的交换

void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}

因为在调整中涉及多次的节点交换,所以额外写一个 Swap 交换函数方便使用

2.4 的向上调整

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 = (child - 1) / 2;}else{break;}}
}

向上调整通常用于最大的调整

当向最大中插入一个新元素时,新元素会被放置在的末尾(即数组的最后一个位置),此时可能会破坏的性质(最大要求每个节点的值都大于或等于其子节点的值

通过调用 AdjustUp 函数,可以将新插入的元素上浮到合适的位置,使得整个重新满足最大的性质

请添加图片描述

🔥值得注意的是:

  1. 无论是左节点还是右节点父亲节点的索引可以通过 (child - 1) / 2 计算得出
  2. while 循环的条件 child > 0 不能写成 child >= 0。当 child 等于 0 时,表示已经到达顶,按照 parent = (child - 1) / 2 计算,(-1) / 2 结果为 0整数除法向下取整),这会导致 parentchild 相等,无法向上调整,陷入类似死循环的效果
  3. 除了 child 这个数据,前面的数据构成,因为向上调整的前提是其他数据都成

2.5 的插入

void HeapPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){HPDataType* tmp = (HPDataType*)realloc(php->a, php->capacity * 2 * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity *= 2;}php->a[php->size] = x;AdjustUp(php->a, php->size);php->size++;
}

实现了向上调整,那么插入就很容易了,检查容量是否足够插入,调整新插入的数据也构成即可

🔥值得注意的是: 不是 php->capacity * 2 ,而是 php->capacity * 2 * sizeof(HPDataType),注意是字节数不是容量数

2.6 的向下调整

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;}if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}

向下调整通常用于最小的调整(这里写的是最大的调整)

向下调整的前提是左右子都必须是大或者小

请添加图片描述
🔥值得注意的是:

  1. 必须把大的那个孩子往上调,保证的性质
  2. 这里假设子节点中的左孩子 child = parent * 2 + 1 是子节点中最大的
  3. child + 1 < n && a[child + 1] > a[child] 的目的是判断是否存在右子节点,如果存在,再判断左子节点是否大于右子节点,如果大于就交换
  4. 不能写成 a[child + 1] > a[child] && child + 1 < n,要先判断是否存在再访问,不然就非法越界了

2.7 的删除

在写删除代码前,我们要思考一个问题:

为什么不用像之前顺序表那样常用的删除方法——后一个覆盖前一个?

在这里插入图片描述

比如一个数组 {42,12,18,4,2,3}

如果采用后一个覆盖前一个的方法删除节点:

  1. 向下调整的效率是 O( l o g n log_n logn) ,后一个覆盖前一个的效率是 O(n) ,假设需要挪动100万次,那么向下调整只需最多20次就能解决,这效率翻得可不止一倍两倍
  2. 后一个覆盖前一个之后父子关系全乱,无法进行调整

删除通常是删除顶的数据,将顶的数据最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

那么为什么是采用最后一个叶节点和根节点交换?

在这里插入图片描述

画图可以发现这种交换方式,不会太大程度上破坏的结构,保证能够进行向下调整来恢复的秩序

请添加图片描述

🔥值得注意的是: 删除特定位置元素的方法删除是一样的

  1. 遍历整个来找到目标元素位置
  2. 目标位置的元素的最后一个元素进行交换
  3. 根据交换后该元素的值与周围元素的大小关系,决定是进行上浮操作还是下沉操作来恢复的性质

因此删除的代码也能轻松写出来:

void HeapPop(HP* php)
{assert(php);assert(!HeapEmpty(php));Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}

2.8 顶获取

HPDataType HeapTop(HP* php)
{assert(php);return php->a[0];
}

返回顶元素的值,最大顶元素是中的最大值最小顶元素是中的最小值

2.9 的判空

bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}

如果 php->size 等于 0,说明中没有元素,函数返回 true;否则返回 false

2.10 的节点个数

int HeapSize(HP* php)
{assert(php);return php->size;
}

设置一个返回 size 的函数方便获取的节点个数,否则获取 size 只能通过遍历

2.11 的打印

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

2.12 的排序(向上建

既然有调整大小顺序的性质,那么就可以据此实现排序的功能

我们知道无论是向上调整,还是向下调整,都要基于一个具有完整性质的下来实现,分为向上建向下建

向上建

向上建的核心思想是逐个插入元素到,每插入一个元素就对其进行向上调整操作,使其满足的性质。具体来说,从数组的第一个元素开始,依次将每个元素插入到已经构建好的部分中,然后通过上浮操作将该元素调整到合适的位置,确保整个数组始终保持的性质。其实就是对插入的模拟实现

建好之后,就能对进行排序,排序分为升序降序

  • 升序:建
  • 降序:建

为什么排序是这样建呢?

在这里插入图片描述

假设我们要实现升序如果是建小的话,最小值就放在最上面不能动了,剩下的数如果想排序那又得看成一个,但是这样父子关系全乱了,因此每排好一个数就要重新建,那么效率就太低了

请添加图片描述

实现升序就要考虑建大,然后再模拟删除的方式,不断把大的值调到最下面最上面小的值通过向下调整,把调整为正常的大,保证左右子都是大。此时最大的值又在最顶上,--end,和倒数第二个节点交换,重复上面的操作,循环往复就能实现排序

因此排序的代码为:

void HeapSort(HPDataType* a, int n)
{//向上调整建,i是下标,n是个数,a是数组 -- O(N * log N)for (int i = 0; i < n; ++i){AdjustUp(a, i);}int end = n - 1;while (end > 0){Swap(&a[end], &a[0]);AdjustDown(a, end, 0);--end;}
}

2.13 的排序(向下建

向下建

向下建的过程可以看作是一种分治策略,将一个大问题分解为多个小问题。向下建的核心思想是从最后一个非叶子节点开始,依次对每个非叶子节点进行向下调整(下沉)操作,使得以该节点为根的子满足的性质,最终整个数组构成一个简单来说就是对下面每一个小依次调整,最终整体就形成了一个大

在这里插入图片描述

假设有个数组{2,1,5,7,6,8,0,9,4,3},要实现升序建大

  1. 先从 6 这个开始调整
  2. 然后按数组顺序往前推,调整 7 这个
  3. 依次往前推对每个调整,最终就建成了一个大

建好之后,排序的方法和向上建是一样的

因此排序的代码为:

void HeapSort(HPDataType* a, int n)
{//向下调整建 -- O(N)//n-1是最后一个节点,(n-1-1)/2是最后一个节点的父节点for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){Swap(&a[end], &a[0]);AdjustDown(a, end, 0);--end;}
}

3.排序效率比较

向下建
请添加图片描述
因此:向下建的时间复杂度为O(N)

用同样的方法,也能算出向上建的时间复杂度为O(N * log N)

所以向下建是更高效的

4.代码展示

传送门:Gitee代码

4.1 Heap.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>typedef int HPDataType;typedef struct Heap
{HPDataType* a;int size;int capacity;
}HP;void HeapInit(HP* php);void HeapDestroy(HP* php);void Swap(HPDataType* p1, HPDataType* p2);void AdjustUp(HPDataType* a, int child);void AdjustDown(HPDataType* a, int n, int parent);void HeapPush(HP* php, HPDataType x);void HeapPop(HP* php);HPDataType HeapTop(HP* php);bool HeapEmpty(HP* php);int HeapSize(HP* php);void HeapPrint(HP* php);void HeapSort(HPDataType* a, int n);

4.2 Heap.c

#define _CRT_SECURE_NO_WARNINGS#include "Heap.h"void HeapInit(HP* php)
{assert(php);php->a = (HPDataType*)malloc(sizeof(HPDataType) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;
}void HeapDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = php->size = 0;
}void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}//除了child这个数据,前面的数据构成
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 = (child - 1) / 2;}else{break;}}
}void HeapPush(HP* php, HPDataType x)
{assert(php);if (php->size == php->capacity){HPDataType* tmp = (HPDataType*)realloc(php->a, php->capacity * 2 * sizeof(HPDataType));if (tmp == NULL){perror("realloc fail");return;}php->a = tmp;php->capacity *= 2;}php->a[php->size] = x;AdjustUp(php->a, php->size);php->size++;
}//左右子都必须是大或者小
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;}if (a[child] > a[parent]){Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}void HeapPop(HP* php)
{assert(php);assert(!HeapEmpty(php));Swap(&php->a[0], &php->a[php->size - 1]);php->size--;AdjustDown(php->a, php->size, 0);
}HPDataType HeapTop(HP* php)
{assert(php);return php->a[0];
}bool HeapEmpty(HP* php)
{assert(php);return php->size == 0;
}int HeapSize(HP* php)
{assert(php);return php->size;
}void HeapPrint(HP* php)
{int i;for (i = 0; i < php->size; ++i){printf("%d ", php->a[i]);}printf("\n");
}void HeapSort(HPDataType* a, int n)
{//向上调整建,i是下标,n是个数 -- O(N * log N)for (int i = 0; i < n; ++i){AdjustUp(a, i);}/*向下调整建 -- O(N)n-1是最后一个节点,(n-1-1)/2是最后一个节点的父节点*//*for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}*/int end = n - 1;while (end > 0){Swap(&a[end], &a[0]);AdjustDown(a, end, 0);--end;}
}

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!


http://www.ppmy.cn/ops/161591.html

相关文章

C# String.Intern 方法 详解

总目录 前言 在C#开发中&#xff0c;字符串作为最常用的数据类型之一&#xff0c;其内存管理直接影响程序性能。当处理海量文本数据时&#xff0c;重复字符串的内存占用可能成为性能瓶颈。string.Intern 方法正是为解决这一问题而生的核心工具&#xff0c;它通过字符串驻留池&…

网络原理--TCP的特性

TCP报文的结构&#xff1a; TCP的报头前20字节是固定长度&#xff0c;也可以通过“选项”来增加。 一、用来确保可靠性&#xff0c;最核心的机制&#xff0c;称为“确认应答” 引入一个情景&#xff1a; A向B询问cat和dog的意思&#xff1a; 这种情况是理想情况&#xff0c;…

Python的那些事第三十一篇:快速数据帧处理与可视化的高效工具Vaex

Vaex:快速数据帧处理与可视化的高效工具 摘要 在大数据时代,高效的数据处理和可视化工具对于数据科学家和分析师至关重要。Vaex作为一种开源的Python库,专为处理超大数据集而设计,通过惰性计算、内存映射和并行化技术,显著提升了数据处理的效率和性能。本文详细介绍了Va…

k8s集群内的pod连接集群外部的mysql, k8s集群内部服务如何连接集群外部mysql? 一文搞明白

一、为什么不将mysql服务部署到k8s集群中使用呢&#xff1f; 1.有状态服务在K8s中的管理比较复杂&#xff0c;特别是持久化存储的问题。虽然K8s有StatefulSet和PV/PVC&#xff0c;但配置和维护起来需要更多工作,同时以下问题仍需解决&#xff1a;-存储可靠性&#xff1a;如果使…

给SQL server数据库表字段添加注释SQL,附修改、删除注释SQL及演示

目录 一. 前提小知识(数据库连接&#xff0c;数据库&#xff0c;SCHEMA&#xff0c;Table的关系) 二. 添加备注 2.1 添加备注基本语法(sys.sp_addextendedproperty) 2.2 SQL演示 2.3?fn_listextendedproperty函数查询备注个数 2.4 开发常用添加注释语法 三. 修改备注 …

【Windows系统node_modules删除失败(EPERM)问题解析与应对方案】

Windows系统node_modules删除失败(EPERM)问题解析与应对方案 问题现象 当开发者尝试删除Node.js项目的node_modules目录时&#xff0c;常会遇到如下错误提示&#xff1a; [Error: EPERM: operation not permitted, unlink D:\project\...\esbuild.exe] {errno: -4048,code: …

Solidity 开发环境

Solidity 开发环境 Solidity编辑器&#xff1a;Solidity编辑器是⼀种专⻔⽤于编写和编辑Solidity代码的编辑器。常⽤的Solidity编辑器包括 Visual Studio Code、Atom和Sublime Text。以太坊开发环境&#xff1a;以太坊开发环境&#xff08;Ethereum Development Environment&a…

爬虫运行后如何保存数据?

爬虫运行后&#xff0c;将获取到的数据保存到本地或数据库中是常见的需求。Python 提供了多种方式来保存数据&#xff0c;包括保存为文本文件、CSV 文件、JSON 文件&#xff0c;甚至存储到数据库中。以下是几种常见的数据保存方法&#xff0c;以及对应的代码示例。 1. 保存为文…