堆排序

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

二叉堆

定义:

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

性质:

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

种类:

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

存储方式:

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

排序

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

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

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

问题解决

添加一个数到堆

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

思想步骤:

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值