【追求卓越12】算法--堆排序

引导

        前面几节,我们介绍了有关树的数据结构,我们继续来介绍一种树结构——堆。堆的应用场景有很多,比如从大量数据中找出top n的数据;根据优先级处理网络请求;这些情景都可以使用堆数据结构来实现。

什么是堆?

正如上文所说,堆是一种树结构。它的定义满足以下两点:

  1. 堆是一个完全二叉树
  2. 堆中每个节点的值必须大于等于(或小于等于)其子树中每个节点的值

完全二叉树在前面我们已经讲过;第二点,其实等价于堆中每个节点大于等于(或小于等于)其左右子节点。

对于每个节点大于等于左右子节点,我们称为大顶堆;每个节点小于等于左右子节点,我们称为小顶堆。

堆的操作

堆这种数据结构有点不一样。不适合查找或删除指定值的操作。

但是它可以帮助我们解决一些特定的问题。但是不同的问题,都离不开以下核心操作:插入一个元素,删除堆顶元素。

创建一个堆

我们知道完全二叉树适合用数组进行保存。因为它不需要保存左右指针,通过数组下标就可以实现。

数组下表为i的节点,其左子节点的数组下标为2*i,右子节点的数组下标为 2 *i+1;任意节点的父节点下标为i/2;根节点的下标为1.

插入一个元素

        向堆中插入一个数据,我们可以将新加入的数据加入到堆的后面,使其继续维持一个完全二叉树。

        但是插入一个新的数据就无法保证其父节点大于等于(或小于等于)左右子节点了。于是我们还要进行调整,使其满足堆的特性。我们称为这个过程为堆化。堆化的过程,其实也简单,只要循着插入节点,向上对比,进行交换即可。

/
*
大顶堆*
/
#define MAXLEN 1024
int heap[MAXLEN+1] = 0;
int count = 0;
int insert(int data)
{
    if(count >= MAXLEN)
        return -1; /
*堆满了*
/

    count++;

    heap[count] = data;
    int i = count;
    int temp = 0;
    while(i/2 > 0 && heap[i] > heap[i/2])
    {
        temp = heap[i/2];
        heap[i/2] = heap[i];
        heap[i] = temp;

        i = i/2;
    }
}

删除堆顶元素

由堆的特性所致,删除堆顶元素,实际上就是找出最大或者最小值的过程。删除堆顶元素其实也简单,可按照以下思路进行:

  1. 将最后一个元素放到堆顶,保证树是一个完全二叉树
  2. 从堆顶开始进行堆化,使其满足特质二

/
*
大顶堆*
/
#define MAXLEN 1024
int heap[MAXLEN+1] = 0;
int count = 0;
int  removeTop()
{
    if(count == 0) return -1;
    heap[1] = a[count];
    count--;

    heapify(count,1);
    return 0;
}
/
*从某个节点开始,向下堆化*
/
int heapify(int count, int start)
{
    while(true)
    {
        int max = start;
        if(i
*2 < count && heap[i*
2]>heap[max])
            max = i
2;
*        if(i*
2+1 < count && heap[i
2+1]>heap[max])
*            max = i*
2+1;   

        if(max == start)
            break;
        swap(heap,max,start);/
*交换max下标和start下标的值*
/
        start = max;
    }
}

        通过插入一个数据和删除堆顶元素的操作流程,我们知道时间的消耗主要集中在堆化中。由于完全二叉树的树高不会超过logn。所以堆的插入数据和删除堆顶元素的时间复杂度都是O(logn)。

如何实现堆排序?

        我们已经知道堆的性质以及基本的操作。但是对于一组数据我们该如何进行堆排序呢?原始的数据肯定是无序也不是堆结构的。因此我们第一步应该是建立堆。

建堆

建堆的方式有两种

第一种最简单:首先假设堆中的数据为0,依次将原始数据放到堆中。我们知道插入数据的时间复杂度是O(logn)。故插入第一个数据的消耗的时间为log1,第二个数据消耗的时间为log2...以此推测,第n个数据消耗时间为logn。

方法一消耗的时间为:log1+log2+log3...+logn=O(logn!),并且需要额外的内存。

第二种思路就比较特殊:对于完全二叉树而言,下标从n/2 + 1到n的节点都是叶子节点,并且叶子节点是不需要堆化的。

因此我们只需要将原始数据中下标从1到n/2进行堆化即可,并且不需要额外的内存。

int buildheap(int heap[],int count)
{
    int i = n/2;
    for( ; i >= 1 ; i--)
        heapify(heap,count,i);
}

那么第二种思路的时间复杂度是多少呢?

我们知道堆化的操作和所在节点的高度有关。由于叶子节点不需要堆化,并且每层节点和节点高度成正比,如图:

故消耗的时间为:

