二叉树 - 堆 | 数据结构中的小技巧大作用


在这里插入图片描述

📷 江池俊: 个人主页
🔥个人专栏: ✅数据结构冒险记 ✅C语言进阶之路
🌅 有航道的人,再渺小也不会迷途。


在这里插入图片描述

文章目录

    • 一、堆的概念及介绍
    • 二、结构图示
    • 三、堆的代码实现(图解)
      • 3.1 创建堆结构体即接口
      • 3.2 堆的初始化 && 交换两个数(用于parent 和 child 的交换 )
      • 3.3 堆的向上调整
      • 3.4 堆向下调整算法(以小堆为例)
      • 3.5 堆的创建
        • 【向上调整建堆时间复杂度】
        • 【向下调整建堆时间复杂度】
      • 3.6 堆的插入
      • 3.7 堆的删除
      • 3.8 取堆顶的数据
      • 3.9 求堆的数据个数
      • 3.10 堆的判空
    • 四、源代码
      • 4.1 Heap.h文件
      • 4.2 Heap.c文件
      • 4.3 Test.c文件

一、堆的概念及介绍

堆(Heap)是计算机科学中一类特殊的数据结构的统称。
堆通常是一个可以被看做一棵完全二叉树的数组。

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

堆满足下列性质:

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

将根节点最大的堆叫做最大堆大根堆,根节点最小的堆叫做最小堆小根堆
在这里插入图片描述
在这里插入图片描述


二、结构图示

二叉堆是一颗完全二叉树,且堆中某个节点的值总是不大于其父节点的值,该完全二叉树的深度为 k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层所有的结点都连续集中在最左边。

其中堆的根节点最大称为最大堆,如下图所示:
在这里插入图片描述
我们可以使用数组存储二叉堆,右边的标号是数组的索引。
在这里插入图片描述

在这里插入图片描述

假设当前元素的索引位置为 i,可以得到规律:

parent(i) = i/2(取整)
left child(i) = 2*i+1
right child(i) = 2*i +2

三、堆的代码实现(图解)

3.1 创建堆结构体即接口

typedef int HPDataType; //数据元素类型
typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap; //堆的结构//堆的初始化 (可要可不要)
void HeapInit(Heap* hp);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

3.2 堆的初始化 && 交换两个数(用于parent 和 child 的交换 )

// 堆的初始化
void HeapInit(Heap* hp)
{assert(hp);hp->a = NULL;hp->size = 0;hp->capacity = 0;
}
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType temp = *p1;*p1 = *p2;*p2 = temp;
}

3.3 堆的向上调整

注意:此算法的前提是 在进行向上调整前此树已经是堆
向上调整操作用于在插入新元素时保持堆的性质。

算法思想如下:

  1. 首先,计算给定节点的父节点索引。如果当前节点是根节点(即索引为0),则没有父节点,不需要进行向上调整。

  2. 然后,进入一个循环,条件是当前节点的索引大于0。这是因为根节点已经是堆中的最大值(对于大顶堆)或最小值(对于小顶堆),无需再向上调整。

  3. 在循环中,比较当前节点和其父节点的值。如果当前节点的值小于其父节点的值(对于大顶堆)或大于其父节点的值(对于小顶堆),则需要进行向上调整。

  4. 交换当前节点和其父节点的值,将父节点移动到正确的位置。然后更新当前节点的索引为父节点的索引,并重新计算父节点的索引。

  5. 如果当前节点的值大于或等于其父节点的值(对于大顶堆)或小于或等于其父节点的值(对于小顶堆),则说明已经到达了正确的位置,可以跳出循环。

通过以上步骤,可以实现向上调整操作,确保堆的性质得到维护。
在这里插入图片描述

//向上调整 --- 插入时使用,保证堆的结构
void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;//while(parent >= 0)while (child > 0){if (a[child] < a[parent]) //< 改成 > 就是大堆的向上调整{Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}

时间复杂度分析
最坏的情况下是从第一个非叶子节点一路比较到根节点,比较的次数为完全二叉树的高度-1,即时间复杂度为 O(log2N)

3.4 堆向下调整算法(以小堆为例)

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

int array[] = {27,15,19,18,28,34,65,49,25,37};

在这里插入图片描述

算法思想如下:

  1. 假设当前节点的左孩子为最小值节点。
  2. 判断当前节点是否有右孩子,如果有且右孩子的值小于左孩子的值,则将右孩子的下标赋值给child
  3. 如果当前节点的值大于child节点的值,说明需要向下调整,交换当前节点和child节点的值。
  4. 更新parentchild,继续向下调整。
  5. 如果当前节点的值小于等于child节点的值,说明已经找到合适的位置,跳出循环。

