【数据结构】堆

简介: 【数据结构】堆

前言


🚩前面了解了树(-> 传送门 <-)的概念后,本章带大家来实现一个跟树有关的数据结构——堆(本章有对堆排序和topk问题的讲解)。

🚩普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把 堆 (一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

如下图:

efb38bdb006b4f23b1ef985c5533fb8a.png

🚩所以,我们在实现 堆 的时候可以将它看作为一棵二叉树来思考,这是它的 逻辑结构 。但其 物理结构 是一个实实在在的 顺序数组 (在内存当中存储的形式)。

🚩那么接下来就开始实现一个属于自己的堆吧!


堆的概念及结构


  • 本章是以大堆为例,只要会了大堆,小堆也是不在话下。
  • 堆是把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且它的任意元素 Ki(i = 0, 1, 2, 3, …, n - 1) 都满足 Ki <= K(i * 2 + 1) 且 Ki <= (i * 2 + 2) ( 或者 Ki >= K(i * 2 + 1) 且 Ki >= K(i * 2 + 2) ),这样的堆我们称之为 小堆( 或者 大堆 )。我们还可以将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
  • 堆的性质:
  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树。


  • 有了对堆的概念和性质的理解,相信大家对堆的元素是如何在数组中存储的(物理结构)明白了许多,就是依照上面大堆小堆的概念来维持存储关系的。


图示:



94fd89d4f18448e1bc902401e774dfe1.png

9375c7f07fcb474a83447b361daf326a.png


观察上图可知,第一张图是一个大堆的逻辑结构和物理结构,其每个结点的左右子结点都比这个结点要小或者等于;第二张图是一个小堆的逻辑结构和物理结构,其每个结点的左右子结点都比这个结点要大或者等于。所以,一个堆,他只能是小堆或者大堆,不具有这两个堆的性质的,都不属于堆,下面我们来判断一下那些是堆那些不是堆:

A. 100,60,70,50,32,65  // 这是一个大堆
B. 60,70,65,50,32,100  // 这不是堆
C. 65,100,70,32,50,60  // 这不是堆
D. 70,65,100,32,50,60  // 这不是堆
E. 32,50,80,70,65,100  // 这是一个小堆 
F. 50,100,70,65,60,32  // 这不是堆

其实如果想要更好的判断,只需要将其逻辑结构画出来,就一目了然了。


4f000b1967e44447bb6c62825101e54f.gif

62f7a1c2e4394808b848824928ad9efd.png


了解了堆的概念和结构后,接下来就是实现啦!!!


堆初始化


  • 首先我们需要三个文件:heap.h,heap.c,test.cheap.h用来存放所需头文件,堆的结构体和相关函数声明。heap.c用来实现相关函数接口,test.c用来测试。

下面是所需头文件:

#include <stdio.h>
// assert断言所需
#include <assert.h>
// 动态内存开辟函数所需
#include <stdlib.h>
// bool类型所需
#include <stdbool.h>
// 拷贝函数所需
#include <string.h>


  • 我们知道,堆的底层存储结构是一个顺序的数组,因此这里需要跟前面我们实现的顺序表一样用动态内存空间。使用动态空间,就需要一个变量来统计容量,当然,为了后面一些函数功能的需求,还需要一个变量来统计堆的数据个数。因此,一个堆的结构定义如下:
// 堆的元素的数据类型
typedef int HPDataType;
typedef struct Heap
{
  // 指向存储数据的连续的空间(数组)
  HPDataType* a;
  // 统计当前堆中数据的个数
  int size;
  // 统计当前空间的容量
  int capacity;
}Heap;
  • 一个堆所需的函数接口声明如下:
// 以大堆为例
// 堆初始化
void HeapInit(Heap* php);
// 数组创建堆
void HeapCreateArray(Heap* php, HPDataType* a, int n);
// 插入数据
void HeapPush(Heap* php, HPDataType x);
// 删除数据
void HeapPop(Heap* php);
// 获取堆顶数据
HPDataType HeapTop(Heap* php);
// 获取堆数据个数
int HeapSize(Heap* php);
// 堆判空
bool HeapEmpty(Heap* php);
// 堆的销毁
void HeapDestroy(Heap* php);
// 交换
void swap(HPDataType* a, HPDataType* b);
// 向上调整
void adjustup(HPDataType* a, int child);
// 向下调整
void adjustdown(HPDataType* a, int size, int parent);
// 堆排序
void HeapSort(HPDataType* a, int n);
// topk问题
void PrintTopK(HPDataType* a, int n, int k);


  • 有了以上的铺垫,堆的初始化就是将堆的结构里的指针置为空,统计数据个数和统计空间容量的变量置为0即可。(当然也可以初始化的时候就开个初始空间)

相关函数接口功能的实现:

// 堆初始化
void HeapInit(Heap* php)
{
  // 防止传递一个NULL上来,传NULL的话php就是NULL,后面的操作就不能进行了
  assert(php);
  php->a = NULL;
  php->capacity = php->size = 0;
}


堆的判空


  • 有了统计堆的数据个数的变量size,判空就简单多了。只需要判断一下size是否为0即可,如果size0,说明为空,返回true,如果size不为0,说明堆中有数据,返回false


相关函数接口功能的实现:

// 堆判空
bool HeapEmpty(Heap* php)
{
  assert(php);
  return php->size == 0;
}


堆的销毁

  • 堆的销毁也很简单,将堆空间释放即可。当然size跟统计容量的capacity也置为0

相关函数接口功能的实现:

// 堆的销毁
void HeapDestroy(Heap* php)
{
  assert(php);
  free(php->a);
  php->capacity = php->size = 0;
}


插入数据


首先,插入数据要看看容量够不够,如果不够,就需要扩容。


插入数据就是在数组的最后一个位置插入,由于size是数据的个数也就是数组的长度,所以size就是指向堆的最后一个数据的下一个位置,因此我们只需要在size位置插入即可。当然,插入过后,多了一个数据,因此size要自加1。


当我们插入数据之后,现在堆的结构很可能就不是一个堆(以大堆为例)了,因此,这里需要对插入进来的那个数据执行一个向上调整的算法,图示:


4a613fae88a145dda63d9b8da4ecf13e.gif

对于向上调整算法,我们需要找到这个结点(假设下标为child)的父亲结点(假设下标为parent),具体操作为:parent = (child - 1) / 2 ,找到父结点后,就与他进行比较,由于我们以大堆为例,所以如果child位置上的数据大于parent位置上的数据,就需要将这两个数据交换一下,然后循环往复,继续寻找父结点进行比较,直到到达应该处在的位置为止(直到是个堆为止)。


相关函数接口功能的实现:


// 插入数据
void HeapPush(Heap* 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;
  }
  // 在size位置插入数据,然后size自加1
  php->a[php->size++] = x;
  // 对插入的那个数据执行向上调整算法,重整堆
  // php->size - 1是插入的那个数据的下标
  adjustup(php->a, php->size - 1);
}

