数据结构 堆——详细动画图解,形象理解

作者主页

📚lovewold少个r博客主页

​➡️栈和队列博客传送门

🌳参天大树充满生命力,其根深叶茂,分枝扶疏,为我们展示了数据分治的生动形态


目录

🌳 树

树的常见概念

📒树的表示

二叉树

一棵二叉树是结点的一个有限集合,该集合:

📲二叉树的基本类型

🌲满二叉树(完美二叉树)

🌳完全二叉树

🌴 二叉树的性质

💾二叉树的存储方式

顺序存储

链式存储

📌 堆的定义

🔧堆的常用操作

堆的初始化

堆的构建

堆的向上调整的堆化算法

堆的向下调整的堆化算法

​编辑

时间复杂化分析

堆的插入

堆的删除

获取堆顶元素

获取堆有效元素个数

堆的判空

堆的销毁

完整代码

🚀堆的应用

🅰️Top-K问题

堆实现逻辑

🅱️堆排序

✒️总结


前言

        树木随处可见,对于树的形容,我们总是以枝繁叶茂,树木丛生等来形容它。一颗树的主干能长出许多的分支,每一颗分支上又可以有更多分支。而这和我们要学的新结构抽象成的逻辑结构极为相似。

        前面我们学习的数据结构大多数都是一对一的关系,前面接触的单链表,栈等无外乎是对于数据的存储和查找。可是现实中还存在着一对多的关系,也就需要研究这种一对多的数据关系——树(Tree)。同时树中有特殊的完全二叉树,还有特殊的完全二叉树——堆。


🌳 树

树是一种非线性的数据结构,代表这祖先和后代之间的派生关系,树是一种由n(n>=0)个节点组成的集合,其中:

  1. 有且仅有一个节点被指定为根节点;
  2. 其余节点被分为m(m>=0)个互不相交的子集,每个子集本身也是一棵树,称为根的子树。

树在我们的计算机中主要应用为文件的管理,这里我们来用Linux更加直观的展示树在文件管理中的应用。

我们进入Linux系统的根目录,这就好比是文件管理的的主干。

在这个根目录下我们还可以看见许多的文件以及文件夹,而我们知道的是文件夹中还可以套文件夹和文件

我们利用tree指令就可以看见整个树形结构的文件系统,观察<tmp>,我们就能看见文件就像一颗树一样,分支纵横,枝繁叶茂,而tmp也只为根的一个分支。

比如在根目录的树形展示中,通过记数,我们发现拥有17008个文件夹和115225个文件。而文件却多而不乱,通过访问各个分支就能访问到想要的文件,这就是树的魅力所在。

树的常见概念

  • 【根节点 root】:位于二叉树顶层的节点,没有父节点。
  • 【叶子节点 leaf】:没有子节点的节点,左右节点指向都为空。
  • 【非终端节点或分支节点】:度不为0的节点。
  • 【双亲节点或父节点】:若一个节点含有子节点,则称这个节点为子节点的父节点。
  • 【孩子节点或子节点】:一个节点含有的子树的根节点为该节点的子节点。
  • 【兄弟节点】:具有相同父节点的节点称为兄弟节点。
  • 【堂兄弟节点】:双亲在同一层的节点互为堂兄弟节点。
  • 【节点的祖先】:从根节点到该节点经过的所有分支节点可称之为该节点的祖先。
  • 【边 edge】:连接两个节点的线段,即可以抽象为指针。
  • 【节点的度】:一个节点含有的子树的个数称为该节点的度。二叉树的度叶子节点为零,取值为0,1,2。
  • 【树的度】:一棵树最大的节点的度即为树的度。
  • 【节点的层次 level】:从顶至底部递增,根节点所在层为1。
  • 【节点高度 height】:高度通常表述为根节点到叶子节点的层距离。
  • 【二叉树高度 height】:根节点到叶子节点的层距离。
  • 【深度 depth】:到根节点经过的层数。
  • 【森林 Forest】:多棵互不相交的树的集合。对每个节点而言,其子树的集合即为森林。(但是一般不这么去理解,针对与后面的递归逻辑,我们只简单抽象为左右子树即可)。