通过以上步骤,可以实现向下调整操作,确保堆的性质得到维护。
在这里插入图片描述

//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{//假设左孩子小int child = parent * 2 + 1;while (child < size){//如果右孩子更小,则将child的下标置为右孩子的下标if (child + 1 < size && a[child] > a[child + 1]) //后面的 “>” 改成 “<”即为大堆的向下调整{child++;}if (a[child] < a[parent]) // “<”改成“>”即为大堆的向下调整{Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}

时间复杂度分析
最坏的情况即图示的情况,从根一路比较到叶子节点,比较的次数为完全二叉树的高度,即时间复杂度为 O(log2N)

3.5 堆的创建

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

int a[] = {1,5,3,8,7,6}; 

在这里插入图片描述
【1】向上调整建堆

//向上调整建对堆 --- 0(N*logN)
int n = sizeof(a) / sizeof(a[0]);for (int i = 1; i < n; i++)
{AdjustUp(a, i);
}

【2】向下调整建堆

// 向下要调整建堆 --- O(N) 
int n = sizeof(a) / sizeof(a[0]);
//找倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{AdjustDown(a, n, i);
}

【3】模拟堆插入的过程建堆

// 堆的构建 --- 小堆 O(logN)
void HeapCreate(Heap* hp, HPDataType* a, int n)
{//模拟堆插入的过程建堆assert(hp);hp->a = (HPDataType*)malloc(sizeof(HPDataType)*n);hp->size = 0;hp->capacity = n;for (int i = 0; i < n; i++){HeapPush(hp, a[i]);}
}
【向上调整建堆时间复杂度】

在这里插入图片描述

【向下调整建堆时间复杂度】

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

在这里插入图片描述

3.6 堆的插入

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

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{assert(hp);if (hp->size == hp->capacity){int newcapacity = hp->capacity == 0 ? 4 : hp->size * 2;HPDataType* temp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);if (temp == NULL){perror("realloc fail");exit(-1);}hp->a = temp;hp->capacity = newcapacity;}hp->a[hp->size++] = x;AdjustUp(hp->a, hp->size - 1);
}

3.7 堆的删除

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

// 堆的删除
void HeapPop(Heap* hp)
{assert(hp);assert(hp->size > 0);Swap(&hp->a[0], &hp->a[hp->size - 1]);hp->size--;//从父亲的位置开始往下调AdjustDown(hp->a, hp->size, 0);
}

3.8 取堆顶的数据

// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{assert(hp);assert(hp->size > 0);return hp->a[0];
}

3.9 求堆的数据个数

// 求堆的数据个数
int HeapSize(Heap* hp)
{assert(hp);return hp->size;
}

3.10 堆的判空

// 堆的判空
int HeapEmpty(Heap* hp)
{assert(hp);return hp->size == 0;
}

四、源代码

4.1 Heap.h文件

#pragma once#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int HPDataType; //数据元素类型
typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap; //堆的结构//堆的初始化(可要可不要)
void HeapInit(Heap* hp);
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);

4.2 Heap.c文件

#define _CRT_SECURE_NO_WARNINGS 1#include "Heap.h"// 堆的初始化
void HeapInit(Heap* hp)
{assert(hp);hp->a = NULL;hp->size = 0;hp->capacity = 0;
}// 堆的构建 --- 小堆
void HeapCreate(Heap* hp, HPDataType* a, int n)
{assert(hp);hp->a = (HPDataType*)malloc(sizeof(HPDataType)*n);hp->size = 0;hp->capacity = n;for (int i = 0; i < n; i++){HeapPush(hp, a[i]);}
}// 堆的销毁
void HeapDestory(Heap* hp)
{assert(hp);free(hp->a);hp->a = NULL;hp->size = hp->capacity = 0;
}void Swap(HPDataType* p1, HPDataType* p2)
{HPDataType temp = *p1;*p1 = *p2;*p2 = temp;
}//向上调整 --- 插入时使用,保证堆的结构
void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;//while(parent >= 0)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(Heap* hp, HPDataType x)
{assert(hp);if (hp->size == hp->capacity){int newcapacity = hp->capacity == 0 ? 4 : hp->size * 2;HPDataType* temp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);if (temp == NULL){perror("realloc fail");exit(-1);}hp->a = temp;hp->capacity = newcapacity;}hp->a[hp->size++] = x;AdjustUp(hp->a, hp->size - 1);
}//向下调整 --- 删除的时候使用
void AdjustDown(HPDataType* a, int size, int parent)
{//假设左孩子小int child = parent * 2 + 1;while (child < size){//如果右孩子更小,则将child的下标置为右孩子的下标if (child + 1 < size && a[child] > a[child + 1]) //后面的 “>” 改成 “<”{child++;}if (a[child] < a[parent]) // “<”改成“>”即为大堆的向下调整{Swap(&a[child], &a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}}// 堆的删除
void HeapPop(Heap* hp)
{assert(hp);assert(hp->size > 0);Swap(&hp->a[0], &hp->a[hp->size - 1]);hp->size--;//从父亲的位置开始往下调AdjustDown(hp->a, hp->size, 0);
}
// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{assert(hp);assert(hp->size > 0);return hp->a[0];
}
// 求堆的数据个数
int HeapSize(Heap* hp)
{assert(hp);return hp->size;
}
// 堆的判空
int HeapEmpty(Heap* hp)
{assert(hp);return hp->size == 0;
}