向上调整算法:

// 向上调整
void adjustup(HPDataType* a, int child)
{
  // 找父结点
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    // 如果child位置的数据大小大于parent位置的数据大小就交换
    if (a[child] > a[parent])
    {
      // 交换
      swap(&a[child], &a[parent]);
      // 交换过后,child依旧指向开始指向的那个数据
      child = parent;
      // 继续找父结点
      parent = (child - 1) / 2;
    }
    else break; // 如果小于等于了,说明此时的堆是个真正的堆了,直接跳出循环
  }
}


这里有个swap函数,其实现为:

// 交换
// 注意是传递地址交换,因为形参的改变不改变实参
void swap(HPDataType* a, HPDataType* b)
{
  HPDataType tmp = *a;
  *a = *b;
  *b = tmp;
}


删除数据


删除数据其实是删除堆顶的数据,由于堆是由一个数组来存放数据的,那么我们该如何删除堆顶(数组的第一个元素数据)的数据呢?


这里我们将堆顶数据和数组的最后一个数据交换一下,然后size减一,就实现了删除。


当然,删除过后,此时的堆已经不再是堆了,所以我们需要对新的堆顶数据执行一下向下调整算法,图示:

4f172807bb89447ab73f85270ca66e27.gif

对于向下调整算法,我们先要找到该结点(假设下标为parent)的孩子结点,而孩子结点又分为左孩子结点(下标为parent * 2 + 1)和右孩子结点(下标为parent * 2 + 2),所以我们需要找出两个孩子结点当中较大的那个,如果该节点的数据比较大的那个孩子结点的数据要小,那就进行交换,然后循环往复继续向下寻找孩子结点重整堆。


整个操作,我们可以先比较两个孩子的大小找出大的那个,然后在与大的这个孩子结点进行比较,如果父结点比他小(以大堆为例),说明这个孩子结点该上位了。然后继续向下执行这个操作。


相关函数接口功能的实现:


