目录
一. 知识铺垫(满二叉树和完全二叉树的一些性质)
二.堆的结构和性质
三.堆排序
步骤一:建堆
向上调整建堆(时间复杂度:N*logN):
时间复杂度的计算:
向下调整建堆(时间复杂度:O(N))
时间复杂度的计算:
步骤二:利用堆的删除思想来讲新排序
最终代码实现
堆排序的总体时间复杂度
TOP K问题
二叉树具有很多性质,这边主要介绍几个在堆中应用比较多的几个重要性质:
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1) 个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是 2^h-1.
3. 若规定根节点的层数为1,则二叉树的深度是 log₂(n-1).
4.对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
i=0,i为根节点编号,无双亲节点
i位置节点的 双亲序号:(i-1)/2;
i位置节点的 左孩子序号: 2i+1, 若2i+1>=size 则无左孩子 ;
i位置节点的 右孩子序号: 2i+2, 若2i+2>=size 则无左孩子 ;
ps:如果记不下这些结论,可以随手画出 如上图所示的二叉树,标上下标,结论就显而易见了
- 堆 总是 一棵完全二叉树 , 所以我们通常用一块连续的空间来存储堆(所以他具备完全二叉树所拥有的性质 包含但不限于上面那些重要性质)
- 堆中的某个节点的值总是大于或小于其孩子节点(所以他的根节点永远是结构中的最大值或最小值)
- 父亲大于孩子的叫大堆,根节点为最大值
- 父亲小于孩子的叫小堆,根节点为最小值
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的实现大体可以分为两步:
1.建堆: 升序建大堆, 降序建小堆
2.利用堆的删除思想来讲新排序: 将堆顶的数据(以升序为例,堆顶元素即为最大值)和最后一个数据交换(找到了最大值,并放到相应的位置)然后后对堆顶的数据再进行向下调整算法(维护堆结构),如此往复.便可以实现排序
关于堆排序最关键的步骤就是建堆了,建堆有2种方法,分别是向上调整和向下调整.孰优孰劣,我们接下来便会具体分析
向上调整建堆(时间复杂度:N*logN):
从第一个孩子(下标为 1 的元素)开始遍历,依次向上调整,直到遍历完所有元素,即完成建堆.
向上调整代码示例:
//向上调整 void AdjustUp(int* a, size_t child) { size_t parent = (child - 1) / 2; while (child > 0) { //if (a[child] < a[parent]) //建立小堆 如果孩子小于父母 if (a[child] > a[parent]) //建立大堆 如果孩子大于父母 { Swap(&a[child], &a[parent]); //交换 a[child]和a[parent] 元素交换 child = parent; parent = (child - 1) / 2; } else { break; } } } //向上调整--建堆 for(int i =1;i
时间复杂度的计算:
向下调整建堆(时间复杂度:O(N)):
从最后一个非叶子节点(下标(n-1-1)/2)依次往前遍历,进行向下调整,直到遍历到第一个元素,即完成建堆
向下调整代码示例:
bigJustDown(int* a, int size, int root){ int prent = root; int child = prent * 2 + 1; //假定 左孩子 while (child
a[child]){ 判断左右孩子大小 child++; //右孩子比较大 } //判断父节点 和 孩子节点 大小 if (a[prent] < a[child]){ //孩子比较大, 父节点向下调整 int tmp = a[prent]; a[prent] = a[child]; a[child] = tmp; prent = child; child = prent * 2 + 1; } else{ //孩子比较小,调整完成 break; } } }//向下调整建立大堆 void HeapSort(int* a, int n){ int child = (n - 1 - 1) / 2; //选择第一个非叶子节点向下调整 //建立大堆 for (child; child >= 0; child--){ //依次往上选择节点,进行向下调整 bigJustDown(a, n, child); } /* ... 堆排序 ... */ } 时间复杂度的计算:
向下调整只需要O(N)次就可以完成,之所以比向上调整来得快,根本原因在于它从最后一个
非叶子节点开始,而在堆当中,每层的节点都以指数级增长,最后一层的节点最多可以是其他非叶子节点的总和还要多一个,eg:第4层节点数2^3而根据等比数列求和公式,前三层节点个数之和也就(2^3)-1个
由于该方法由 Floyd 提出,因此又称 Floyd 算法
ps:关于向上调整和向下调整,比较简单容易理解,这里就不上图片说明了,如果有问题,可以移
将堆顶元素与末尾元素进行交换,使末尾元素最大.然后继续调整堆(向下调整), 再将堆顶元素与末尾元素交换, 得到第二大元素.如此反复进行交换、重建、交换。
最终代码实现
//向下调整建立大堆 bigJustDown(int* a, int size, int root){ int prent = root; int child = prent * 2 + 1; while (child
a[child]){ child++; } if (a[prent] < a[child]){ int tmp = a[prent]; a[prent] = a[child]; a[child] = tmp; prent = child; child = prent * 2 + 1; } else{ break; } } } //堆排序的实现 升序 void HeapSort(int* a, int n){ //步骤一::建立大堆 int child = (n - 1 - 1) / 2; for (child; child >= 0; child--){ bigJustDown(a, n, child); } //建立大堆时间复杂度为 o(N) (前面已经计算过) //步骤二:: 利用堆的删除思想进行排序 int end = n - 1; while (end >=0){ //遍历N-1次 //交换,出最大元素 int tmp = a[0]; a[0] = a[end]; a[end] = tmp; //向下调整 bigJustDown(a, end--, 0); //每次向下调整复杂度为 o(log N) } } //总体时间复杂度: N+(N-1)*logN == o(N* logN) 堆排序的总体时间复杂度
步骤一: 建立大堆时间复杂度为 o(N) (前面已经计算过)
步骤二:利用堆的删除思想进行排序
遍历 N-1次
每次向下调整复杂度为 o(log N)
总体时间复杂度: o(N)+ (N-1)*logN == o(N* logN)
即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能 数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
这种方式的优势不仅仅在于它的时间复杂度低,同时当数据空间很大时,它的空间复杂度也很低,更适合决解实际问题
// TopK问题:找出N个数里面最大/最小的前K个问题。 // 比如:未央区排名前10的泡馍,西安交通大学王者荣耀排名前10的韩信,全国排名前10的李白。等等问题都是Topk问题, // 需要注意: // 找最大的前K个,建立K个数的小堆 // 找最小的前K个,建立K个数的大堆 void PrintTopK(int* a, int n, int k){ int* top = (int*)malloc(sizeof(int) * k); assert(top); int i = 0; for (i; i < k; i++){ top[i] = a[i]; } for (int child = (k - 1 - 1) / 2; child >= 0; child--){ AdjustDown(top, k, child); } for (i; i < n; i++){ if (a[i] > top[0]){ //和小堆比较最小元素比较,大就插入; top[0] = a[i]; AdjustDown(top, k, 0); } } for (; k >0;k--){ printf("%d ", top[0]); top[0] = top[k-1]; AdjustDown(top, k, 0); } free(top); } //测试函数 void TestTopk(){ int n = 10000; int* a = (int*)malloc(sizeof(int) * n); srand(time(0)); for (size_t 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[115] = 1000000 + 5; a[2305] = 1000000 + 6; a[99] = 1000000 + 7; a[76] = 1000000 + 8; a[423] = 1000000 + 9; a[0] = 1000000 + 1000; PrintTopK(a, n, 10); }