【数据结构】 实现 堆 结构 ---超细致解析

2023-10-26

目录

二叉树的性质:

二叉树的存储结构:

顺序存储:

链式存储:

堆的概念和性质:

堆的实现:

堆的初始化:

 堆的插入:

 向上调整函数:

堆的删除:

向下调整函数:

向上建堆:

向下建堆:

TopK问题:


二叉树的性质:

在我们实现堆之前我们要知道堆的实现是依靠的是二叉树 所以我们在实现对之前要了解一下二叉树的基本性质:>

  1. 如果根节点的层数为1,则一个非空二叉树的第 i 层上最多有2^(i-1)个节点
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2^h - 1
  3. 对于任何一棵二叉树,如果度为0的节点个数是n0,度为2的分支节点个数为n2,则有n0=n2+1
  4. 如果说根节点的层数为1,那么具有那个节点的满二叉树的深度 h=log2(n+1);

这些就是我们二叉树的一些基本性质 

二叉树的存储结构:

二叉树一般有两种存储结构 一个顺序结构 一个链式结构

顺序存储:

顺序存储顾名思义 就是存储的数据是有一定顺序的 所以顺序存储就是用数组来进行存储 但是使用顺序存储一般用来存储完全二叉树 因为不是完全二叉树会有空间的浪费(就是有些地方应该为NULL 那么在数组里面这些地方也应该空着 它们只能属于自己的位置)

像这样大家应该就知道如果说不是完全二叉树使用顺序结构的话 会有空间的浪费 数据越多浪费的空间也可能更多 

链式存储:

链式存储意思就是用链表来表示 一个链表的节点就包括 这个节点的数据和左右节点的指针

typedef struct BinaryTreeNode
{
	int data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

堆的概念和性质:

堆 其实就是 一个完全二叉树 其中堆又分为 小堆 和 大堆 

小堆 就是 根节点是所有数据中最小的 

大堆 就是 根节点是所有数据中最大的

堆的实现:

首先这里我们就用 数组 的方式来存储数据

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

另外有个点大家要注意 我们的堆是不支持什么在 中间 插入删除数据的 这种操作是顺序表中使用的 但我们的堆虽然是使用的数组的结构 但那只是物理上面的 逻辑上面可不是那样的哦

这里我们为什么还有一个size capacity呢 这两个参数是用来判断是否扩容的 我们这里设置的是一个动态增长的数组

堆的初始化:

void HeapInit(Heap* hp)
{
	assert(hp);

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

void HeapDestory(Heap* hp)
{
	assert(hp);

	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

 堆的插入:

我们先了解一下堆的插入是如何实现 首先我们的堆的结构是一个动态的数组 我们在他的尾部插入数据 但大家想想仅仅只是把数据放进去就完了嘛 我们上面说过我们堆里面的数据位置是有讲究的 如果说随便改变是有可能会把我们之前建立的结构破坏的

所以如果我们建立的是一个小堆的话 现在放进去一个较小的数据 那么这个数据肯定是要往前面进行调整的 不然我们的堆就不是小堆 :>

看过上图 大家应该知道我们 因给如何调整我们的堆结构了把:

就是  向上调整 

 这个就是我们的向上调整的整体操作

void HeapPush(Heap* hp, HPDataType x)
{
	if (hp->_size == hp->_capacity)
	{
		int newcapcity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* tmp=(HPDataType *)malloc(newcapcity * sizeof( HPDataType));
		assert(tmp);
		hp->_a = tmp;
	}

	hp->_a[hp->_size++] = x;
	AdjustUp(hp->_a, hp->_size - 1);//因为我们我们调整是从最后一个数据开始调 所以要传他的下标
}

因为我们堆的实现物理上还是依靠的顺序表 虽说结构上是树 但我们在插入数据的时候 难免会遇到要扩容的情况 所以我们使用了 sizecapacity 来实时监控我们的空间

然后就来看我们的向上调整函数:

 向上调整函数:

在实现我们的向上调整函数之前 我们首先要了解 树中 父亲和孩子的关系 

左孩子=父亲×2+1

右孩子=父亲×2+2

然后大家想想 其实无论奇数还是偶数 -1÷2 的结果都是一样的 我的编译器 ÷ 是默认的向下取整 带入几个数算了就知道他们的结果都是一样的 但你要-2÷2就不太好了

void AdjustUp(HPDataType* a,int child)
{
	assert(a);
	size_t parent = (child - 1) >> 1;
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			swap(a+child, a+parent);
			child = parent;
			parent= (child - 1) >> 1;
		}
		else
		{
			break;
		}
	}
}

看到这个图大家应该明白我们的child 排到0时 就不用再继续往上调整了

所以我们 设置的外部循环是 child>0  至于为什么不用parent判断 因为parent不会小于0

我们的child等于0的时候 (0-1)/2 parent还是等于0的

里面设置一个if 小于就交换 大于就不用再换了 大于一个数那它都大于它上面的了

然后重新给我们的 child parent 赋值

堆的删除:

堆的删除操作就是删除 根节点 也就是最大或者最小的数 那大家觉得怎样删除既能删掉数据还不会破坏我们的堆结构呢?

可能我们会觉得删除数据嘛 就把它删了不就行了 把它后面的数据往前面覆盖 把它覆盖掉不就行了嘛 真的是这样嘛我们来看一下:

其次还有一个问题 就是如果这样删除数据 每次移动数据都是O(N)

现在我们再来看正确的做法:

先将最后一个数据和根节点交换然后把size--删掉最后一个数据

这样现在我们的根节点就是一个较大的数 然后使用 向下调整 函数 

向下调整函数:

因为我们向下调整最多调整高度次 所以我们这个删除函数的时间复杂度是O(N)

 大家看这张图明白是怎么做的了嘛

  1. 找出左右孩子中最小的那一个
  2. 与父亲比较 
  3. 如果交换继续向下调整

两个孩子节点为什么要选最小的?

因为我们的父亲节点有两个孩子节点 要从中选出较小的那一个 该怎么选呢?

因为我们选择最小的换上去后 也是比它的兄弟节点小的

void Adjustdown(HPDataType* a, size_t size, int root)//向下调整
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child + 1] < a[child])
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child= parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

这里我们的循环条件是 while(child<n) 因为当我们排到叶子节点的时候 这个时候已经是最后一次了 之所以不用 while(parent<n) 是因为有可能这样 我们的child会越界  当 我们已经计算出child>n了 可是parent<n 这个时候在进去比较就会越界了 除非在第二个 if 判断语句那里在加一个限制条件

至于选左右孩子最小 就像上面那样就好了 但是要注意有时我们的左孩子可能不存在所以我们还要保证 child+1<n  

我们的child赋值一开始也是右孩子的下标

了解了这些我们就可以实现一下我们的堆排序了 

    Heap hp;

	HeapInit(&hp);
	
    int a[11]={1,0,2,8,5,6,9,3,8,4};
    for(int i=0;i<sizeof(a)/sizeof(int);i++)
    {   
        HeapPush(&hp,a[i]); 
    }    

    size_t j=0;
    while(!HeapEmpty(&hp))
    {
        a[j++]=HeapTop(&hp);//0 1 2 3 4 5 6 8 8 9
        HeapPop(&hp);
    }
    
    HeapDestory(&hp);

 大家注意我们这里的时间复杂度是 N*logN 哦 入数据是N*logN 排序也是N*logN  所以总的时间复杂度就是N*logN 因为我们有N个数 每个数无论是入数据还是删除数据每次都是logN

logN其实就是我们的层数(可以说是接近吧 毕竟2^h-1=N h=log(N+1)) 大家可以这样理解

接下来给大家说一下 另一种更好的排序 在此之前呢我们先来了解一下两种思想:向上建堆 和 向下建堆 

向上建堆:

 首先向上建堆的思想是 我们把一个数组的首元素看作是一个堆 其后的数据作为要插入的数据 就好像是上面堆的插入的思想 

    int a[10]={0};
    for(int i=0;i<10;i++)
    {
        a[i]=i;    
    }
    
    for(int i=1;i<size;i++)
    {
        AdjustUp(a,i);
    }

我们依次对数组中的元素进行向上调整 那么最后调整完 就是一个调整好的堆了

向下建堆:

向下建堆其实也是和向上建堆的思想其实是一样的 所以向下建堆就是使用向下调整函数来调整我们的原数组

那我们向下调整是从那里开始呢 

大家看这张图觉得我们向下调整应该从那里开始呢?

  如果我们从第一个数开始调 这样大家调完就会发现是不可行的 回顾我们上面的向下调整会发现我们从第一个数看它的左右子树都是小堆  所以呢 其实我们使用向下调整算法要知道开始位置往下的左右子树 要么全是大堆 要么全是小堆

看了上面这段话 大家有思路了嘛 我们知道我们在对一个节点开始向下调整 那么它的左右子树必须是堆 所以我们从下往上依次进行向下调整 那这样是不是就保证了刚刚的条件了呢 

 大家看这张图明白怎么做了嘛?

for(int i=(size-1-1)/2;i>=0;i--)//size-1是最后一个节点的下标
{
    AdjustDown(a,size,i);
}

 其实就是从 倒数的第一个非叶子节点(最后一个节点的父亲节点)开始往上递归做向下调整 