📒树的表示

        树结构相对与以前的数据结构要更加复杂,既要保存节点的值域,也要保存节点和节点之间的关系。树在实际中有多种表示方式如:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法。这里我们简单的用孩子兄弟表示法。即我们预想并不能考虑到树的根节点到底有多少分支,但是我们可以不去考虑根节点去链接所有孩子节点。我们通过根管老大,老大管老二的链接方式,让父节点链接左孩子,让左孩子去链接他的兄弟节点。

​​逻辑上这一棵树的概念如图所示,在代码上我们通过孩子兄弟表示法进行连接。

typedef int DataType;
struct Node
{struct Node* firstChild1; // 第一个孩子结点struct Node* pNextBrother; // 指向其下一个兄弟结点DataType data; // 结点中的数据域
};

二叉树

        二叉树是一种特殊的树,在日常操作和解决问题的过程中我们更常使用二叉树。什么是二叉树呢,顾名思义,即每一颗节点有两个分支。“一分二支”即作为二叉树操作中主要思想。与链表类似,二叉树的基本单元为节点,每一个节点包含值,左节点,右节点。每一个根节点都通过指针分别指向他的左右节点,在二叉树中,除叶子节点外,其他所有节点都包含子节点和非空子树。

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2.  由一个根节点加上两棵别称为左子树和右子树的二叉树组成

从上图可以看出:

  1.  二叉树不存在度大于2的结点
  2.  二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

📲二叉树的基本类型

🌲满二叉树(完美二叉树)

满二叉树所有层的节点都被完全填满的二叉树。

  • 在满二叉树中,叶节点的度为零,其余所有节点的度为2;
  • 若树的高度为h,则节点总数为2^{h+1}+1,计算方式即为等比数列的前h项合,呈现标准的指数关系。

🌳完全二叉树

完美二叉树的除了最底层未被填满,其余层都被填满,且叶子节点是从左往右的填充。

🌴 二叉树的性质

  1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1) 个结点。
  2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1
  3. 对任何一棵二叉树, 如果度为0其叶结点个数为 n0,度为2的分支结点个数为 n2,则有 n0=n2+1
  4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= log_2(n+1)。 (ps: log_2 是以2为底,n+1为对数)
  5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
    • 若i>0,i位置节点的双亲序号:(i-1)/2;若i=0,i为根节点编号,无双亲节点
    • 若2i+1<n,左孩子序号:2i+1;否则无左孩子
    • 若2i+2<n,右孩子序号:2i+2;否则无右孩子

💾二叉树的存储方式

        关于数据结构的存储方式,我们一般使用两种方式,即顺序存储和链式存储,二叉树我们同样使用这两种方式。这时候不免要考虑了,链式存储能理解,顺序存储又怎么能实现这个逻辑呢。首先需要清楚的是,顺序存储即数组更适合连续存储,这就比较适合对完全二叉树的存储。

顺序存储

        我们通过对一颗完美二叉树建立节点索引,按照层序遍历的方式进行存储,就会发现孩子节点和双亲节点可以通过映射公式建立逻辑联系。

  • 即通过任意一个孩子节点的索引值可以通过 (n-1)/2(向下去整)找到其唯一的双亲节点索引。
  • 通过一个双亲节点的索引值 2*n+1 ;可以找到唯一的左孩子节点索引,再通过左孩子节点+1即可访问右孩子节点。

        非完全二叉树由于后续节点之间不联系,存在空的情况,因此不太适合用数组去存储。数组对于完全二叉树的最优体现就是对堆的实现。

链式存储

        二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所,在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面到高阶数据结构如红黑树等会用到三叉链,因此不做过多铺垫。

        不是完全二叉树的篇章么,怎么又跳到了堆。别急,堆是基于完全二叉树的升华体现,要了解树不妨先看看堆。

📌 堆的定义

