堆排序
2017-08-29 14:43
218 查看
堆排序在排序算法中算是比较晦涩难懂的一种,和快速排序、归并排序一样,平均时间复杂度为O(nlogn) ,要了解堆排序,必须点亮前置技能点—二叉堆&二叉树。
一种经过排序的完全二叉树
性质:
任意父节点的值都大于等于或小于等于子节点的值
每个节点的左右子堆也都是二叉堆
种类:
最大堆
也称大顶堆,即每个父节点的值都大于等于子节点的值,适用于从小到大排序.
最小堆
也称小顶堆,即每个父节点的值都小于等于子节点的值,适用于从大到小排序.
存储方式:
一般存储在一个一维数组之中,利用数组下标来进行父子节点的判断,例如:下标为1的两个儿子节点的下标分别为3和4,那么可得出下标为 i 的节点的两个儿子节点分别为 i∗2+1和 i∗2+2
添加一个数到堆如何更新?
删除一个数后堆如何更新?
初始化序列必然为无序的序列,怎么将其调整为一个二叉堆?
调整为二叉堆后,怎么有序的进行取数?
取完数之后的堆是否不变?怎么变?如何解决?
注意:以下问题解决解析皆以最小堆为例.
思想步骤:
将新数添加到数组的末尾
直接将新数插入末尾,此时可以发现,一条从根节点到新节点的路径出来了,而且这条路径的节点值还是递增的,我们要将新节点插入到这条路径上去,这样问题就转换成了插入排序的问题,如果有对插入排序有不理解的可以看一下这篇排序总结:常见的排序方法
根据插入排序的思想,很容易写出添加节点的代码,只是插入排序的时候每次下标是前进1,而在这里每次下标取当前节点的父节点,也就是 (i−1)/2
思想步骤:
当删除根节点之后,后面节点必然后前移,所以说我们可以暂时将最后一个节点值和根节点交换,然后缩短数组长度,即可实现删除根节点,但是此时二叉堆需要更新
首先检查原根节点的左右儿子,如果 min(左儿子,右儿子) 大于等于当前根节点,那么停止更新,二叉堆正确,如果小于当前根节点,那么交换 min(左儿子,右儿子) 和当前根节点,然后继续进行检查,直到找到替换前的根节点所应该处的正确位置.
方案一:将数一个一个的加入
初始化为一个空堆,然后一个一个加入到堆中
方案二:将数的后面一半初始化为一个队堆,然后将另一半加入堆并调整
因为堆构建到最后我们是构建出来了一个完全二叉树,所以说对所有的非叶子节点进行筛选,所以只需要加入一半然后调整即可
二叉堆
定义:一种经过排序的完全二叉树
性质:
任意父节点的值都大于等于或小于等于子节点的值
每个节点的左右子堆也都是二叉堆
种类:
最大堆
也称大顶堆,即每个父节点的值都大于等于子节点的值,适用于从小到大排序.
最小堆
也称小顶堆,即每个父节点的值都小于等于子节点的值,适用于从大到小排序.
存储方式:
一般存储在一个一维数组之中,利用数组下标来进行父子节点的判断,例如:下标为1的两个儿子节点的下标分别为3和4,那么可得出下标为 i 的节点的两个儿子节点分别为 i∗2+1和 i∗2+2
排序
在进行排序之前,我们先梳理一下思路,想一下我们可能会遇到的问题?添加一个数到堆如何更新?
删除一个数后堆如何更新?
初始化序列必然为无序的序列,怎么将其调整为一个二叉堆?
调整为二叉堆后,怎么有序的进行取数?
取完数之后的堆是否不变?怎么变?如何解决?
注意:以下问题解决解析皆以最小堆为例.
问题解决
添加一个数到堆
此操作在一个长度不会变化的数组中几乎不会使用,但是在处理长度动态变化,有可能添加新元素的数组时,此操作可能会用到.思想步骤:
将新数添加到数组的末尾
直接将新数插入末尾,此时可以发现,一条从根节点到新节点的路径出来了,而且这条路径的节点值还是递增的,我们要将新节点插入到这条路径上去,这样问题就转换成了插入排序的问题,如果有对插入排序有不理解的可以看一下这篇排序总结:常见的排序方法
根据插入排序的思想,很容易写出添加节点的代码,只是插入排序的时候每次下标是前进1,而在这里每次下标取当前节点的父节点,也就是 (i−1)/2
// 新加入结点,下标为i void insertHeapFix(int num[], int i) { int j, temp; temp = num[i]; j = (i - 1) / 2;//父结点 while (j >= 0 && i != 0) { if (num[j] <= temp){ break; } num[i] = num[j];//插入排序思想,把较大的子结点往下移动,替换它的子结点 i = j; j = (i - 1) / 2; } num[i] = temp; } void insertNumToHeap(int num[], int a, n){ //新节点置为最后 num = a; //调整堆 insertHeapFix(num, n); }
删除一个数
因为二叉堆是存储在数组中的,所以说删除操作比较麻烦,而且根据定义,二叉堆每次只能删除根节点.思想步骤:
当删除根节点之后,后面节点必然后前移,所以说我们可以暂时将最后一个节点值和根节点交换,然后缩短数组长度,即可实现删除根节点,但是此时二叉堆需要更新
首先检查原根节点的左右儿子,如果 min(左儿子,右儿子) 大于等于当前根节点,那么停止更新,二叉堆正确,如果小于当前根节点,那么交换 min(左儿子,右儿子) 和当前根节点,然后继续进行检查,直到找到替换前的根节点所应该处的正确位置.
void heapFix(int num[], int id, int top) { int temp = num[id]; int i = id;//当前遍历节点,从根节点开始 int j = i * 2 + 1;//子节点 while(j < top) { //寻找左右子节点的最小值 if(j + 1 < top && num[j + 1] < num[j]) { j++; } //找到了合适位置 if(num[j] >= temp) { break; } //更新指针 num[i] = num[j]; i = j; j = i * 2 + 1; } num[i] = temp; } void deleteNum(int num[], int n){ //将原根节点置于最后 swap(num[0], num[n - 1]); heapFix(num, 0, n - 1); }
初始化序列必然为无序的序列,怎么将其调整为一个二叉堆?
这个过程也就是常说的建堆过程,这里有两种方案:方案一:将数一个一个的加入
初始化为一个空堆,然后一个一个加入到堆中
方案二:将数的后面一半初始化为一个队堆,然后将另一半加入堆并调整
因为堆构建到最后我们是构建出来了一个完全二叉树,所以说对所有的非叶子节点进行筛选,所以只需要加入一半然后调整即可
for(int i = n / 2 - 1; i >= 0; i--){ heapFix(i, n); }
怎么有序的进行取数?
因为根节点的值永远是当前堆最小的,所以说每次我们将根节点存储并从堆中删除,然后重新调整堆,直到堆中只剩下一个节点,即 num[0],即:for(int i = n - 1; i >= 1; i--) { swap(num[i], num[0]); heapFix(num, 0, i); }
堆排序应用
求出 100w 的数据量数中最小的10个数
分析:这道题目在很多面试题中都有出现,虽然可能不是如此直接,但是思想都一样。这个时候,我们的关键点不在于 100w ,而在于那个 10,因为只需要找到 10 个数,所以说,我们维护一个大小为10的最大堆即可,堆顶元素为当前 10 个数中最大的数,如果新进来的数比堆顶元素小,那么删除堆顶元素,添加新元素.for(int i = 5; i < n; i++) { //新来的数较小 if(num[i] < num[0]) { //直接和根节点进行交换即可,因为后面的数我们都不需要维护. swap(num[i], num[0]); //调整堆 heapFix(0, 5); } }
相关文章推荐