4.3 Test.c文件

#define _CRT_SECURE_NO_WARNINGS 1#include "Heap.h"void Test1()
{int a[] = { 4,6,2,1,5,8,2,9 };Heap hp;int len = sizeof(a) / sizeof(a[0]);//HeapInit(&hp);模拟堆插入的过程建堆//for (int i = 0; i < len; i++)//{//	HeapPush(&hp, a[i]);//}HeapCreate(&hp, a, len);//打印堆中前k个元素/*int k = 4;while (k--){printf("%d ", HeapTop(&hp));HeapPop(&hp);}*///打印堆while (!HeapEmpty(&hp)){printf("%d ", HeapTop(&hp)); HeapPop(&hp);}printf("\n");
}int main()
{Test1();return 0;
}

今天的分享到此结束,后续将继续向大家带来更多数据结构的小知识!

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

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

相关文章

HackTheBox - Medium - Linux - Noter

Noter Noter 是一种中型 Linux 机器&#xff0c;其特点是利用了 Python Flask 应用程序&#xff0c;该应用程序使用易受远程代码执行影响的“节点”模块。由于“MySQL”守护进程以用户“root”身份运行&#xff0c;因此可以通过利用“MySQL”的用户定义函数来利用它来获得RCE并…

快速上手的AI工具-文心3.5vs文心4.0

前言 大家好晚上好&#xff0c;现在AI技术的发展&#xff0c;它已经渗透到我们生活的各个层面。对于普通人来说&#xff0c;理解并有效利用AI技术不仅能增强个人竞争力&#xff0c;还能在日常生活中带来便利。无论是提高工作效率&#xff0c;还是优化日常任务&#xff0c;AI工…

一篇文章看懂云渲染,云渲染是什么?云渲染如何计费?云渲染怎么选择

云渲染是近年兴起的新行业&#xff0c;很多初学者对它不是很了解&#xff0c;云渲染是什么&#xff1f;为什么要选择云渲染&#xff1f;它是如何计费的又怎么选择&#xff1f;这篇文章我们就带大家了解下吧。 云渲染是什么 云渲染简单来说就是把本地的渲染工作迁移到云端进行的…

以超市数据微案例-fineBI可视化分析

一、入门案例&#xff1a; 2.分析思路&#xff1a; 数据清晰界面中添加毛利额计算 **所以在新增步骤之后&#xff0c;必须点击保存并更新&#xff0c;否则可视化界面中无法使用最新的数据 4、数据可视化分析 1&#xff09;销售额最高的十大商品种类 为1-8月超市数据&#xff…

响应式编程

Reactive-Stream Reactive Streams是JVM面向流的库的标准和规范 1、处理可能无限数量的元素 2、有序 3、在组件之间异步传递元素 4、强制性非阻塞,背压模式 在Java中,常用的背压机制是响应式流编程中的反压(Reactive Streams Backpressure)。反压是一种生产者-消费者模型,…

【刷题笔记4】

动态规划题目汇总 斐波那契数列&#xff1a;1&#xff0c;1&#xff0c;2&#xff0c;3&#xff0c;5&#xff0c;8&#xff0c;13…… 递归一把解决三类问题&#xff1a;1.数据定义是按照递归的&#xff08;斐波那契数列&#xff09;。2.问题解法是按递归算法实现的。 3.数据…

JUC-Java内存模型JMM

JMM概述 Java Meory Model java内存模型。在不同的硬件和不同的操作系统上&#xff0c;对内存的访问方式是不一样的。这就造成了同一套java代码运行在不同的操作系统上会出问题。JMM就屏蔽掉硬件和操作系统的差异&#xff0c;增加java代码的可移植性。这是一方面。 另一方面JM…

ios适配虚拟home键

在H5开发过程中遇到一个兼容性问题。iphone手机的虚拟home键会对屏幕底部的内容造成遮挡。要处理此问题&#xff0c;需要清楚安全区域这个概念。 安全区域 根据刘海和虚拟Home键&#xff0c;Apple为其设备提供了屏幕安全区域的视觉规范 竖屏&#xff1a;竖屏的时候&#xff…