堆是一种特殊的完全二叉树,它可以用数组来实现,数组中的每个元素对应一个结点。

  • 堆有两种类型:大堆和小堆。
    • 大堆中,每个结点的值都大于等于它的两个子结点的值;
    • 小堆中,每个结点的值都小于等于它的两个子结点的值。
  • 堆中根结点的值是堆中最大或最小的值,可以用来实现优先队列或堆排序等算法。
  • 堆中某个结点的编号为i,那么它的父结点的编号为(i-1)/2,它的左子结点的编号为2i+1,它的右子结点的编号为2i+2。
  • 堆中某个结点所在的层数为logi+1向上取整,其中log在我们程序界是以2为底的对数。例如,编号为7的结点所在的层数为log8=3。

堆的存储上并不是有序的,但在在每一棵子树都存在根节点相对于左右子树为最大值(大堆),最小值(小堆)。

🔧堆的常用操作

//堆的初始化
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);

堆的初始化

堆由于用数组来进行存储,父子节点采用索引进行定位。

typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;
void HeapInit(Heap* hp)
{assert(hp);hp->a = NULL;hp->capacity = hp->size = 0;
}
  • 通过任意一个孩子节点的索引值可以通过 (n-1)/2(向下去整)找到其唯一的双亲节点索引。
  • 通过一个双亲节点的索引值 2*n+1 ;可以找到唯一的左孩子节点索引,再通过左孩子节点+1即可访问右孩子节点。

堆的构建

        堆的构建简单来讲就是插入数据进堆,由于插入的val并不能保证大小堆的根节点和左右子节点大小关系,因此需要修复从插入节点到根节点路径上的每一节点。这个进行堆调节的一个过程也叫堆化。一般来讲建堆操作通常是对一个已经存在的数组进行堆化,通过建堆利用堆的大小堆根节点的最大和最小进行排序等操作。因此在入堆过程中我们通常有两种堆化方式,那么这两种方式的思维逻辑和时间复杂度怎么样呢?

堆的向上调整的堆化算法

·        在建堆的过程中首先创建一个空堆,然后遍历列表对每个元素依次入堆操作,这种方式就是在构建堆的时候,新元素进入堆末,再通过对其祖先路径上元素进行比较,如果子节点元素小于父节点,则交换(构建大堆相反),进行"从底到顶"的堆化.

void AdjustDown(HPDataType* a, int n, int parent)
{assert(a);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 = child * 2 + 1;}else{break;}}
}

堆分为大堆小堆,这里就用小堆代码来展示。

堆的向下调整的堆化算法

        由上至下的调整操作方式和由下至上的操作相反,构建堆的时候,我们先将列表按照层序遍历的方式连接起来即可。后续,我们需要调节根节点的值与其两个子节点的值进行比较,根据大堆小堆的根节点与子节点来确定二者比较关系。然后循环执行此操作,知道越过叶子节点或遇到无需交换的节点时结束。这里主要的思考方式就是找父比子。

要理解的是,构建堆需要倒序遍历堆,依次对每一个非叶子节点都需要执行向下调整的堆化。

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

这里是以图示的大堆向下调整算法代码

时间复杂化分析

        在构建堆的过程中,通过这两种不同的算法,我们有不同的建堆方式。

向上调整建堆:

        首先创建空堆,遍历待插入的列表,依次将每一个元素入堆,即先将元素添加到堆末,然后对该元素向上调整。元素数量为n,每个元素入堆的时间复杂度为logn,因此建堆整体时间在nlogn。这种建堆方式,是上层先达到大小堆关系,因此是从上至下的创建的堆。

向下调整建堆:

        和向上不同的是先将所有元素原封不动入堆,然后按照倒序层次遍历堆,依次对每一个非叶子节点进行向下调整,当调整到叶子节点的时候调整完毕。这种建堆方式是下层先有序,然后依次向上遍历建堆,因此是从下至上创建的堆。

        这里理解起来有点麻烦,为什么向下调整是倒序建立的堆呢。答案其实很简单,要对一个根节点进行如图所示的调整,得先保证左右子树是一定的大小堆,然后才能向下调整,否则调整是没有意义的。而要保证每一个根节点的左右子树是堆,只需要从倒数第二层即叶节点上一层开始向下调整,保证左右子树和根节点的大小关系。往上遍历的过程中,自然一直能保证左右子树是大堆或者小堆。