我们找到后 在从该节点-- 就可以把所有的数都调整一遍了

并且建堆的方式不同 建出来的堆也是不一样的 虽然 建的都是小堆

既然有这两种方法那哪一种方法更好呢?看这文章就知道了:【数据结构】堆的建立 (时间复杂度计算-堆排序)---超细致_luck++的博客-CSDN博客

这里面还包括了 堆排序的实现 以及整体的思路 打击有兴趣可以看看哦

TopK问题:

TopK问题 字如其名 其实就是 我们在N个数里面 找出最大的(最小)前K个数

大家有什么思路嘛?

其实有这样的几个思路:

① 我们直接进行排序 选数

② 我们建立以个N个数的大堆 Pop k 次 就选出 前K个最大的数

这两种方法可行 但是万一我们的N很大呢 是不是我们的排序效率就很低了呢 是不是我们的建N个数的大堆也就空间不支持了呢?

 对于这种问题我们可以这样解决:

  假设我们要选前K个最大的数  我们首先排出K个数的小堆 然后我们再遍历N-K个数 如果遇到比小堆的根节点的数更大的数就交换 并且交换完对堆进行向下调整 这样就能保证下一次入数据不会因为根节点的数据更大而进不去了 

void PrintTopK(int* a, int n, int k)
{
	int* kminHeap = (int*)malloc(4 * k);
	assert(kminHeap);
	for (int i = 0; i < k; i++)//将数据放入我们建堆的数组中
	{
		kminHeap[i] = a[i];
	}

	for (int i = (k - 1 - 1) / 2; i >= 0; i--)//向下调整建堆(小堆)
	{
		Adjustdown(kminHeap, k, i);
	}

	for (int i = 0; i < n; i++)//N-K个数依次与堆顶的数比较
	{
		if (kminHeap[0] < a[i])
		{
			kminHeap[0] = a[i];
			Adjustdown(kminHeap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminHeap[i]);
	}
}

void TestTopk()
{
	int* p = (int*)malloc(10000 * sizeof(int));
	assert(p);
	srand((unsigned int)time(0));

	for (int i = 0; i < 10000; i++)
	{
		p[i] = rand() % 10000;
	}
	p[1221] = 10000 + 3;
	p[223] = 10000 + 5;
	p[678] = 10000 + 9;
	p[345] = 10000 + 2;
	p[897] = 10000;
	p[4567] = 10000 + 10;
	p[2345] = 10000 + 6;

	PrintTopK(p, 10000, 7);
}

这里我们N个数都是比10000小的数 我们随机挑选几个把它置为比10000大的数 来测试

无论我们放的数在哪都可以 大家可以下来试试

 我们这种思路的时间复杂度:O(K+logK*(N-K)) 我们建堆使用的是向下建堆 时间复杂度为:O(N) 我们每次排小堆 都是logK次 最坏情况下 N-K 个数就要排 

咋们这样的时间效率是不是就有了提升 相对之前的方法


 到这里呢 我们关于堆的讲解就结束了 感谢大家能够看到这里!!!预祝大家都能收到自己心仪大厂的offer!!!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【数据结构】 实现 堆 结构 ---超细致解析 的相关文章

  • Merkle树

    白皮书引入 Merkel树是一种数据结构 图1 1 比特币白皮书插图 生成一个Merkel树 图1 2 来自维基百科插图 分析 自下而上 我们有四个文件 比特币系统的话就是交易 这个数据结构可以用在各种方面 L1 L2 L3 L4 四个文件
  • QT学习-数据类型转换

    文章目录 前言 一 num转QString 二 数据输出格式 三 QString拆分到QStringList 1 去除空格拆分 2 按固定长度拆分 四 QStringList转QByteArray HEX 五 uint8 t与QByteAr

随机推荐

  • java关键字const_java 关键字详解

    一 关键字总览 访问控制 private protected public 类 方法和变量修饰符 abstract class extends final implements interface native new static str
  • 用JAVA语言写一个计算员工月工资的程序

    一 任务需求 某公司分为多个部门 每个部门有一个经理和多个员工 每个员工根据职称发基本工资 员工的工资由基本工资 日加班工资 日缺勤工资等组成 具体需求如下所示 员工的基本信息 包括部门 职务 职称以及工资记录等信息 能记录员工的每一个职称
  • 过压保护芯片,IC电路方案集合

    随着快充适配器 QC3 0充电器 PD充电器的诞生 改变了原先USB的5V充电 增加了9V 12V的充电电压 甚至PD充电器20V也是很常见了 9V 12V 20V高电压的应用产生 虽然说9V 12V 20V的高压输出 不会被误触发 但是消
  • R-FCN+ResNet-50用自己的数据集训练模型(python版本)

    说明 本文假设你已经做好数据集 格式和VOC2007一致 并且Linux系统已经配置好caffe所需环境 博客里教程很多 下面是训练的一些修改 py R FCN源码下载地址 https github com Orpine py R FCN
  • react+antd+table实现表格数据,从头到尾循环、自动分页、滚动展示

    ts写法 分页是20 滚动过程中自动分页调接口返回数据 class Demo extends React Component
  • python调用resnet模型 对人脸图片进行特征提取,提取全连接层特征向量

    resnet feature extraction import os os chdir root caffe master examples import numpy as np import matplotlib pyplot as p
  • 汇编语言----mul指令

    mul指令 把操作数与AX相乘 最后存放在AX中 例子 mov ax 4 mov bx 5 mul bx ax 20
  • 动态设置src路径

    img class sidebar logo data return logo require assets dt logo png 如上动态设置图片的路径需要使用require 不然只能写上固定路径如下 img src assets dt
  • 排行榜|当 DB-Engines 遇见墨天轮国产数据库排行

    提到数据库排名 此时脑海里浮现出的是什么 是 DB Engines 还是墨天轮数据库排行 两者间有什么区别 下面来聊一下业内这两个知名数据库排名平台 本篇文章约有 3000 字 预计阅读时间 7 分钟 如阅读时间有限 请直接阅读文章末尾的对
  • 软件测试/测试开发丨容器编排K8S 下部署分布式UI自动化解决方案

    本文作者为霍格沃兹测试开发学社特约讲师乔巴 K8S目前是业界容器编排领域的事实标准 是几乎所有云原生架构的首选 目前随着云原生架构越来越流行 测试开发人员需要掌握K8S技术栈已经成为越来越迫切的需求 Kubernetes 开源于 2014
  • 敏捷开发-如何理解spring x

    2021年 不断思考一年 从领导风格转变让自己有点不适应 但是一直让自己谦卑心去学习每个人身上特长东西 接触过很多管理者 但是身上特点不足够明显的领导 我觉得不适合这个岗位 所以更多的调整心态去适应 找到每一个天 每个月 每一年战略目标自己
  • vc的编译过程

    对VC 工程编译过程的梳理 VC 的项目和解决方案文件解读 无非就是利用这些信息进行一个软件的编译 这些文件里面是存放的项目的配置和工程的组织 类似于makefile文件 但是只有VC 6 0的时候可以导出makefile文件 VC6的pr
  • python3 [爬虫入门实战]爬虫之scrapy爬取中国医学人才网

    自己第一次试着用scrapy进行爬取网页 总共爬下9240条数据 也就两分钟不到 400多页吧 用的比较简单 但是爬取成功后感觉成就感满满的 来张爬取结果图 爬取字段 hospitalName hospitalDesc hospitalSi
  • 【满分】【华为OD机试真题2023B卷 JS】乱序整数序列两数之和绝对值最小

    华为OD2023 B卷 机试题库全覆盖 刷题指南点这里 乱序整数序列两数之和绝对值最小 知识点排序数组 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 给定一个随机的整数 可能存在正整数和负整数 数组 nums 请你在该数组
  • X11协议基础与实践

    X11协议基础与实践 概念 X11 X Window System 是一种位图显示的视窗系统 X表示X协议 11是协议版本号 X 协议主要由 X server 和 X client 组成 l X server 管理主机上与显示相关的硬件设置
  • 对角线遍历

    param number matrix return number var findDiagonalOrder function matrix if matrix null matrix length 0 return let m matr
  • “40道高频区块链面试题”——我的一些看法

    最近看到了一篇文章如下 超强攻略 40道高频区块链面试题大放送 年底跳槽看过来 地址我也贴出来吧 https mp weixin qq com s 3Fa2XG4R11QDfMSAaBCngw 哦 CSDN的地址也出来了 https blo
  • vscode好用的前端插件和快捷键

    用到好用的vscode插件 总结一下 文章目录 一 常用主题 1 Material Theme主题 2 Community Material Theme主题 3 vscode icons 二 基础插件 1 Code Spell checke
  • java生成二维码图片(有logo),并在图片下方附文字

    logo配置类 Created by Amber Wang on 2017 11 27 17 25 import java awt public class LogoConfig logo默认边框颜色 public static final
  • 【数据结构】 实现 堆 结构 ---超细致解析

    目录 二叉树的性质 二叉树的存储结构 顺序存储 链式存储 堆的概念和性质 堆的实现 堆的初始化 堆的插入 向上调整函数 堆的删除 向下调整函数 向上建堆 向下建堆 TopK问题 二叉树的性质 在我们实现堆之前我们要知道堆的实现是依靠的是二叉