// 删除数据,删除堆顶的数据
void HeapPop(Heap* php)
{
  // 判断php的合法性并且要保证堆不为空,空就不能删了
  assert(php && !HeapEmpty(php));
  // 将堆的第一个数据与堆的最后一个数据交换
  swap(&php->a[0], &php->a[php->size - 1]);
  // size减一表示删除最后一个数据
  php->size--;
  // 对新的堆顶执行向下调整算法
  adjustdown(php->a, php->size, 0);
}


向下调整算法:

// 向下调整
void adjustdown(HPDataType* a, int size, int parent)
{
  // 先假设大的那个孩子结点为左孩子结点
  int child = parent * 2 + 1;
  while (child < size)  // 如果child小于此时数组的长度就继续
  {
    // 第一个判断是防止没有右孩子结点的情况
    // 第二个判断是如果右孩子存在并且右孩子结点的数据大于左孩子结点的数据,就child加一指向右孩子结点
    if (child + 1 < size && a[child + 1] > a[child]) child++;
    // 如果父节点数据小于child结点数据,就交换重整堆
    if (a[child] > a[parent])
    {
      swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else break;  // 如果父节点数据大于child结点数据,说明堆已经调整完毕,直接跳出循环不在调整
  }
}


堆的数据个数


  • 由于我们定义了一个变量来统计堆的数据个数,所以在这里只需要返回那个变量的值即可。

相关函数接口功能的实现:

// 获取堆数据个数
int HeapSize(Heap* php)
{
  assert(php);
  return php->size;
}


获取堆顶数据


  • 获取堆顶数据就是数组的第一个元素。
  • 如果此时堆为空,就不能获取了。

相关函数接口功能的实现:


// 获取堆顶数据
HPDataType HeapTop(Heap* php)
{
  assert(php && !HeapEmpty(php));
  return php->a[0];
}

用数组创建堆


用数组创建堆就是将一个数组里面的所有元素拷贝到堆的数组里,然后进行建堆。


我们首先开辟一个堆空间(就是一段连续的数组),长度与给的数组的长度相同,然后将给的数组里的元素拷贝过来,最后将堆里面的数据进行建堆。


如何建堆?我们从最后一个结点的父节点开始,依次执行向下调整算法,直到根节点执行完全后,便建成了堆。当然我们也可以从第二个结点开始,依次执行向上调整算法,直到最后一个结点执行完后便建成了堆,不过这样的时间复杂度为O(n * logn),而前面的向下调整算法的方式的时间复杂度为O(n),所以这里我们采用向下调整算法的方式来建堆。至于这两个调整算法的时间复杂度是如何计算出来的,这里就不做讨论,它的本质其实是有关数列求和的计算。


向下调整算法方式建堆图示:


04ef730caa1f464581e738d450afe55a.gif

d9df7cce210f4851b1d12e5d8625de2f.gif


  • 有了上面的思路,接下来就是代码实现了。

相关函数接口功能的实现:

// 数组创建堆
void HeapCreateArray(Heap* php, HPDataType* a, int n)
{
  // php不能为NULL,a不能为NULL
  assert(php && a);
  // 开辟可存放n个数据的堆空间
  php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
  if (php->a == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  // 拷贝数据
  memcpy(php->a, a, sizeof(HPDataType) * n);
  php->size = php->capacity = n;
  // 从最后一个结点的父节点开始依次执行向下调整算法,直到根结点执行完后结束
  // 这里i为什么初始化 (n - 2) / 2 呢? 其实就是最后一个结点的父节点的下标
  // 前面说了,寻找父节点的下标是 (child - 1) / 2
  // 这里是寻找最后一个结点的父结点,而最后一个结点的下标是 (n - 1)(n是数组长度)
  // 所以它的父节点就是 (n - 1 - 1) / 2 , 也就是(n - 2) / 2
  for (int i = (n - 2) / 2; i >= 0; i--) adjustdown(php->a, n, i);
}


对数组堆排序


  • 有了建堆的认识,堆排序也不难,只不过需要注意几个细节。


对数组排序,首先就是要对这个数组建堆,如果是要将数组升序,就建为大堆,如果是要将数组降序,就建为小堆,为什么呢?这里以升序为例进行讲解,弄懂了升序,降序也只是大于小于号的问题了。


当数组建成大堆形式后(同样的,使用向下调整算法建堆),堆顶元素是最大的,此时我们可以将堆顶元素与最后一个元素进行交换,这样最大的元素就到了数组的末尾了。然后我们对这个处在数组最后一个位置的最大元素视而不见,将交换过去的堆顶元素执行向下调整算法,这时,第二大的元素就到了堆顶,然后此时的堆顶元素继续与最后一个元素进行交换 (注意第一个交换过去的最大的元素已经不在范围内了,也就是说每将一个当前最大的数交换过去后,可视作size减一一次) ,然后再将交换过去的堆顶元素执行向下调整算法…这样循环往复,最终该数组就变成了升序。

image.gif


相关函数接口功能的实现:

// 堆排序
void HeapSort(HPDataType* a, int n)
{
  assert(a);
  // 向下调整, 这里是建大堆
  for (int i = (n - 2) / 2; i >= 0; i--) adjustdown(a, n, i);
  // 排序(建的大堆就是升序)
  int k = n - 1;
  while (k > 0)
  {
    swap(&a[0], &a[k]);
    adjustdown(a, k, 0);
    k--;
  }
}


有关topk问题


如何在百万个成绩数据里面找出前十个最好的?这就是典型的topk问题。可以看到,它需要处理的数据非常多(当然也有的很少,不过面试一般就是问多的情况,让你找出前k个最那啥的),因此这里需要用到堆来解决。


为什么堆可以解决呢?堆的堆顶要么是整个堆里最大的元素,要么是整个堆里最小的元素。根据这一性质,假如求很多数据中前k个最小的数据,我们可以先开辟一个堆空间,该堆空间可以存放k个数据,然后我们将很多数据中的前k个数据拷贝进堆里,并将其建成大堆,此时堆的堆顶元素就是堆里所有元素最大的那一个,接着,我们从很多数据的第k个数开始,遍历这些数据,如果该数据小于此时堆顶的数据,就将堆顶数据更新为这个小的数据,然后对其执行向下调整算法,执行完过后,堆顶还是堆中最大的那个元素,就这样判断并操作,直到遍历结束,堆中就是那前k个最小的数啦。最后将这些数打印即可。(找前k个最小的数就是建大堆,反之,建小堆,这里就不作讨论了)


原理(以求前k个最小的数为例):每次比堆顶小的元素入堆并调整后,之前堆中最大的元素被抛弃,新的最大的元素上位了,这样循环往复下去,每次操作除大的进小的,当然最后堆中就是前k个最小的数啦。


相关函数接口功能的实现:

// topk问题
void PrintTopK(HPDataType* a, int n, int k)
{
  assert(a);
  // 开辟能够存放k个数据的堆空间
  HPDataType* topk = (HPDataType*)malloc(sizeof(HPDataType) * k);
  if (topk == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  // 拷贝前k个数据进堆
  memcpy(topk, a, sizeof(HPDataType) * k);
  // 找前k个最小的,建大堆
  for (int i = (k - 2) / 2; i >= 0; i--) adjustdown(topk, k, i);
  // 对topk堆进行 除大的进小的 操作
  for (int i = k; i < n; i++)
  {
    if (a[i] < topk[0])
    {
      topk[0] = a[i];
      // 每次进来一个较小的元素都要执行一遍向下调整算法来调整堆
      adjustdown(topk, k, 0);
    }
  }
  // 打印  topk
  for (int i = 0; i < k; i++) printf("%d ", topk[i]);
  // 最后使用完堆后记得释放
  free(topk);
}


整体代码展示

heap.h

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size;
  int capacity;
}Heap;
// 以大堆为例
// 堆初始化
void HeapInit(Heap* php);
// 数组创建堆
void HeapCreateArray(Heap* php, HPDataType* a, int n);
// 插入数据
void HeapPush(Heap* php, HPDataType x);
// 删除数据
void HeapPop(Heap* php);
// 获取堆顶数据
HPDataType HeapTop(Heap* php);
// 获取堆数据个数
int HeapSize(Heap* php);
// 堆判空
bool HeapEmpty(Heap* php);
// 堆的销毁
void HeapDestroy(Heap* php);
// 交换
void swap(HPDataType* a, HPDataType* b);
// 向上调整
void adjustup(HPDataType* a, int child);
// 向下调整
void adjustdown(HPDataType* a, int size, int parent);
// 堆排序
void HeapSort(HPDataType* a, int n);
// topk问题
void PrintTopK(HPDataType* a, int n, int k);


heap.c

#include "heap.h"
// 堆初始化
void HeapInit(Heap* php)
{
  assert(php);
  php->a = NULL;
  php->capacity = php->size = 0;
}
// 数组创建堆
void HeapCreateArray(Heap* php, HPDataType* a, int n)
{
  assert(php && a);
  php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
  if (php->a == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  memcpy(php->a, a, sizeof(HPDataType) * n);
  php->size = php->capacity = n;
  for (int i = (n - 2) / 2; i >= 0; i--) adjustdown(php->a, n, i);
}
// 插入数据
void HeapPush(Heap* 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 HeapPop(Heap* php)
{
  assert(php && !HeapEmpty(php));
  swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  adjustdown(php->a, php->size, 0);
}
// 获取堆顶数据
HPDataType HeapTop(Heap* php)
{
  assert(php && !HeapEmpty(php));
  return php->a[0];
}
// 获取堆数据个数
int HeapSize(Heap* php)
{
  assert(php);
  return php->size;
}
// 堆判空
bool HeapEmpty(Heap* php)
{
  assert(php);
  return php->size == 0;
}
// 堆的销毁
void HeapDestroy(Heap* php)
{
  assert(php);
  free(php->a);
  php->capacity = php->size = 0;
}
// 交换
void swap(HPDataType* a, HPDataType* b)
{
  HPDataType tmp = *a;
  *a = *b;
  *b = tmp;
}
// 向上调整
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 adjustdown(HPDataType* a, int size, int parent)
{
  int 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;
  }
}
// 堆排序
void HeapSort(HPDataType* a, int n)
{
  assert(a);
  // 向下调整, 这里是建大堆
  for (int i = (n - 2) / 2; i >= 0; i--) adjustdown(a, n, i);
  // 排序(建的大堆就是升序)
  int k = n - 1;
  while (k > 0)
  {
    swap(&a[0], &a[k]);
    adjustdown(a, k, 0);
    k--;
  }
}
// topk问题
void PrintTopK(HPDataType* a, int n, int k)
{
  assert(a);
  HPDataType* topk = (HPDataType*)malloc(sizeof(HPDataType) * k);
  if (topk == NULL)
  {
    perror("malloc fail");
    exit(-1);
  }
  memcpy(topk, a, sizeof(HPDataType) * k);
  // 找前k个最小的,建大堆
  for (int i = (k - 2) / 2; i >= 0; i--) adjustdown(topk, k, i);
  for (int i = k; i < n; i++)
  {
    if (a[i] < topk[0])
    {
      topk[0] = a[i];
      adjustdown(topk, k, 0);
    }
  }
  for (int i = 0; i < k; i++) printf("%d ", topk[i]);
  free(topk);
}


test.c

#include "heap.h"
void test1()
{
  Heap hp;
  HeapInit(&hp);
  HeapPush(&hp, 22);
  HeapPush(&hp, 122);
  HeapPush(&hp, 16);
  HeapPush(&hp, 45);
  HeapPush(&hp, 56);
  HeapPush(&hp, 18);
  HeapPush(&hp, 134);
  HeapPush(&hp, 99);
  HeapDestroy(&hp);
}
void test2()
{
  Heap hp;
  HeapInit(&hp);
  int arr[] = { 34,113,78,44,98,99,35,26,18,68 };
  int n = sizeof(arr) / sizeof(arr[0]);
  HeapCreateArray(&hp, arr, n);
  while (!HeapEmpty(&hp))
  {
    printf("%d ", HeapTop(&hp));
    HeapPop(&hp);
  }
  printf("\n");
  HeapDestroy(&hp);
}
void test3()
{
  int arr[] = { 34,113,78,44,98,99,35,26,18,68 };
  int n = sizeof(arr) / sizeof(arr[0]);
  HeapSort(arr, n);
  for (int i = 0; i < n; i++) printf("%d ", arr[i]);
  printf("\n");
}
void testTopK()
{
  int arr[] = { 34,113,78,44,98,99,35,26,18,68 };
  int n = sizeof(arr) / sizeof(arr[0]);
  PrintTopK(arr, n, 5);
}
int main()
{
  //test1();
  test2();
  test3();
  testTopK();
  return 0;
}


写在最后


💝堆的实现一定要清楚它的逻辑结构和物理结构的区别,后面的堆排序与topk问题相对重要,大家不要忽视了哟!

❤️‍🔥后续将会持续输出有关数据结构与算法的文章,你们的支持就是我写作的最大动力!


感谢阅读本小白的博客,错误的地方请严厉指出噢~

相关文章
|
11天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
26 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
13天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
56 16
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
67 1
|
2月前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
52 5
【数据结构】优先级队列(堆)从实现到应用详解
|
1月前
|
存储 算法 调度
数据结构--二叉树的顺序实现(堆实现)
数据结构--二叉树的顺序实现(堆实现)
|
1月前
|
存储 算法 分布式数据库
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
1月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
29 0
|
1月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
1月前
【数据结构】大根堆和小根堆
【数据结构】大根堆和小根堆
27 0

热门文章

最新文章