• [数据结构]~堆


    前言

    作者小蜗牛向前冲

    名言我可以接受失败,但我不能接受放弃

     如果觉的博主的文章还不错的话,还请点赞,收藏,关注👀支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。

    目录

     一 堆

     二 堆的实现

    1 堆实现的功能

    2 向上调整算法和向下调整算法

    3 实现堆

    三 堆的应用

    1 堆排序 

    2 TOP-K问题



     一 堆

    1 堆的概念

      有一组数据,我们将他们用完全二叉树的方式储存在一个一维数组中,并满足一定规律。 则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆根节点最小的堆叫做最小堆或小根堆

     2 堆的性质

    堆中某个节点的值总是不大于或不小于其父节点的值;
    堆总是一棵完全二叉树

    这里我们要区分堆在逻辑上是个树的形状,但是在物理层面上是挨着存储的。 

     二 堆的实现

    1 堆实现的功能

    对于堆来是一般要实现入堆出堆的功能,这里我们对于堆的建立很像顺序表,但他们仍然是有很大区别的,下面大家可以在堆的实现过程中细细体会。

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. #include
    6. typedef int HPDataType;
    7. //定义堆
    8. typedef struct heap
    9. {
    10. HPDataType* a;
    11. int size;
    12. int capacity;
    13. }HP;
    14. //打印
    15. void HeapPrint(HP* php);
    16. //初始化
    17. void HeapInit(HP* php);
    18. //销毁堆
    19. void HeapDestroy(HP* php);
    20. // 入堆
    21. void HeapPush(HP* php, HPDataType x);
    22. // 出堆顶元素
    23. void HeapPop(HP* php);
    24. // 返回堆顶元素
    25. HPDataType HeapTop(HP* php);
    26. //判断堆是否为空
    27. bool HeapEmpty(HP* php);
    28. //求堆的大小
    29. int HeapSize(HP* php);
    30. //向上调整
    31. void ADjustDown(HPDataType* a, int n, int parant);
    32. //向下调整
    33. void ADjustUp(HPDataType* a, int child);
    34. //交换
    35. void swop(int* p1, int* p2);

    2 向上调整算法和向下调整算法

    在这里我们先来了解堆调整的二种算法:

    向上调整算法

    我们先说一个结论:

    数组小标计算父子关系公式:

    parant = (child-1)/2;

    LeftChild = parant*2+1;      奇数

    RightChild = parant*2+2;    偶数      

    好我们知道了,这个公式我们就可以通过数组建树 ,至于如何建堆,下面在说,我们继续可看到向上调整算法。

    我们知道是通过数组来建堆,我们要插入一个数据,直接插入到数组的后一个元素这肯定是不符合堆的规则的,那么我们可以用向上调整算法来进行调整,调整的方式见上图(小堆)。 

    下面我们用代码来实现一下:

    1. /向上调整
    2. void ADjustUp(HPDataType* a, int child)
    3. {
    4. int parent = (child - 1) / 2;
    5. while (child > 0)
    6. {
    7. //小堆
    8. if (a[parent] > a[child])
    9. {
    10. //交换
    11. swop(&a[parent], &a[child]);
    12. }
    13. else
    14. {
    15. break;
    16. }
    17. //向上调整
    18. child = parent;
    19. parent = (child - 1) / 2;
    20. }
    21. }

    那么我们在来思考一下,该算法的时间复杂度为都少呢?

    假设我们认为数有N层,我们入堆一个数,最坏的结果是N-1次也就是,该算法的时间复杂度为O(N)。

    向下调整算法

    这个算法我们可用来解决出堆的问题,为什么这么说呢?因为每次我们出堆,我们都要保持堆的结构的完整性,那么我们就要选出最小或者最大的数做堆顶。

    当我们要出堆时,只要让数组的首元素和最后的元素交换,在size--,在用向下调整算法就可以保持堆的结构的父子关系

    该算法的核心就是当在交换后,让首元素(父节点)和最小的子节点交换,以次类推得到小堆。

    要想得到大堆只要改变:

     把a[minChild]a[parant]即可。

    代码实现如下:

    1. void ADjustDown(HPDataType*a,int n,int parant)
    2. {
    3. int minChild = parant * 2 + 1;
    4. //找出最小的孩子
    5. while (minChild < n)
    6. {
    7. if (minChild + 1 < n && a[minChild] > a[minChild + 1])
    8. {
    9. minChild++;
    10. }
    11. if (a[minChild] < a[parant])
    12. {
    13. //交换
    14. swop(&a[parant], &a[minChild]);
    15. parant = minChild;
    16. minChild = parant * 2 + 1;
    17. }
    18. else
    19. {
    20. break;
    21. }
    22. }
    23. }

    那该算法的时间复杂度又是多少呢?

    我们假设该树有N层,总节点数为n:

    第1层:有2^0个节点

    第2层:有2^1个节点

    第3层:有2^2个节点

    …………………………

    第N层:有2^(N-1)个节点

    从中可以看出这就是个等比数列,所以我们直接通过公式求和:

    T(N)=2^N-1,对于该算法最坏的结果就是每层都要调整,则n = 2^N-1,所以时间复杂度为

    N = log(n+1),即为O(logn)

    综上说:

    向上调整算法的时间复杂度为O(n),而向下调整法的时间复杂度为O(logn),也就是向下调整算法的效率是更高的。

    3 实现堆

    堆的初始化

    1. //初始化堆
    2. void HeapInit(HP* php)
    3. {
    4. assert(php);//断言
    5. php->a = NULL;
    6. php->size = php->capacity = 0;
    7. }

    在入堆和出堆的过程中我们都次要用到交换这个功能,所以我们在这里去实现个交换功能

    1. //交换
    2. void swop(int* p1, int* p2)
    3. {
    4. int tmp = *p1;
    5. *p1 = *p2;
    6. *p2 = tmp;
    7. }

    堆的销毁

    1. //堆的销毁
    2. void HeapDestroy(HP* php)
    3. {
    4. assert(php);
    5. free(php->a);
    6. php->a = NULL;
    7. php->capacity = php->size = 0;
    8. }

     入堆

    这里用到了向上调整算法,上面我们知道向下调整算法是比向下调整更优的,所以说我们这里是可以改进的。

    1. void HeapPush(HP* php, HPDataType x)
    2. {
    3. assert(php);
    4. //扩容
    5. if (php->size == php->capacity)
    6. {
    7. int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    8. HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType*) * newCapacity);
    9. if (tmp == NULL)
    10. {
    11. perror("realloc fail");
    12. exit(-1);
    13. }
    14. php->a = tmp;
    15. php->capacity = newCapacity;
    16. }
    17. //插入
    18. php->a[php->size] = x;
    19. php->size++;
    20. //向上调整保持堆的形状
    21. ADjustUp(php->a, php->size - 1);
    22. }

    出堆顶元素

    1. //出堆顶元素
    2. void HeapPop(HP* php)
    3. {
    4. assert(php);
    5. assert(!HeapEmpty(php));
    6. //交换
    7. swop(&php->a[0], &php->a[php->size - 1]);
    8. php->size--;
    9. //向下调整
    10. ADjustDown(php->a,php->size,0);
    11. }

    返回堆顶元素

    1. //返回堆顶元素
    2. HPDataType HeapTop(HP* php)
    3. {
    4. assert(php);
    5. assert(!HeapEmpty(php));
    6. return php->a[0];
    7. }

    判断堆是否为空

    1. bool HeapEmpty(HP* php)
    2. {
    3. assert(判断堆是否为空php);
    4. return php->size == 0;//为空返回true,不为空返回flase
    5. }

    返回堆的长度

    1. //返回堆的长度
    2. int HeapSize(HP* php)
    3. {
    4. assert(php);
    5. return php->size - 1;
    6. }

    完整实现:

    1. #define _CRT_SECURE_NO_WARNINGS
    2. #include"heap.h"
    3. //打印堆
    4. void HeapPrint(HP* php)
    5. {
    6. assert(php);
    7. int i = 0;
    8. while (php->size > i)
    9. {
    10. printf("%d ", php->a[i]);
    11. ++i;
    12. }
    13. printf("\n");
    14. }
    15. //初始化堆
    16. void HeapInit(HP* php)
    17. {
    18. assert(php);//断言
    19. php->a = NULL;
    20. php->size = php->capacity = 0;
    21. }
    22. //交换
    23. void swop(int* p1, int* p2)
    24. {
    25. int tmp = *p1;
    26. *p1 = *p2;
    27. *p2 = tmp;
    28. }
    29. //向上调整
    30. void ADjustUp(HPDataType* a, int child)
    31. {
    32. int parent = (child - 1) / 2;
    33. while (child > 0)
    34. {
    35. //小堆
    36. if (a[parent] > a[child])
    37. {
    38. //交换
    39. swop(&a[parent], &a[child]);
    40. }
    41. else
    42. {
    43. break;
    44. }
    45. //向上调整
    46. child = parent;
    47. parent = (child - 1) / 2;
    48. }
    49. }
    50. //堆的销毁
    51. void HeapDestroy(HP* php)
    52. {
    53. assert(php);
    54. free(php->a);
    55. php->a = NULL;
    56. php->capacity = php->size = 0;
    57. }
    58. // 入堆
    59. void HeapPush(HP* php, HPDataType x)
    60. {
    61. assert(php);
    62. //扩容
    63. if (php->size == php->capacity)
    64. {
    65. int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    66. HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType*) * newCapacity);
    67. if (tmp == NULL)
    68. {
    69. perror("realloc fail");
    70. exit(-1);
    71. }
    72. php->a = tmp;
    73. php->capacity = newCapacity;
    74. }
    75. //插入
    76. php->a[php->size] = x;
    77. php->size++;
    78. //向上(或者向下)调整保持堆的形状
    79. ADjustDown(php->a, php->size, 0);
    80. }
    81. void ADjustDown(HPDataType*a,int n,int parant)
    82. {
    83. int minChild = parant * 2 + 1;
    84. //找出最小的孩子
    85. while (minChild < n)
    86. {
    87. if (minChild + 1 < n && a[minChild] > a[minChild + 1])
    88. {
    89. minChild++;
    90. }
    91. if (a[minChild] < a[parant])
    92. {
    93. //交换
    94. swop(&a[parant], &a[minChild]);
    95. parant = minChild;
    96. minChild = parant * 2 + 1;
    97. }
    98. else
    99. {
    100. break;
    101. }
    102. }
    103. }
    104. //出堆顶元素
    105. void HeapPop(HP* php)
    106. {
    107. assert(php);
    108. assert(!HeapEmpty(php));
    109. //交换
    110. swop(&php->a[0], &php->a[php->size - 1]);
    111. php->size--;
    112. //向下调整
    113. ADjustDown(php->a,php->size,0);
    114. }
    115. //返回堆顶元素
    116. HPDataType HeapTop(HP* php)
    117. {
    118. assert(php);
    119. assert(!HeapEmpty(php));
    120. return php->a[0];
    121. }
    122. //判断堆是否为空
    123. bool HeapEmpty(HP* php)
    124. {
    125. assert(php);
    126. return php->size == 0;//为空返回true,不为空返回flase
    127. }
    128. //返回堆的长度
    129. int HeapSize(HP* php)
    130. {
    131. assert(php);
    132. return php->size - 1;
    133. }

    三 堆的应用

    对于堆来是主要用途:

    堆排序
    解决TOP-K问题

    1 堆排序 

    为什么说堆可用来排序呢?我们知道堆顶一定是这堆中最大数或者最小数,所以我们可以利用这一特性进行排序。

    堆排序即利用堆的思想来进行排序,总共分为两个步骤:

    建堆

    升序:建大堆

    降序:建小堆

     对于建堆来说,我们可以去写一个堆,但这样太麻烦了,我们可以直接通过向下调整算法建堆。

    我们假设树的高度为h

    第1层,2^0个节点,需要向下移动h-1层

    第2层,2^1个节点,需要向下移动h-2层

    第3层,2^2个节点,需要向下移动h-3层

    第4层,2^3个节点,需要向下移动h-4层

    ………………………………………………
    第h-1层,2h-2个节点,需要向下移动1层

    调整次数 = 每一次层节点个数*这层最坏向下调整的次数

    我们建堆的时间复杂度我们可以通过计算得:

     所以说向下调整建堆的时间复杂度为O(N)

     利用堆删除思想来进行排序

    其实就是建堆尾和堆头交换,后通过向下调整算法,将最大数或者最小数倒序排出。

     完整代码:

    1. //思路:依次选择数,从后往前排
    2. // 升序------大堆
    3. // 降序------小堆
    4. //堆排
    5. void HeapSort(int* a, int n)
    6. {
    7. //从下调整建堆
    8. for (int i = (n - 2) / 2;i >= 0;--i)
    9. {
    10. ADjustDown(a, n, i);
    11. }
    12. //交换,选数
    13. int i = 1;
    14. while (i < n)
    15. {
    16. swop(&a[0], &a[n - i]);
    17. ADjustDown(a, n - i,0);
    18. ++i;
    19. }
    20. }

    2 TOP-K问题

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

    如求世界最高10人的身高,我们第1个想法是世界上所以的人的身高都排序个序,但这样是不是代价太大了,那我们有没有更加简单的方法呢?这将可以用到我们的堆了。

    用数据集合的前K个元素来建堆:

    前K个最大元素,建小堆

    前K个最小元素,建大堆

    堆中选数

    用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

    完整代码:

    1. #define _CRT_SECURE_NO_WARNINGS
    2. #include
    3. #include
    4. #include
    5. #include
    6. typedef int HPDataType;
    7. //定义堆
    8. typedef struct heap
    9. {
    10. HPDataType* a;
    11. int size;
    12. int capacity;
    13. }HP;
    14. //交换
    15. void swop(int* p1, int* p2)
    16. {
    17. int tmp = *p1;
    18. *p1 = *p2;
    19. *p2 = tmp;
    20. }
    21. //向下调整算法
    22. void ADjustDown(HPDataType* a, int n, int parant)
    23. {
    24. int minChild = parant * 2 + 1;//默认左孩子是最小
    25. //找出最小的孩子
    26. while (minChild < n)
    27. {
    28. if (minChild + 1 < n && a[minChild] > a[minChild + 1])
    29. {
    30. minChild++;
    31. }
    32. if (a[minChild] < a[parant])
    33. {
    34. //交换
    35. swop(&a[parant], &a[minChild]);
    36. parant = minChild;
    37. minChild = parant * 2 + 1;
    38. }
    39. else
    40. {
    41. break;
    42. }
    43. }
    44. }
    45. //创建数据
    46. void CreateDataFile(const char* filename, int N)
    47. {
    48. //以写入的方式打开文件
    49. FILE* fin = fopen(filename, "w");
    50. if (NULL==fin)
    51. {
    52. perror("fopen fail");
    53. return;
    54. }
    55. srand(time(0));
    56. //写入数据
    57. for (int i = 0;i < N;++i)
    58. {
    59. fprintf(fin, "%d\n", rand() % 1000000);
    60. }
    61. fclose(fin);
    62. }
    63. //TOP前10位数
    64. void PrintTopK(const char* filename, int k)
    65. {
    66. assert(filename);
    67. //打开文件
    68. FILE* fout = fopen(filename, "r");
    69. if (NULL==fout)
    70. {
    71. perror("fopen fail");
    72. return;
    73. }
    74. //为堆分配空间
    75. int* minHeap = (int*)malloc(sizeof(int) * k);
    76. if (NULL == minHeap)
    77. {
    78. perror("malloc fail");
    79. return;
    80. }
    81. //读取前K个元素
    82. for (int i = 0;i < k;++i)
    83. {
    84. fscanf(fout, "%d", &minHeap[i]);
    85. }
    86. //建出小堆
    87. for (int i = (k - 2) / 2; i >= k;--i)
    88. {
    89. ADjustDown(minHeap, k, i);
    90. }
    91. //比较后N-K个元素比堆顶元素大的就入堆
    92. int val = 0;
    93. while (fscanf(fout, "%d", &val) != EOF)
    94. {
    95. if (val > minHeap[0])
    96. {
    97. minHeap[0] = val;
    98. ADjustDown(minHeap, k, 0);
    99. }
    100. }
    101. //打印排序结果
    102. for (int i = 0;i < k;++i)
    103. {
    104. printf("%d ", minHeap[i]);
    105. }
    106. //释放空间
    107. free(minHeap);
    108. //关闭文件
    109. fclose(fout);
    110. }
    111. int main()
    112. {
    113. const char* filename = "Date.txt";
    114. int N = 1000000;
    115. int k = 10;
    116. //CreateDataFile(filename, N);
    117. PrintTopK(filename, k);
    118. return 0;
    119. }

     在这里博主遇到了一个BUG想了很久,想和大家分享一下;

    报了个断错误,我调试到90行代码时报错,我想这里这么会错误,断错误一般是传了个空参数

    ,我一想我就往上看,看一眼(我没错啊,我这么会错呢),就这样倒腾了半天。

     突然发现,自己将fout置空了。

     这样的问题大家都有遇到到吧!那我们有上面方式避免吗?

    其实有的,我们可以这样写。

     如果我们仍然把等于(==)写成了赋值(=)会这么样呢?

    这样编译就不会通过,这样我就能及时是发现问题并解决。

  • 相关阅读:
    openai的其他文本补全模型
    Python接口自动化测试数据驱动DDT使用实战
    Day2:写前端项目(html+css+js)
    JDK1.8新特性
    Linux系统编程系列之守护进程
    ZMQ之面向服务的可靠队列(管家模式)
    项目:数据库表的梳理
    1006 Sign In and Sign Out
    spring框架漏洞整理(Springboot漏洞)
    MySQL----事务
  • 原文地址:https://blog.csdn.net/qq_61552595/article/details/126828125