T = 2^0
*h + 2^1*
(h-1) + 2^2*(h-2) +...+ 2^(h-1)*1
T = -h +2 + 2^2 + 2^3 + ... + 2^h
T=2^(h+1) -h - 2

由于h=logn(以2为底)通过化简,得出时间复杂度为O(2n-h-2)=O(n)。

至此,我们已将完成建堆

排序

在删除堆顶元素时,我就说过了,这就是排序的过程。因此堆排序,就是将堆顶元素不断删除,直至堆为空。

代码如下:

int heapsort()
{
    buildheap(heap,count);
    int k = count;
    while(k > 1)
    {
        printf("%d\r",heap[1]);
        swap(heap,1,count);
        k--;
        heapify(a,k,1);
    }
    printf("\n");
}

现在我们看堆排序的时间复杂度是多少?

        建堆的时间复杂度是O(n),排序的时间复杂度是O(nlogn)(实际上应该是logn+log(n-1)+log(n-2)+...+log(1),这里方便,我们记录为O(logn))。故堆排序的时间复杂度为O(nlogn)。

快速排序的性能为什么比堆排序的要好?

在排序章节,我们提到了快速排序,它是时间复杂度也是O(nlogn)。但是在实际开发过程中,我们快速排序的性能优于堆排序。我觉得原因有以下几点:

  1. 堆排序的访问没有快速排序好。即使堆排序也是以数组为存储。但是由于它访问不是连续的,不能很好的利用CPU缓存。
  2. 对于同样的数据,堆排序的数据交换要多于快速排序。

堆应用

优先级队列

        在工作中,我们经常会遇到定时任务的问题。一般思路:将每个任务保存到数组中,每过一个时间间隔(1秒),就检测一下数组,看哪个任务达到了设定时间,如果到达了就取出任务执行,并删除

其实这样的定时器效率是很低的,为什么呢?

  1. 往往到达下一个任务之间的间隔是需要很长时间,那么在达到之前所有的检测都是无用的,这是在浪费系统资源
  2. 如果任务列表的长度很大,那么每次遍历,都会消耗很多的资源。这也是不可取的。

针对该类问题,我们可以采用优先级队列来解决。我们按照任务时间进行堆化。堆顶就是最先要被执行的任务。

修改方案:

  1. 根据任务时间进行堆化(小堆顶),堆顶任务就是下一个将要被执行的任务
  2. 计算现在距离下一个任务的时间T,sleep T秒,再执行堆顶任务。这样在执行下一个任务之前,定时器不需要做任何事情,性能大大提高。

Top k

堆在求top k的问题中,表现的也同样出色。

求top K的问题,我们可以先将数据排序,再取前K个元素。我们可以通过快排,那么求top K的时间复杂度是O(nlogn),这同样也很快,难道堆会更好吗?

堆应用的思路:

  1. 取数据中的前K个元素,建立小顶堆。
  2. 继续遍历数组中的元素,如果元素大于堆顶元素,则加入堆中,并删除堆顶元素。若比堆顶元素小,则不进行处理。
  3. 当遍历完整个数组后,堆中的数据就是前top K数据。

假设每个元素都需要进行插入,那么就是堆化n个元素,堆化的时间复杂度是O(logK),故使用过堆求top K的问题,其时间复杂度是O(nlogk)。

求中位数

在求中位数的问题中,我们一般的思路是将原始数据进行排序。

再求中位数,如果n为奇数的话,中位数就是n/2+1;如果n为偶数的话,中位数就是n/2或n/2+1。这种方式的消耗主要集中在对n个数据排序上面,使用快速排序,它的时间复杂度就是O(nlogn)。

但这样的方式比较适合静态数据,当数据能够动态添加时,再采取这样的方式,就不再适合了。

堆对于这种动态数据,求中位数有其优势。我们的思路是这样的:

  1. 先将现有数据进行升序排序,取前n/2个数据进行大堆顶堆化。后n/2个数据进行小堆顶堆化。(假设n为偶数。若n为奇数,取前n/2+1个数据为大堆顶)
  2. 当n为偶数时,中位数就是大堆顶和小堆顶的堆顶元素;当n为奇数时,中位数就是大堆顶的堆顶元素;
  3. 插入一个元素时,如果插入元素小于等于大堆顶元素,我们就将元素插入到大堆顶;反之插入到小堆顶。这样就容易不满足我们的预定:取前n/2个数据进行大堆顶堆化。后n/2个数据进行小堆顶堆化。(假设n为偶数。若n为奇数,取前n/2+1个数据为大堆顶)。我们就需要将两个堆的堆顶元素互相调整,使其满足上诉约定

        通过上述的逻辑,我们就可以得到一个适合动态数据,求中位数的方案。其时间复杂度主要集中在堆化过程O(logn)。

