您的位置:首页 > 其它

堆排序

2017-08-29 14:43 218 查看
堆排序在排序算法中算是比较晦涩难懂的一种,和快速排序、归并排序一样,平均时间复杂度为O(nlogn) ,要了解堆排序,必须点亮前置技能点—二叉堆&二叉树。

二叉堆

定义:

一种经过排序的完全二叉树

性质:

任意父节点的值都大于等于或小于等于子节点的值

每个节点的左右子堆也都是二叉堆

种类:

最大堆

也称大顶堆,即每个父节点的值都大于等于子节点的值,适用于从小到大排序.



最小堆

也称小顶堆,即每个父节点的值都小于等于子节点的值,适用于从大到小排序.



存储方式:

一般存储在一个一维数组之中,利用数组下标来进行父子节点的判断,例如:下标为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);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: