【数据结构】二叉树——堆如何实现

简介: 【数据结构】二叉树——堆如何实现

一、二叉树的顺序结构

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

二、堆的概念及结构

如果有一个关键码的集合K ={ k0,k1,k2……,k(n-1)}【0,1,2,……,n-1这些都是下标】,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=k(2*i+1)且Ki<=k2*i+2【Ki>=k(2*i+1)且Ki>=k(2*i+2)】i=0,1,2…,则称为小堆【或大堆】。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:1)堆中某个节点的值总是不大于或不小于其父节点的值;2)堆总是一棵完全二叉树。

理解:堆分为大堆和小堆;大堆/大根堆:树中父亲的数据都大于等于孩子;小堆/小根堆:树中父亲的数据都小于等于孩子

堆解决的问题:堆排序、TOP-K

三、堆的实现

heap.h

1. #pragma once
2. 
3. #include <stdio.h>
4. #include <assert.h>
5. #include <stdlib.h>
6. #include <stdbool.h>
7. 
8. typedef int HPDataType;
9. 
10. typedef struct Heap
11. {
12.   HPDataType* a;
13.   size_t size;
14.   size_t capacity;
15. }HP;
16. 
17. void HeapInit(HP* php);
18. void HeapDestory(HP* php);
19. void HeapPrint(HP* php);
20. void Swap(HPDataType* pa, HPDataType* pb);
21. void HeapPush(HP* php, HPDataType x);
22. void HeapPop(HP* php);
23. bool HeapEmpty(HP* php);
24. size_t HeapSize(HP* php);
25. HPDataType HeapTop(HP* php);

heap.c

1. 
2. #include "heap.h"
3. 
4. void HeapInit(HP* php)
5. {
6.  assert(php);
7.  php->a = NULL;
8.  php->size = php->capacity = 0;
9. }
10. void HeapDestory(HP* php)
11. {
12.   assert(php);
13.   free(php->a);
14.   php->a = NULL;
15.   php->size = php->capacity = 0;
16. }
17. 
18. //按数组打印
19. void HeapPrint(HP* php)
20. {
21.   assert(php);
22.   for (size_t i = 0; i < php->size; ++i)
23.   {
24.     printf("%d ", php->a[i]);
25.   }
26.   printf("\n");
27. }
28. 
29. void Swap(HPDataType* pa, HPDataType* pb)
30. {
31.   HPDataType tmp = *pa;
32.   *pa = *pb;
33.   *pb = tmp;
34. }
35. 
36. bool HeapEmpty(HP* php)
37. {
38.   assert(php);
39.   return php->size == 0;
40. }
41. //多少个数据
42. size_t HeapSize(HP* php)
43. {
44.   assert(php);
45.   return php->size;
46. }
47. HPDataType HeapTop(HP* php)
48. {
49.   assert(php);
50.   assert(php->size > 0);
51.   return php->a[0];
52. }
1. void AdjustUp(HPDataType* a, size_t child)
2. {
3.  size_t parent = (child - 1) / 2;
4.  //这个比较取决于大小堆
5.  //小堆
6.  //最后一次比较,是parent是0,进行比较,当再次进行调整后。就不需要进行了,此时的child等于0,parent也是0[因为size_t是正整数】
7.  //-1/2还是等于0
8.  while (child > 0)
9.  {
10.     if (a[child] < a[parent])
11.     {
12.       Swap(&a[child], &a[parent]);
13.       child = parent;
14.       parent = (child - 1) / 2;
15.     }
16.     else
17.     {
18.       break;//跳出循环
19.     }
20.   }
21. }
22. 
23. void HeapPush(HP* php, HPDataType x)
24. {
25.   assert(php);
26.   数据插入数组后
27.   //先判断是否有地方进行扩容
28.   if (php->size == php->capacity)
29.   {
30.     size_t newCapacity = php->capacity == 0 ? 4 : (2 * (php->capacity));
31.     //开辟空间,要有一个临时变量进行开辟,否则如果开辟失败,里面的数据就都找不到了
32.     HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
33.     if (tmp == NULL)
34.     {
35.       printf("malloc fail\n");
36.       exit(-1);
37.     }
38.     php->a = tmp;
39.     php->capacity = newCapacity;
40.   }
41.   php->a[php->size] = x;
42.   (php->size)++;//先插入,后size++,此时size这个下标的位置并没有值
43.   向上调整的算法,成为堆
44.   size_t child = (php->size) - 1;
45.   AdjustUp(php->a, child);
46. }
47.

堆的插入:先插入一个数字到数组的尾上【插入的这个数字后,可能不满足堆的概念】,再进行向上调整算法,直到满足堆

1. void AdjustDown(HPDataType* a, size_t root, size_t size)
2. {
3.  //找出小的
4.  //注意:可能没有右孩子
5.  size_t parent = root;
6.  size_t child = parent * 2 + 1;
7.  while (child < size)
8.  {
9.    //避免越界
10.     if (child + 1 < size && a[child] > a[child + 1])
11.     {
12.       child++;
13.     }
14.     if (a[child] < a[parent])
15.     {
16.       Swap(&a[child], &a[parent]);
17.       parent = child;
18.       child = parent * 2 + 1;
19.     }
20.     else
21.     {
22.       break;//跳出循环
23.     }
24.   }
25. }
26. 
27. void HeapPop(HP* php)
28. {
29.   assert(php);
30.   //当删除数据的时候,要判断有没有值
31.   assert(php->size > 0);
32.   Swap(&php->a[0], &php->a[php->size - 1]);
33.   php->size--;
34.   AdjustDown(php->a, 0, php->size);
35. }

堆的删除:删除堆是删除堆顶【最小或者最大的数据】的数据,将堆顶的数据和最后一个数据交换,然后删除数组最后一个数据,再进行向下调整算法。【先交,后删除,在进行向下调整算法】    

向下调整算法:首先找出两个孩子节点中小(大)的那一个,然后去和父节点比较,进行交换,父节点的数据总是小于等于(大于等于)子节点,然后再从交换的孩子向下比较】

堆的插入、删除的时间复杂度为O(logN)  

四、堆的应用

4.1 堆排序

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

1. 建堆(在数组上建堆,那么堆排序的空间复杂度为O(1))

升序:建大堆

降序:建小堆

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

4.1.1 建堆

建堆有两种方法:(1)使用向上调整,插入数据的思想建堆。插入数据到新的数组,就是在不断插入的过程中向上调整实现排序 【代码1】(2)使用向下调整【从倒数第一个非叶子节点开始,即最后一个节点的父亲,即(size-1-1)/2】【找到这个父亲的节点,向下排序,然后这个父亲节点依次减一【就找到各个小堆】,依次向下排序,就成为了一个堆。】【代码2】

【建堆结束后,可以让数组成为一个堆】

代码1展示:

1. void Swap(HPDataType* pa, HPDataType* pb)
2. {
3.  HPDataType tmp = *pa;
4.  *pa = *pb;
5.  *pb = tmp;
6. }
7. 
8. 
9. void AdjustUp(HPDataType* a, size_t child)
10. {
11.   size_t parent = (child - 1) / 2;
12.   //这个比较取决于大小堆
13.   //小堆
14.   //最后一次比较,是parent是0,进行比较,当再次进行调整后。就不需要进行了,此时的child等于0,parent也是0[因为size_t是正整数】
15.   //-1/2还是等于0
16.   while (child > 0)
17.   {
18.     if (a[child] < a[parent])
19.     {
20.       Swap(&a[child], &a[parent]);
21.       child = parent;
22.       parent = (child - 1) / 2;
23.     }
24.     else
25.     {
26.       break;//跳出循环
27.     }
28.   }
29. }
30. 
31. void HeapSort(int* a, int n)
32. {
33.   //升序,建大堆,向上
34.   size_t i = 0;
35.   for (i = 1; i < n; ++i)
36.   {
37.     AdjustUp(a, i);
38.   }
39. }
40. 
41. int main()
42. {
43.   int a[] = { 4, 3, 10 , 2, 5, 9 };
44.   HeapSort(a, sizeof(a) / sizeof(int));
45.   for (int i = 0; i < sizeof(a) / sizeof(int); i++)
46.   {
47.     printf("%d ", a[i]);
48.   }
49.   printf("\n");
50.   return 0;
51. }

代码2展示:

1. void HeapSort(int* a, int n)
2. {
3.  //升序,建堆,向上
4.  /*int i = 0;
5.  for (i = 1; i < n; ++i)
6.  {
7.    AdjustUp(a, i);
8.  }*/
9. //向下
10.   int i = 0;
11.   for (i = (n - 2) / 2; i >= 0; --i)
12.   {
13.     AdjustDown(a, i, n);
14.   }
15. }

建堆的时间复杂度:

向上建堆:首先每一层的节点数为2^(h-1);建堆是从第二层开始插入数据,第二层有2^(2-1)个节点,成为一个堆,向上调整的最坏次数为2^(2-1)*1;第三层有2^(3-1)个节点,成为一个堆,向上调整的次数为2^(3-1)*2;……;那么向上调整累积建堆次数为2^(2-1)*1+2^(3-1)*2+2^(4-1)*3+……+2^(h-1)*(h-1)。这是一个等差数列*等比数列。利用错位相减,可以算出次数为2^h*(h-2)+2; 最终时间复杂度为O(N*logN)

向下建堆:首先每一层的节点数为2^(h-1);建堆是从(从倒数第一个非叶子节点开始)【这个非叶子节点不一定是倒数第二层的最后一个,但是此时可以把堆看做满级二叉树【两者的时间复杂度,差别不大】,那么此时的非叶子节点就是倒数第二层的最后一个】倒数第二层开始向下调整,一直到第一层向下调整结束,每一层有2^(h-1)个节点,每一个节点和下面部分成为一个堆,每个节点向下调整的最坏次数为2^(h-1)*(h);那么向下调整累积建堆次数为2^0*(h-1)+2^1*(h-2)+2^2*(h-2)+……+2^(h-2)*1,这是一个等差数列*等比数列。利用错位相减,可以算出次数为2^h-1-h,因为2^h-1=N,; 最终时间复杂度为O(N).。

总结:建堆最好用向下建堆

建堆:升序建大堆,降序建小堆。【如果升序建小堆,最小的数已经在第一个位置了,再次选出次小的,需要不断建堆选数。那么总的时间复杂度为O(N^2),既然这样,还不如直接遍历选数,时间复杂度也是O(N^2)】【升序应该建大堆】

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

升序,大堆为例:建立大堆之后,最大值就在最前面,然后,最大值和最后一个值【下标为n-1】进行互换,然后不管n-1这个下标进行建堆,然后最大值再次与最后一个值进行【下标为n-2】进行互换。一直到下标为0的元素与下标为1的元素进行过交换,数组就完成了排序。【时间复杂度为:O(N*logN)】

4.2 TOP-K问题

N个数找出最大/最小的前K个

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

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

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能

数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前 K 个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

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

将剩余N-K个元素 依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

时间复杂度为:O(K+logK*(N-K));空间复杂度为:O(K).

1. void PrintTopK(int* a, int n, int k)
2. {
3.  // 建堆--用a中前k个元素建堆
4.  int* kminHeap = (int*)malloc(sizeof(int) * k);
5.  if (kminHeap == NULL)
6.  {
7.    printf("malloc fail \n");
8.    exit(-1);
9.  }
10.   //前k个元素,放在数组里面
11.   for (int i = 0; i < k; ++i)
12.   {
13.     kminHeap[i] = a[i];
14.   }
15. 
16.   // 建小堆
17.   for (int j = (k - 2) / 2; j >= 0; --j)
18.   {
19.     AdjustDown(kminHeap, j, k);//k指的是下标,数组最后元素的下标,为了方便找到父节点
20.   }
21. 
22.   // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
23.   for (int i = k; i < n; ++i)
24.   {
25.     if (a[i] > kminHeap[0])
26.     {
27.       kminHeap[0] = a[i];
28.       AdjustDown(kminHeap, 0, k);
29.     }
30.   }
31. 
32.   for (int j = 0; j < k; ++j)
33.   {
34.     printf("%d ", kminHeap[j]);
35.   }
36.   printf("\n");
37.   free(kminHeap);
38. }
39. 
40. void TestTopk()
41. {
42.   int n = 10000;
43.   int* a = (int*)malloc(sizeof(int) * n);
44.   srand(time(0));
45.   for (size_t i = 0; i < n; ++i)
46.   {
47.     a[i] = rand() % 1000000;
48.   }
49.   a[5] = 1000000 + 1;
50.   a[1231] = 1000000 + 2;
51.   a[531] = 1000000 + 3;
52.   a[5121] = 1000000 + 4;
53.   a[115] = 1000000 + 5;
54.   a[2305] = 1000000 + 6;
55.   a[99] = 1000000 + 7;
56.   a[76] = 1000000 + 8;
57.   a[423] = 1000000 + 9;
58.   a[0] = 1000000 + 1000;
59.   PrintTopK(a, n, 10);
60. }


相关文章
|
13天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
28 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
15天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
57 16
|
15天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
65 8
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
68 1
|
1月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
20 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
1月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
21 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
1月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
27 1
|
1月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
24 1
|
1月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