【追求卓越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.mzph.cn/news/162126.shtml

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

相关文章

【20年扬大真题】编写程序,功能是计算1~10之间的偶数之和

【20年扬大真题】 编写程序&#xff0c;功能是计算1~10之间的偶数之和 #include<stdio.h> int main() {int i 1;int sum 0;for (i 1;i < 10;i){if (i % 2 0){sum i;}}printf("%d", sum); }

Java核心知识点整理大全9-笔记

目录 null文章浏览阅读9w次&#xff0c;点赞7次&#xff0c;收藏7次。Java核心知识点整理大全https://blog.csdn.net/lzy302810/article/details/132202699?spm1001.2014.3001.5501 Java核心知识点整理大全2-笔记_希斯奎的博客-CSDN博客 Java核心知识点整理大全3-笔记_希斯…

FindMy技术用于充电宝

充电宝是一种便捷的充电器&#xff0c;方便个人随身携带&#xff0c;能够自行储备电能&#xff0c;为主流电子设备提供充电服务。它广泛应用于没有外部电源供应的场所&#xff0c;例如旅行、户外活动或紧急情况下&#xff0c;为用户的手持设备提供持续的电力支持&#xff0c;确…

spring boot加mybatis puls实现,在新增/修改时,对某些字段进行处理,使用的@TableField()或者AOP @Before

1.先说场景&#xff0c;在对mysql数据库表数据插入或者更新时都得记录时间和用户id 传统实现有点繁琐&#xff0c;这里还可以封装一下公共方法。 2.解决方法&#xff1a; 2.1&#xff1a;使用aop切面编程&#xff08;记录一下&#xff0c;有时间再攻克&#xff09;。 2.1.1&am…

centos 安装k8s教程(一键安装k8s)

第一步 准备几台机器 第二步 K8s Manager 服务器中添加docker支持 安装教程请查看这个博客 docker 安装详细教程 点我 第三步安装 KuboardSpray 教程在这里 第四步 下载k8s资源包 第五步 安装k8s 点击安装后 显示如下&#xff1a;等待完成

arduino入门一:点亮第一个led