总结

        本节,我们介绍了堆这种数据结构以及堆结构的定义。也从代码的角度去实现了堆和堆排序。

        最后也比较了快速排序相对于堆排序的优点。我后面会继续总结一个关于堆排序的应用方向的一系列题。争取吃透。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/208914.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二、类与对象(二)

8 this指针 8.1 this指针的引入 我们先来定义一个日期的类Date&#xff1a; #include <iostream> using namespace std; class Date { public:void Init(int year, int month, int day){_year year;_month month;_day day;}void Print(){cout << _year <&l…

golang学习笔记——罗马数字转换器

文章目录 罗马数字转换器代码 参考LeetCode 罗马数字转整数代码 罗马数字转换器 编写一个程序来转换罗马数字&#xff08;例如将 MCLX 转换成 1,160&#xff09;。 使用映射加载要用于将字符串字符转换为数字的基本罗马数字。 例如&#xff0c;M 将是映射中的键&#xff0c;其值…

范围查询 range级别 继续优化思路

问题&#xff1a; 这几天工作遇到了一个问题。千万级别的表&#xff0c;每秒钟产生很多数据&#xff0c;select count(id) from table where flag 1 and create_time < 2023.11.07;分区表&#xff0c;range级别&#xff0c;已经是走create_time列上的索引&#xff0c;flag…

基于SpringBoot+Redis的前后端分离外卖项目-苍穹外卖(七)

分页查询、删除和修改菜品 1. 菜品分页查询1.1 需求分析和设计1.1.1 产品原型1.1.2 接口设计 1.2 代码开发1.2.1 设计DTO类1.2.2 设计VO类1.2.3 Controller层1.2.4 Service层接口1.2.5 Service层实现类1.2.6 Mapper层 1.3 功能测试1.3.2 前后端联调测试 2. 删除菜品2.1 需求分析…

vue3项目中使用富文本编辑器

前言 适配 Vue3 的富文本插件不多&#xff0c;我看了很多插件官网&#xff0c;也有很多写的非常棒的&#xff0c;有UI非常优雅让人耳目一新的&#xff0c;也有功能非常全面的。 如&#xff1a; Quill&#xff0c;简单易用&#xff0c;功能全面。editorjs&#xff0c;UI极其优…

风电场叶片运输车模型-FBX格式-带动画-数字孪生场景搭建

FBX格式的风电场中叶片运输车辆模型&#xff0c;按照真实尺寸建模&#xff0c;车辆多个部位带动画效果&#xff0c;适用于风电场三维数字化场景和风电场数字孪生使用&#xff0c;也可以用来作为各种三维平台的测试模型。 模型效果图 下载地址 叶片运输车模型下载地址

Java-接口

接口 接口 接口就是公共的行为规范,只要实现时符合标准就可以通用. 接口可以看成是: 多个类的公共规范,是一种引用数据类型. 使用关键字interface实现接口. 接口是不能被实例化的. 接口中的成员变量默认是 public static final 接口中只能有抽象方法,当中的方法不写,也是pu…

visionOS空间计算实战开发教程Day 4 初识ImmersiveSpace

细心的读者会发现在在​​Day1​​​和​​Day2​​​的示例中我们使用的都是​​WindowGroup​​。 main struct visionOSDemoApp: App {var body: some Scene {WindowGroup {ContentView()}} } 本节我们来认识在visionOS开发中会经常用到的另一个概念​​ImmersiveSpace​​…

react中模块化样式中:global的作用

在react中如果是通过import styles from ./index.less这种方式模块化引入样式的话&#xff0c;那么编译后的less文件里的样式名都会自动添加后缀。而:global的作用就是不让类名添加后缀

2023年03月 Scratch(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Scratch等级考试(1~4级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 小猫的程序如图所示,积木块的颜色与球的颜色一致。点击绿旗执行程序后,下列说法正确的是?( ) A:小猫一直在左右移动,嘴里一直说着“抓到了”。 B:小猫会碰到球,然后停止。…

《QT从基础到进阶·三十八》QWidget实现炫酷log日志打印界面

QWidget实现了log日志的打印功能&#xff0c;不仅可以在界面显示&#xff0c;还可以生成打印日志。先来看下效果&#xff0c;源码放在文章末尾&#xff1a; LogPlugin插件类管理log所有功能&#xff0c;它可以获取Log界面并能打印正常信息&#xff0c;警告信息和错误信息&…

加入破局 180 天,成功立起 IP

大家好&#xff0c;我是破局合伙人木川 在成为破局合伙人之前&#xff0c;我就是那个两三年不怎么发朋友圈的人&#xff0c;成为破局合伙人之后&#xff0c;开始日更朋友圈 在成为破局合伙人之前&#xff0c;我就是那个喜欢单打独斗的人&#xff0c;成为破局合伙人之后&#xf…