深入浅出堆—C语言版【数据结构】

简介: 深入浅出堆—C语言版【数据结构】

1. 了解堆

1.1 堆的概念


1.2 堆的性质:

堆中某个节点的值总是不大于或不小于其父节点的值;

堆总是一棵完全二叉树。


1.3 堆的结构图片

1.3.1 小堆

满足下面条件的是小堆


1.3.2 大堆

满足下面条件的是大堆

注意不一定是从大到小、从小到大存储的!!!


堆有什么作用呢?

下面来细讲,别走开!!!

2. 堆的实现

2.1 插入数据进堆

void HeapPush(HP* 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)
    {
      printf("realloc fail\n");
      exit(-1);
    }
    php->a = tmp;
    php->capacity = newcapacity;
  }
  php->a[php->size] = x;
  php->size++;
  AdjustUp(php->a, php->size - 1);
}

注意点!!!


假如一开始我们的堆是小堆,但是在插入数据以后要保持还是小堆,要将插入的数据的大小和它的父亲进行比较,比较的两种情况:


1. 如果插入的数据比父亲还要大,那就不需要调整


2. 如果插入的数据比父亲还要小,那就需要调整  


 如果需要调整,我们就要使用向上调整算法,保持插入数据后的堆还是小堆


2.2 向上调整函数

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

2.3 堆的删除

能不能使用覆盖删除呢—不能!!!

使用覆盖删除,会打乱父子之间的下标关系,父子关系就会全部乱掉,因此我们使用下面的方法来删除数据


1. 先将下标为0位置的数据和下标最大的数据进行交换

2. 然后直接size--

3. 然后还需要使用向下调整算法,把堆再调整为小堆


void HeapPop(HP* php)
{
  assert(php);
  assert(php->size > 0);
  Swap(&(php->a[0]), &(php->a[php->size - 1]));1.交换
  php->size--;//2. 删除堆顶元素
  AdjustDwon(php->a, php->size, 0);//向下调整,保证还是小堆
}

2.4 向下调整

void AdjustDwon(HPDataType* a, int size, int parent)
{
  int child = parent * 2 + 1;
  while (child < size)
  {
    // 选出左右孩子中小那个
        //这里的if里面的判断大小尽量写成小于是小堆,大于是大堆
    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;
    }
  } 
}


3. 堆的应用

3.1 建堆(两种方式)

3.1.1 建堆方式1

利用插入元素的方式,向上调整建堆

void AdjustUp(HPDataType* a, int child)
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    //if (a[child] < a[parent])
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
/
void HeapSort(int* a, int n)//传一个数组过来,还有元素个数
{
  // 建堆方式1:O(N*logN)
  for (int i = 1; i < n; ++i)
  {
    AdjustUp(a, i);//从插入的第二个元素开始
  }
}


建堆方式1的时间复杂度 ——错位相减法

3.1.2 建堆方式2

利用向下调整建堆

方法:找到最后一个元素的父亲,并从这个位置开始向下调整                                  

void HeapSort(int* a, int n)
{
  // 建堆方式2:O(N)
  for (int i = (n-1-1)/2; i >= 0; --i)
  {
    AdjustDwon(a, n, i);
  }
  // O(N*logN)
  int end = n - 1;
  while (end > 0)
  {
    Swap(&a[0], &a[end]);
    AdjustDwon(a, end, 0);
    --end;
  }
}


建堆方式2的时间复杂度——错位相减法



3.2 堆排序

排升序,建大堆,再向下调整

为什么建大堆呢?

建大堆,堆顶元素是最大的数,让堆顶元素和最后一个元素交换,再向下调整,注意:这里向下调整时是调整的数组大小-1个,也就是调整刚刚交换下来前面的数据


排降序,建小堆,再向下调整


void HeapSort(int* a, int n)
{
  // 建堆方式2:O(N)
  for (int i = (n-1-1)/2; i >= 0; --i)
  {
    AdjustDwon(a, n, i);
  }
  // O(N*logN)
  int end = n - 1;
  while (end > 0)
  {
    Swap(&a[0], &a[end]);//这里的end是9,传过去向下调整的元素个数也是9,
                             //就不会调整刚刚从堆顶传下来的数据
    AdjustDwon(a, end, 0);
    --end;
  }

3.3 堆的TOP—K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。


实现思路:

这样空间复杂度非常小

注意:


          求前k个最大的数,是建小堆


          解释:由于建立的前k个数是小堆,后面n-k个数都可能比对顶的数值大,比堆顶的元素大,就替换堆顶的元素,然后再向下调整,保持前k个数是小堆,然后再比较····


          求前k个最小的数,是建大堆(同上)


代码实现:

void PrintTopK(int* a, int n, int k)
{
  // 1. 建堆--用a中前k个元素建堆
  int* kMinHeap = (int*)malloc(sizeof(int)*k);
  assert(kMinHeap);
  for (int i = 0; i < k; ++i)//将a数组里面前10个数赋值给KMinHeap
  {
    kMinHeap[i] = a[i];
  }
  for (int i = (k - 1 - 1) / 2; i >= 0; --i)//向下调整建堆,建立k个数的小堆
  {
    AdjustDwon(kMinHeap, k, i);
  }
  // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
  for (int j = k; j < n; ++j)
  {
    if (a[j] > kMinHeap[0])
    {
      kMinHeap[0] = a[j];
      AdjustDwon(kMinHeap, k, 0);//再向下调整,保持前k个数是小堆
    }
  }
  for (int i = 0; i < k; ++i)
  {
    printf("%d ", kMinHeap[i]);
  }
  printf("\n");
}
void TestTopk()
{    
    //随机生成一万个数字,每个数字%1百万,这一万都是比一百万小的数字,
    //我们将其中的10个数改为比一百万大的值
  int n = 10000;
  int* a = (int*)malloc(sizeof(int)*n);
  srand(time(0));
  for (int i = 0; i < n; ++i)
  {
    a[i] = rand() % 1000000;
  }
  a[5] = 1000000 + 1;
  a[1231] = 1000000 + 2;
  a[531] = 1000000 + 3;
  a[5121] = 1000000 + 4;
  a[120] = 1000000 + 5;
  a[99] = 1000000 + 6;
  a[0] = 1000000 + 7;
  a[76] = 1000000 + 8;
  a[423] = 1000000 + 9;
  a[3144] = 1000000 + 10;
  PrintTopK(a, n, 10);
}

本文讲的是二叉树的顺序存储结构(堆)的实现,下期我们来讲二叉树的链式存储结构,到时候记得来支持小余哦!!!

如果觉得文章不错,期待你的一键三连哦,你个鼓励是我创作的动力之源,让我们一起加油,顶峰相见!!!

相关文章
|
22天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
109 9
|
19天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
29 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
21天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
60 16
|
21天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
72 8
|
24天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
50 4
|
25天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
56 3
|
24天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
40 0
|
13天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
21 1
|
16天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。
|
19天前
|
存储 JavaScript 前端开发
执行上下文和执行栈
执行上下文是JavaScript运行代码时的环境,每个执行上下文都有自己的变量对象、作用域链和this值。执行栈用于管理函数调用,每当调用一个函数,就会在栈中添加一个新的执行上下文。