void setup() { pinMode(12, OUTPUT);//12引脚设置为输出模式 } void loop() { digitalWrite(12, HIGH);//设置12引脚为高电平 delay(1000);//延迟1000毫秒&#xff08;1秒&#xff09; digitalWrite(12, LOW);//设置12引脚为低电平 delay(1000); }

电脑桌面便签工具选择哪一款?

随着互联网时代的不断发展&#xff0c;电脑成为日常工作及办公中必不可少的工具&#xff0c;通过电脑这款工具&#xff0c;大家可以更好的进行工作、学习等方面的交流&#xff1b;电脑桌面便签由于可以为大家整合一些工作及学习方面的备忘事项及笔记等&#xff0c;因而深受大家…

【Vue】Node.js的下载安装与配置

目录 一.下载安装 官网&#xff1a; 二.环境变量的配置 三.设置全局路径和缓存路径 四.配置淘宝镜像 五.查看配置 六.使用npm安装cnpm ​ 一.下载安装 官网&#xff1a; https://nodejs.org/en/download 下载完之后&#xff0c;安装的时候一直点next即可&#xff0c…

FlinkCDC实现主数据与各业务系统数据的一致性(瀚高、TIDB)

文章末尾附有flinkcdc对应瀚高数据库flink-cdc-connector代码下载地址 1、业务需求 目前项目有主数据系统和N个业务系统,为保障“一数一源”,各业务系统表涉及到主数据系统的字段都需用主数据系统表中的字段进行实时覆盖,这里以某个业务系统的一张表举例说明:业务系统表Ta…

CSGO游戏搬砖市场下跌分析,是跑还是入?

CSGO市场下跌分析&#xff0c;是跑还是入&#xff1f; 以下所有都是阿阳本人最近几年观察市场和踩坑的一点经验&#xff0c;由于篇幅不长所以肯定会很浅薄&#xff0c;大伙下嘴轻点 。 首先现在真的是CSGO市场最低点吗&#xff1f;后续还会跌吗&#xff1f;我们究竟是该继续观…

Course1-Week1:机器学习简介

Course1-Week1&#xff1a;机器学习简介 文章目录 Course1-Week1&#xff1a;机器学习简介1. 课程简介1.1 课程大纲1.2 Optional Lab的使用 (Jupyter Notebooks)1.3 欢迎参加《机器学习》课程 2. 机器学习简介2.1 机器学习定义2.2 有监督学习2.3 无监督学习 3. 线性回归模型3.1…

python二叉树链树_树的链式存储结构

二叉链树是一种树状数据结构&#xff0c;其中每个节点最多有两个子节点&#xff0c;分别称为左子节点和右子节点。每个节点包含一个数据元素和指向其左右子节点的指针。二叉链树可以是空树&#xff0c;也可以是具有以下特点的非空树&#xff1a; 1. 每个节点最多有两个子节点。…

netstat

netstat 命令用于显示网络状态 参数说明&#xff1a; -a或--all 显示所有连线中的Socket&#xff0c;默认不显示LISTEN相关 -n 拒绝显示别名&#xff0c;能显示数字的全部转化成数字 -e或--extend 显示网络扩展信息(User&#xff0c;Inode) -p或--programs 显示正在使用So…

JavaEE 多线程01

为什么引入多线程? 首先进程已经能很好的完成多任务这个情景下的并发编程了,那为什么又引入多线程呢? 这是因为在一些情景下,我么需要大量的创建和销毁进程来完成一些任务,此时多进程对系统的开销就会很大了. 假设有这样一个场景,服务器同时接收到很多个服务请求,这个时候服务…

Python基础教程: sorted 函数

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 sorted 可以对所有可迭代的对象进行排序操作&#xff0c; sorted 方法返回的是一个新的 list&#xff0c;而不是在原来的基础上进行的操作。 从新排序列表。 &#x1f447; &#x1f447; &#x1f447; 更多精彩机密、教程…

taro h5 ios解决input不能自动获取焦点拉起键盘

描述&#xff1a;页面中有个按钮&#xff0c;点击跳转到第二个页面&#xff08;有input&#xff09;&#xff0c;能直接获取焦点拉起键盘输入 安卓&#xff1a; 直接用focus() ios&#xff1a; focus无效&#xff0c;必须手动拉起 原理&#xff1a; 点击按钮的时候拉起一…

一元三次方程求解——浮点数二分

题目描述 思路 根与根之差的绝对值>1。可以得出距离为1的区间最多只有一个根若存在2个数x1和x2&#xff0c;且x1 < x2&#xff0c;f(x1) x f(x2) < 0&#xff0c;则(x1, x2)之间一定有一个根我们可以遍历每一个区间为1的范围&#xff0c;先判断左端点是否是根&#x…

Python---global关键字---设置全局变量

global 英 /ˈɡləʊb(ə)l/ adj. 全球的&#xff0c;全世界的&#xff1b;全面的&#xff0c;整体的&#xff1b;&#xff08;计算机&#xff09;全局的&#xff1b;球形的 需求&#xff1a;如果有一个数据&#xff0c;在函数A和函数B中都要使用&#xff0c;该怎么办&…

【PyGIS】使用阿里AIEarth快速下载指定区域指定年份的土地利用数据

说明 中国逐年土地覆盖数据集(CLCD) 由武汉大学的杨杰和黄昕教授团队基于Landsat影像制作了中国逐年土地覆盖数据集(annual China Land Cover Dataset, CLCD),数据包含1985—2021年中国逐年土地覆盖信息。研究团队基于Landsat长时序卫星观测数据,构建时空特征,结合随机森…

Linux常用命令——blockdev命令

在线Linux命令查询工具 blockdev 从命令行调用区块设备控制程序 补充说明 blockdev命令在命令调用“ioxtls”函数&#xff0c;以实现对设备的控制。 语法 blockdev(选项)(参数)选项 -V&#xff1a;打印版本号并退出&#xff1b; -q&#xff1a;安静模式&#xff1b; -v&…