当前位置:   article > 正文

堆的结构实现与应用

堆的结构实现与应用

目录

         前言:

1.认识堆

a.如何认识堆?

b.大根堆与小根堆

c.堆应用的简单认识

2.堆的结构与要实现的功能

3.向上调整算法

4.向下调整算法

5.向堆插入数据并建堆

6.堆的大小

7.堆的判空

8.取堆顶数据

9.删除堆顶数据

10.向上调整时间复杂度

11.向下调整时间复杂度

12.堆排序

a.直接将数组放到堆再取堆顶

b.在将数组放到堆的时候就直接调整,用数组建堆

13.topk问题

总结:


前言:

堆其实与二叉树息息相关,本篇将从如何实现堆,以及堆的应用等方面入手。

1.认识堆

a.如何认识堆?

我们只要记住关键的两点:1.堆必须是完全二叉树。2.堆要么是大堆,要么是小堆。

b.大根堆与小根堆

那什么是大堆,什么又是小堆呢?

大堆:树中任意一个父亲都大于或等于孩子。

小堆:树中任意一个父亲都小于或等于孩子。

c.堆应用的简单认识

堆排序:时间复杂度为O(N*logN),属于快一点的排序。

topk问题:N个数找最大的前K个。

优先级队列:C++中stl的priority_queue容器的底层实现需要用到建堆的思想。

2.堆的结构与要实现的功能

  1. #pragma once
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <stdbool.h>
  5. #include <assert.h>
  6. typedef int HPDataType;
  7. typedef struct Heap
  8. {
  9. HPDataType* a;
  10. HPDataType size;
  11. HPDataType capacity;
  12. }HP;
  13. void InitHeap(HP* php);
  14. void DestroyHeap(HP* php);
  15. void PushHeap(HP* php, HPDataType x);
  16. void HeapPop(HP* php);
  17. HPDataType HeapTop(HP* php);
  18. bool HeapEmpty(HP* php);
  19. int HeapSize(HP* php);
  20. void AdjustUp(HPDataType* a, int child);
  21. void AdjustDown(int* a, int n, int parent);
  22. void Swap(HPDataType* p1, HPDataType* p2);

我们提供以下数据来建堆:

3.向上调整算法

我们现在如果要让提供的数据插入到这个堆里面,如何保证插入的时候就建好堆了呢?这时我们就要用到向上向下调整的算法了。假设我们现在要建一个小堆,就要保证每一个子节点都要小于或者等于它的父节点,用我们提供好的数据,当插入到32这个数据的时候就要进行调整了:

既然是向上调整,那我们就要找孩子的父亲,那如何找到父亲呢?通过下标的关系可以发现,不管是左还是右孩子,只要遵循(child-1)/2就能找到父亲的下标,然后就是交换嘛;交换过后我们要让孩子走到父亲位置,再找到新的父亲,一轮一轮的向上,这就是向上调整算法:

  1. void AdjustUp(HPDataType* a, int child)
  2. {
  3. //默认建小堆
  4. int parent = (child - 1) / 2;
  5. while (child > 0)//等于0就停止了,等于0说明孩子在根的位置,就没有父亲了
  6. {
  7. if (a[child] < a[parent])
  8. {
  9. Swap(&a[child], &a[parent]);
  10. child = parent;//孩子走到父亲的位置继续再找新的父亲
  11. parent = (child - 1) / 2;
  12. }
  13. else
  14. {
  15. break;
  16. }
  17. }

要注意的就是循环条件,如果孩子走到0说明走到根的位置了,就没有父亲了,循环停止。

4.向下调整算法

向下调整算法与向上调整算法相反,其实向下就是找孩子,如果建小堆就让左右孩子中最小的孩子与父亲交换(最小的孩子与父亲交换后,父亲就变成三者中最小的了,符合小堆的性质),再让父亲走到孩子的位置上,再往下继续找到新的孩子,直到孩子不存在的情况。

关键来了,如何找到左右孩子中的最小的那个呢?如何通过父亲找到孩子呢?

我们可以默认左孩子是最小的那一个,如果左孩子大于右孩子,那做孩子的下标+1不就到右孩子了吗?解决了第一个问题,那如何通过父亲找到孩子呢,既然我们默认左孩子是小的那一个,我们可以先找到左孩子,通过下标的关系,我们就知道左孩子child=parent*2+1,好了,这就是向下调整算法的思路,来看代码:

  1. void AdjustDown(int* a, int n, int parent)
  2. {
  3. //默认小堆
  4. int child = parent * 2 + 1;//默认是左孩子
  5. while (child < n)
  6. {
  7. //这里右孩子的存在条件必须放在&&的前面,因为如果放在后面,前面的条件为假,右孩子也为假,就判断不出来是哪个了(检查右孩子存在必须更严格)
  8. if (child + 1 < n && a[child] > a[child + 1])//如果右孩子存在(因为如果左孩子为n-1,那右孩子就为n了,就越界了)并且左孩子大于右孩子,下标就走到右孩子上
  9. {
  10. child = child + 1;
  11. }
  12. if (a[child] < a[parent])
  13. {
  14. Swap(&a[child], &a[parent]);
  15. parent = child;
  16. child = parent * 2 + 1;
  17. }
  18. else
  19. {
  20. break;
  21. }
  22. }
  23. }

注意点:

1.我们既然默认左孩子为小的那一个,那结束条件就应该是左孩子不存在的情况即当左孩子等于n的时候就越界了,而又由于堆是完全二叉树,所以左孩子不存在,那右孩子一定不存在,所以只写这一个就行。

2.child + 1 < n && a[child] > a[child + 1],首先需要注意左孩子存在,但右孩子不存在的情况,所以判断child+1<n,其次这个条件要写到&&的前面,因为如果写到后面,a[child]>a[child+1]为假,就判断不出右孩子可能越界的情况了,所以右孩子的检查应该放到&&前面。

5.向堆插入数据并建堆

  1. void PushHeap(HP* php, HPDataType x)
  2. {
  3. assert(php);
  4. if (php->size == php->capacity)
  5. {
  6. int NewCapacity = php->capacity = 0 ? 4 : php->capacity * 2;
  7. HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * NewCapacity);
  8. if (tmp == NULL)
  9. {
  10. perror("realloc fail");
  11. return;
  12. }
  13. php->a = tmp;
  14. php->capacity = NewCapacity;
  15. }
  16. php->a[php->size] = x;
  17. php->size++;
  18. AdjustUp(php->a, php->size - 1);//向上调整传孩子,即插入的数据,找父亲
  19. }

需要注意的是最后向上调整传的孩子是插入的数据的下标,因为插入后size++了,所以-1才对应插入的数据的下标。

6.堆的大小

  1. int HeapSize(HP* php)
  2. {
  3. assert(php);
  4. return php->size;
  5. }

7.堆的判空

  1. bool HeapEmpty(HP* php)
  2. {
  3. assert(php);
  4. return php->size == 0;
  5. }

8.取堆顶数据

  1. HPDataType HeapTop(HP* php)
  2. {
  3. assert(php);
  4. assert(!HeapEmpty(&php));
  5. return php->a[0];
  6. }

9.删除堆顶数据

如果我们直接删除堆顶的数据会导致这个堆变乱,所以我们采用交换堆顶和堆尾的数据,将堆的大小减1,这样就访问不到堆尾的数据也就起到了删除的效果了,然后我们再从根节点开始做向下调整算法,恢复堆即可,注意空堆不能删,要判空:

  1. void HeapPop(HP* php)
  2. {
  3. assert(php);
  4. assert(!HeapEmpty(&php));
  5. Swap(&php->a[0], &php->a[php->size - 1]);
  6. php->size--;
  7. AdjustDown(php->a, php->size, 0);//从根节点开始向下调整
  8. }

10.向上调整时间复杂度

按最坏的情况计算时间复杂度,就拿满二叉树来说:

我们先找到每一次的节点数,再乘向上调整的次数,假设树的高度为h,那我们就将1-h层的所有节点的调整次数相加,就是时间复杂度(计算采用等比数列求和的乘公比错位相减的方法):

又因为满二叉树的节点个数为2^h-1,所以我们设树有N个节点,就能得到高度,再代入F(h):

实际去除不影响结果的项也就是O(N*logN),N为节点个数。

11.向下调整时间复杂度

一样拿满二叉树来说:

实际计算结果:

实际去除不影响结果的项也就是O(N),N为节点个数。

12.堆排序

a.直接将数组放到堆再取堆顶

  1. void HeapSort1(int* a, int n)
  2. {
  3. HP hp;
  4. InitHeap(&hp);
  5. for (int i = 0; i < n; i++)
  6. {
  7. PushHeap(&hp, a[i]);
  8. }
  9. int i = 0;
  10. while (!HeapEmpty(&hp))
  11. {
  12. int top = HeapTop(&hp);
  13. a[i++] = top;
  14. HeapPop(&hp);
  15. }
  16. }

这样的坏处就是想要改升序降序要改向上向下调整的逻辑,有些麻烦,而且时间上有些麻烦,需要堆排的时候还要写一个堆出来。

b.在将数组放到堆的时候就直接调整,用数组建堆

如果我们要排成降序,就建小堆,小堆选出最小的,首尾交换,最小的放到最后的位置,最后一个数据不看做堆里面的,再次向下调整就可以选出次小的,以此类推,相当于一个一个头插;

调用一次是O(logN),N次就是O(N*logN),计算方法跟向下调整差不多;

向下调整建堆需要倒着调整,叶子节点不需要处理,倒数第一个非叶子节点即最后一个节点的父亲开始调整:

  1. void HeapSort(int* a, int n)
  2. {
  3. for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  4. {
  5. AdjustDown(a, n, i);
  6. }
  7. int end = n - 1;
  8. while (end > 0)
  9. {
  10. Swap(&a[0], &a[end]);
  11. AdjustDown(a, end, 0);
  12. --end;
  13. }
  14. }

如果我们要排升序,只需要改动向下调整或者写两个建堆方法:

改动两处:选孩子时选大的那个;如果孩子大于父亲,交换:

建堆时也可向上调整建堆,具体实现博主暂时不清楚~~~:

  1. for (int i = 1; i < n; i++)//下标为0即第一个数默认是堆
  2. {
  3. AdjustUp(a, i);//建堆,相当于一个一个插入成堆
  4. }

注意:

1.为什么升序不建小堆呢?因为小堆最小的已经在前面了,不管是移动还是怎么剩下的都要重新建堆

2.堆排序整体时间复杂度为N+N*logN,也就是O(N*logN)

13.topk问题

什么是topk问题?

就是N个数找最大的前N个:

面对庞大的数据,数据放在磁盘的文件里面,而内存是有限的,所以我们将这些数据的前k个建堆,将剩下的数据与堆顶元素进行比较,符合条件就交换,然后再调整,重复操作即可,那该怎么建堆呢?首先对前3 数据进行建一个小堆,注意这里不能建大堆,如果建大堆的话,可能最大的数据在前三个数,其余2个数据在余下的 N-K个数里面,这样其余2个就不能进堆了:

  1. void CreateNData()
  2. {
  3. //造数据
  4. int n = 10000;
  5. srand((unsigned int)time(0));
  6. const char* file = "data.txt";
  7. FILE* fin = fopen(file, "w");
  8. if (fin == NULL)
  9. {
  10. perror("fopen error");
  11. return;
  12. }
  13. for (size_t i = 0; i < n; i++)
  14. {
  15. int x = rand() % 1000000;
  16. fprintf(fin, "%d\n", x);
  17. }
  18. fclose(fin);
  19. }
  20. void PrintTopK(int k)
  21. {
  22. const char* file = "data.txt";
  23. FILE* fout = fopen(file, "r");
  24. if (fout == NULL)
  25. {
  26. perror("fopen error");
  27. return;
  28. }
  29. int* kminheap = (int*)malloc(sizeof(int) * k);
  30. if (kminheap == NULL)
  31. {
  32. perror("malloc error");
  33. return;
  34. }
  35. for (int i = 0; i < k; i++)
  36. {
  37. fscanf(fout, "%d", &kminheap[i]);
  38. }
  39. for (int i = (k - 1 - 1) / 2; i >= 0; i--)
  40. {
  41. AdjustDown(kminheap, k, i);
  42. }
  43. int val = 0;
  44. while (!feof(fout))//fscanf读到文件结尾,调用feof,feof读到文件末尾返回非0,否则返回0
  45. {
  46. fscanf(fout, "%d", &val);
  47. if (val > kminheap[0])
  48. {
  49. kminheap[0] = val;
  50. AdjustDown(kminheap, k, 0);
  51. }
  52. }
  53. for (int i = 0; i < k; i++)
  54. {
  55. printf("%d ", kminheap[i]);
  56. }
  57. printf("\n");
  58. }

总结:

堆的结构不难,难在和其他的场景联系到一起并涉及一些算法,所以还是掌握好它结构和算法的基础为主

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/138927
推荐阅读
相关标签
  

闽ICP备14008679号