堆排序

来源:互联网 发布:js获取传入的参数 编辑:程序博客网 时间:2024/06/03 20:29

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

二叉堆

定义:

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

性质:

  • 任意父节点的值都大于等于或小于等于子节点的值
  • 每个节点的左右子堆也都是二叉堆

种类:

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

存储方式:

  • 一般存储在一个一维数组之中,利用数组下标来进行父子节点的判断,例如:下标为1的两个儿子节点的下标分别为3和4,那么可得出下标为 i 的节点的两个儿子节点分别为 i2+1i2+2

排序

在进行排序之前,我们先梳理一下思路,想一下我们可能会遇到的问题?

  • 添加一个数到堆如何更新?
  • 删除一个数后堆如何更新?
  • 初始化序列必然为无序的序列,怎么将其调整为一个二叉堆?
  • 调整为二叉堆后,怎么有序的进行取数?
  • 取完数之后的堆是否不变?怎么变?如何解决?

注意:以下问题解决解析皆以最小堆为例.

问题解决

添加一个数到堆

此操作在一个长度不会变化的数组中几乎不会使用,但是在处理长度动态变化,有可能添加新元素的数组时,此操作可能会用到.

思想步骤:

  • 将新数添加到数组的末尾
    直接将新数插入末尾,此时可以发现,一条从根节点到新节点的路径出来了,而且这条路径的节点值还是递增的,我们要将新节点插入到这条路径上去,这样问题就转换成了插入排序的问题,如果有对插入排序有不理解的可以看一下这篇排序总结:常见的排序方法
  • 根据插入排序的思想,很容易写出添加节点的代码,只是插入排序的时候每次下标是前进1,而在这里每次下标取当前节点的父节点,也就是 (i1)/2
//  新加入结点,下标为ivoid 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[n] = 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);    }}
原创粉丝点击