这种方式的时间复杂度是多少呢,假设完全二叉树的节点数量为n,则节点数量为(n+1)/2(向下整除),因此需要堆化的数量为(n-1)/2。从顶至底部堆化的过程中,每个节点最多堆化到叶子节点,因此最大高度为二叉树高度logn,所有时间复杂度为nlogn么?

我们来点更加精确的计算。

一个节点从底到上的一个堆化中,最大需要去调整的次数为该节点到叶子节点的距离,而该距离为节点高度,因此我们可以得到公式 节点数量*节点高度。

叶子节点不用去调整,因此只需要计算到高度1即可。

Tn = 2⁰ · h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹ · 1 + 2ʰ · 0

把首尾两个元素简化,记为①式:

①: Tn = h + 2¹ · (h - 1) + 2² · (h - 2) + ...... + 2ʰ⁻² · 2 + 2ʰ⁻¹

对①等于号左右两边乘以2,记为②式:

②: 2Tn = 2¹ · h + 2² · (h - 1) + 2³ · (h - 2) + ...... + 2ʰ⁻¹ · 2 + 2ʰ

那么用②式减去①式,其中②式的操作数右移一位使指数相同的部分对齐,错位相减法。

得到

Tn = n - log₂(n + 1)约等于n

向下调整的时间复杂度仅O(n),非常高效,因此这里我们建堆过程中采用向下调整的方法。

void HeapCreate(Heap* hp, HPDataType* a, int n)//对已有数组可以直接构建堆
{assert(hp);assert(a);hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);//开辟空间,如果已有数组,之间开辟等大空间if (hp->a == NULL){perror("malloc fail");exit(-1);}hp->capacity = n;hp->size = n;memcpy(hp->a, a, sizeof(HPDataType) * n);for (int i = 0; i < n; i++){AdjustUp(hp->a, i);}
}

堆的插入

堆插入新元素,即先入堆末,在对此进行向上调整即可。

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

堆的删除

        删除元素我们要考虑的是怎么去删除比较有意义,不论是大堆小堆,我们能直接获取就是堆顶元素,而且堆的性质能保证这个元素为堆最大或者最小。因此很显然直接删堆首元素嘛。但是当我们删除堆顶元素后,怎么样能保证他继续是一个堆呢。

        这个时候我们先将堆首和堆末替换,扶小弟上位,后面对小弟进行向下调整即可。

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;
}

堆的销毁

void HeapDestory(Heap* hp)
{assert(hp);free(hp->a);hp->a = NULL;hp->capacity = hp->size = 0;
}

完整代码

//头文件Heap.h
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<stdio.h>
#include<assert.h>
#include<malloc.h>
#include<string.h>typedef int HPDataType;
typedef struct Heap
{HPDataType* a;int size;int capacity;
}Heap;//自上而下调整
void AdjustUp(HPDataType* a, int child);
//自下而上调整
void AdjustDown(HPDataType* a, int n, int parent);
//堆的初始化
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);
//交换
void Swap(HPDataType* a, HPDataType* b);
//打印
void HeapPrint(Heap* hp);//实现源文件 Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"void HeapInit(Heap* hp)
{assert(hp);hp->a = NULL;hp->capacity = hp->size = 0;
}
void HeapCreate(Heap* hp, HPDataType* a, int n)//对已有数组可以直接构建堆
{assert(hp);assert(a);hp->a = (HPDataType*)malloc(sizeof(HPDataType) * n);//开辟空间,如果已有数组,之间开辟等大空间if (hp->a == NULL){perror("malloc fail");exit(-1);}hp->capacity = n;hp->size = n;memcpy(hp->a, a, sizeof(HPDataType) * n);for (int i = 0; i < n; i++){AdjustUp(hp->a, i);}
}
void Swap(HPDataType* a, HPDataType* b)
{HPDataType tmp = *a;*a = *b;*b = 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 = (parent - 1) / 2;}else{break;}}
}
void AdjustDown(HPDataType* a, int n, int parent)
{assert(a);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 = child * 2 + 1;}else{break;}}
}
void HeapDestory(Heap* hp)
{assert(hp);free(hp->a);hp->a = NULL;hp->capacity = hp->size = 0;
}
void HeapPush(Heap* hp, HPDataType x)
{assert(hp);if (hp->size == hp->capacity){int newcapacity = hp->capacity==0 ? 4 : hp->capacity * 2;HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);if (tmp == NULL){perror("realloc fail");exit(-1);}hp->a = tmp;hp->capacity = newcapacity;}hp->a[hp->size] = x;hp->size++;AdjustUp(hp->a, hp->size - 1);
}
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;
}void HeapPrint(Heap* hp)
{assert(hp);for (size_t i = 0; i < hp->size; i++){printf("%d ", hp->a[i]);}printf("\n");
}

🚀堆的应用

🅰️Top-K问题

        在日常生活中,我们能看见各种各样的榜单,或者是你微信运动的步数,亦或者是美团的餐馆评分前五名,又或者是自己游戏中的排行榜。很多情况下,这些排行榜并不会展示所有的数据,这样的数据反而没有任何参考意义。很多情况下我们总是盯这Top-100,Top-10。因为这些根据某种评分后决定的数据更有含金量,是大多数人的选择或者想知道的信息。

在实现中其实就可以利用堆完成这样子的Top-K问题。

堆实现逻辑

  1. 首先建立一个小堆,其堆顶元素最小。
  2. 再将数组的前K个元素入堆。
  3. 从第K+1个元素开始,若当前元素大于堆顶元素,堆顶元素出堆。当前元素入堆并调整。
  4. 遍历整个数组后,堆中保存的就是最大k个元素。

        整个建堆的过程时间复杂度就很低,相比于多次排序找最值,时间复杂度在k值最小时间复杂度O(n);k较大时,时间复杂度也不会超过O(nlogn)。这种方式在开辟空间也只用开辟K个节点的空间,空间复杂度也很低。

        这种方式适用于动态的数据流变换,在不断加入数据时,堆内元素始终只需要维护其K个,时刻可以保证K个元素及时更新。

        这里我们通过文件操作进行写入随机值,并且利用这些随机值解决Top-K问题来演示。

void PrintTopK(const char* filename, int k)
{// 1. 建堆--用a中前k个元素建堆FILE* fout = fopen(filename, "r");if (fout == NULL){perror("fopen fail");return;}int* minheap = (int*)malloc(sizeof(int) * k);if (minheap == NULL){perror("malloc fail");return;}for (int i = 0; i < k; i++){fscanf(fout, "%d", &minheap[i]);}// 前k个数建小堆for (int i = (k - 2) / 2; i >= 0; --i){AdjustDown(minheap, k, i);}// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换int x = 0;while (fscanf(fout, "%d", &x) != EOF){if (x > minheap[0]){// 替换你进堆minheap[0] = x;AdjustDown(minheap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", minheap[i]);}printf("\n");free(minheap);fclose(fout);
}// fprintf  fscanfvoid CreateNDate()
{// 造数据int n = 10000000;//造了一个千万级别的数据srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");//打开文件if (fin == NULL){perror("fopen error");return;}for (int i = 0; i < n; ++i){int x = (rand() + i) % 10000000;//随机值fprintf(fin, "%d\n", x);//写入文件}fclose(fin);//关闭文件
}int main()
{CreateNDate();PrintTopK("data.txt", 100);//Top-100return 0;
}

可以看见这个数据量还是相当大的,要是用排序等去实现得跑到天昏地暗。我们改变了数据的几个值,观察他能否找出来,可以看见8133211323这个数据能很快被找出来。

🅱️堆排序

        堆排序一个指定的数列,首先我们要确定的是排升序还是降序。升序构建大堆,降序构建小堆。

        但是我们还要思考的是对于一个已有的数列,堆是需要额外开辟一块空间进行打印操作么。很显然,我们需要将一个数组转变为一个有序数组,我们可以直接用这个数组本身作为一个堆,然后对数组依次入堆并向下调整。升降序只需要大小堆构建控制即可。

void HeapSort(int* a, int n)
{for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}


✒️总结

😄堆(Heap)是二叉树和数组的一种抽象数据结构

堆的基本概念:

  1. 堆是一种树状数据结构,通常是一个完全二叉树。
  2. 堆分为两种主要类型:最大堆(Max Heap)和最小堆(Min Heap),具体取决于根节点的值与其子节点的关系。
  3. 在最大堆中,父节点的值大于或等于子节点的值,最大值位于根节点。
  4. 在最小堆中,父节点的值小于或等于子节点的值,最小值位于根节点。

堆常用的应用:

  1. 堆排序:堆排序是一种高效的排序算法,通过使用堆数据结构,可以将数组以O(n log n)的时间复杂度进行原地排序。
  2. 堆可以用于Top-K算法问题。

堆的难点和理解难点:

  1. 堆的插入和删除操作需要维护堆的性质,这涉及到向下调整和向上调整操作。
  2. 确保堆的性质在插入或删除元素后仍然得到维护,需要深刻理解堆的特性。
  3. 堆排序算法的实现相对复杂,需要理解堆的建立和维护。
  4. 在实际应用中,选择最大堆还是最小堆取决于问题的性质。

作者个人水平有限,文章难免出错,如有错误欢迎指正!


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

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

相关文章

探索乡村新风貌:VR全景记录乡村发展,助力乡村振兴

引言&#xff1a; 中国乡村正经历着巨大变革&#xff0c;长期以来&#xff0c;乡村地区一直面临着人口外流、资源匮乏等问题。然而&#xff0c;近年来&#xff0c;政府的政策支持以及新兴技术的崭露头角&#xff0c;如虚拟现实&#xff08;VR&#xff09;全景记录&#xff0c;…

随着 ChatGPT 凭借 GPT-4V(ision) 获得关注,多模态 AI 不断发展

原创 | 文 BFT机器人 在不断努力让人工智能更像人类的过程中&#xff0c;OpenAI的GPT模型不断突破界限GPT-4现在能够接受文本和图像的提示。 生成式人工智能中的多模态表示模型根据输入生成文本、图像或音频等各种输出的能力。这些模型经过特定数据的训练&#xff0c;学习底层模…

【photoshop学习】用 Photoshop 做的 15 件创意事

用 Photoshop 做的 15 件创意事 每个人总是谈论 Photoshop 的无限可能。您可以使用该程序做很多事情&#xff0c;列表几乎是无穷无尽的。 嘿&#xff0c;我是卡拉&#xff01;如果您花过一些时间使用 在线ps&#xff0c;您可能见过我&#xff08;并且注意到我提到了这一点&am…

WPF向Avalonia迁移(四、其他事项)

开发必备 1. Avalonia项目源代码&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;没有源代码&#xff0c;你连控件的背景色怎么改都找不着&#xff01;&#xff01; 2.下载你所使用的版本&#x…

【AI视野·今日Robot 机器人论文速览 第五十一期】Tue, 10 Oct 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 10 Oct 2023 Totally 54 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers On Multi-Fidelity Impedance Tuning for Human-Robot Cooperative Manipulation Authors Ethan Lau, Vaibhav Srivastava, Sh…

【Proteus仿真】【STM32单片机】汽车倒车报警系统设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使用LCD1602液晶、按键、继电器电机模块、DS18B20温度传感器、蜂鸣器LED、HCSR04超声波等。 主要功能&#xff1a; 系统运行后&#xff0c;LCD1602显…

jmeter 请求发送加密参数

最近在做http加密接口&#xff0c;请求头的uid参数及body的请求json参数都经过加密再发送请求&#xff0c;加密方式为&#xff1a;ase256。所以&#xff0c;jmeter发送请求前也需要对uid及json参数进行加密。我这里是让开发写了个加密、解密的jar&#xff0c;jmeter直接调用这个…

Tomcat项目启动报错

java.io.IOException: java.lang.ClassCastException: Cannot cast org.springframework.web.SpringServletContainerInitializer to javax.servlet.ServletContainerInitializer解决办法&#xff1a;可能Tomcat版本不对&#xff0c;使用7.0.90版本启动报错&#xff0c;使用8.0…

【VTK】一文讲解vtkImageActor

很高兴在雪易的CSDN见到你,给你糖糖 系列文章目录 VTK付费专栏_雪易的博客-CSDN博客 感谢订阅的小哥哥小姐姐,小易会继续努力分享,一起进步! 若订阅后有其它需求,欢迎随时联系,CSDN一直在线(^U^)ノ~YO 前言 本文主要讲解vtk的