UE5 C++学习笔记 常用宏的再次理解

1.随意创建一个类&#xff0c;他都有UCLASS()。GENERATED_BODY()这样的默认的宏。 UCLASS() 告知虚幻引擎生成类的反射数据。类必须派生自UObject. &#xff08;告诉引擎我是从远古大帝UObject中&#xff0c;继承而来&#xff0c;我们是一家人&#xff0c;只是我进化了其他功能…

矩阵和矩阵如何相乘?

矩阵与矩阵相乘遵循特定的数学规则。为了相乘&#xff0c;第一个矩阵的列数必须等于第二个矩阵的行数。矩阵乘法的结果是一个新矩阵&#xff0c;其行数等于第一个矩阵的行数&#xff0c;列数等于第二个矩阵的列数。矩阵乘法不满足交换律&#xff0c;即 AB≠BA。 例子&#xff…

131. 分割回文串 - 力扣(LeetCode)

问题描述 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 输入示例 s "aab"输出示例 [["a","a","b"],["…

Unity 面试篇|(九)操作系统与网络篇 【全面总结 | 持续更新】

目录 1. 客户端与服务器交互方式有几种&#xff1f;2. OSI七层模型有哪些&#xff0c;每一层的作用3. UDP/TCP含义&#xff0c;区别4. TCP/IP协议栈各个层次及分别的功能&#xff1f;5. 写出WWW的几个方法&#xff1f;6. Socket粘包7. Socket的封包、拆包8. Socket 客户端 队列…

数学建模常见算法的通俗理解(2)

目录 6 K-Means&#xff08;K-均值&#xff09;聚类算法&#xff08;无需分割数据即可分类&#xff09; 6.1 粗浅理解 6.2 算法过程 6.2.1 选定质心 6.2.2 分配点 6.2.3 评价 7 KNN算法&#xff08;K近邻算法&#xff09;&#xff08;K个最近的决定方案&#xff09; 7.…

【每日一题】按分隔符拆分字符串

文章目录 Tag题目来源解题思路方法一&#xff1a;遍历方法二&#xff1a;getline 写在最后 Tag 【遍历】【getline】【字符串】【2024-01-20】 题目来源 2788. 按分隔符拆分字符串 解题思路 方法一&#xff1a;遍历 思路 分隔符在字符串开始和结束位置时不需要处理。 分隔…

Crow:实现点击下载功能

Crow:设置网站的index.html-CSDN博客 讲述了如何完成一个最简单的网页的路由 很多网页提供了下载功能,怎么实现呢,其实也很简单。 假设网页的目录结构如图 $ tree static static ├── img │ └── goodday.jpg └── index.html //index.html <html> <body&…

专业137总分439东南大学920专业基础综合考研经验电子信息与通信电路系统芯片

我本科是南京信息工程大学&#xff0c;今年报考东南大学信息学院&#xff0c;成功逆袭&#xff0c;专业137&#xff0c;政治69&#xff0c;英语86&#xff0c;数一147&#xff0c;总分439。以下总结了自己的复习心得和经验&#xff0c;希望对大家复习有一点帮助。啰嗦一句&…

C++ :命名空间域

目录 冲突与命名&#xff1a; 举个例子&#xff1a; 全局与局部&#xff1a; 域作用限定符&#xff1a; 命名空间域&#xff1a; 冲突与命名&#xff1a; 在C语言中&#xff0c;我们通常会使用stdlib.h 而stdlib.h 本质上是一个函数的库&#xff0c;在程序中使用的大多数…

Java学习笔记(八)——Lambda表达式

文章目录 Lambda表达式Lambda表达式的省略写法Lambda练习练习1练习2 算法题算法题1 斐波那契数列算法题2 猴子吃桃子算法题3 爬楼梯 Lambda表达式 Lambda表达式是JDK8开始的一种新语法形式。 基本作用&#xff1a;简化函数式接口的匿名内部类的写法。 注意&#xff1a; Lam…

2023年总结我所经历的技术大变革

&#x1f4e2;欢迎点赞 &#xff1a;&#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原创&#x1f4e2;作者格言&#xff1a;新的征程&#xff0c;我们面对的不仅…

STL---Stack和Queue

一、stack的介绍和使用 &#xff08;1&#xff09;介绍 翻译: &#xff08;1&#xff09;stack是一种容器适配器&#xff0c;专门用在具有后进先出操作的上下文环境中&#xff0c;其删除只能从容器的一端进行元素的插入与提取操作。 &#xff08;2&#xff09; stack是作为容器…