flutter 常用组件:文本、图片和按钮

文章目录 文本控件富文本控件图片本地图片网络图片按钮文本控件 ##一’码’当先 Text(这是一段文本这是一段文本这是一段文本这是一段文本这是一段文本这是一段文本这是一段文本这是一段文本,textAlign:TextAlign.center,style: TextStyle(fontWeight: FontWeight.bold, font…

【日常】一名开发人员总结的好习惯,欢迎补充

文章目录 前言首先需要考虑&#xff08;重要&#xff09;设计和架构代码质量测试和调试性能优化 编程的好习惯代码可读性模块化和重用性单一职责异常处理注释和文档常见的Java编码规范的例子下面是一个示例代码&#xff0c;展示了一些编码规范的应用还有一些其他的编码规范值得…

计算机竞赛YOLOv7 目标检测网络解读

文章目录 0 前言1 yolov7的整体结构2 关键点 - backbone关键点 - head3 训练4 使用效果5 最后 0 前言 世界变化太快&#xff0c;YOLOv6还没用熟YOLOv7就来了&#xff0c;如果有同学的毕设项目想用上最新的技术&#xff0c;不妨看看学长的这篇文章&#xff0c;学长带大家简单的…

2023年软考网工上半年下午真题

试题一&#xff1a; 阅读以下说明&#xff0c;回答问题1至问题4&#xff0c;将解答填入答题纸对应的解答栏内。 [说明] 某企业办公楼网络拓扑如图1-1所示。该网络中交换机Switch1-Switch 4均是二层设备&#xff0c;分布在办公楼的各层&#xff0c;上联采用干兆光纤。核心交换…

Jenkins对应java版本

官网地址&#xff1a;Java Support Policy 运行jenkins时,需要使用下列Java版本:

导致 JVM 内存泄露的 ThreadLocal 详解

为什么要有 ThreadLocal 当我们在学习JDBC时获取数据库连接时&#xff0c;每次CRUD的时候都需要再一次的获取连接对象&#xff0c;并把我们的sql交给连接对象实现操作。 在实际的工作中&#xff0c;我们不会每次执行 SQL 语句时临时去建立连接&#xff0c;而是会借助数据库连接…

学习css 伪类:has

学习抖音&#xff1a; 渡一前端提薪课 首先我们看下:has(selector)是什么 匹配包含&#xff08;相对于 selector 的 :scope&#xff09;指定选择器的元素。可以认为 selector 的前面有一个看不见的 :scope 伪类。它的强大之处是&#xff0c;可以实现父选择器和前面兄弟选择器…

TWDS车辆轮对故障、尺寸动态检测系统

随着我国铁路的建设发展&#xff0c;客运专线网络形成&#xff0c;既有铁路的货运能力得到释放&#xff0c;货物运输向重载方向发展&#xff0c;运输组织呈现长交路、运转周期短、编组固定的特点。 跟踪调查表明重载车辆车轮磨耗较普通车辆更为严重。大秦线c80型车辆在不到1个…

mac(M1)安装anaconda3

首先下载 然后正常安装即可&#xff0c;之所以我现在测试了anaconda,因为我发现miniconda后&#xff0c;jupyter notebook的安装就出现问题&#xff0c;所以就直接卸载miniconda&#xff0c;而直接安装anaconda了 (base) yxkbogon ~ % pip list Package …

汽车一键启动点火开关按键一键启动按钮型号规格

汽车点火开关/移动管家一键启动按键/汽车改装引擎启动按钮型号&#xff1a;YD828溥款开关 一键启动按钮&#xff08;适用于配套启动主机使用或原车一键启动开关更换&#xff09; 1.适合配套专用板板安装 2.开孔器开孔安装 3.原车钥匙位安装 外观&#xff1a;黑色 按钮上有3种不…

MVCC和BufferPool缓存机制

文章目录 1. MVCC多版本并发控制机制2. BufferPool缓存机制 1. MVCC多版本并发控制机制 Mysql可以在可重复读隔离级别下可以保证事务较高的隔离性&#xff0c;这个隔离性是由MVCC机制来保证的&#xff0c;对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性&#…