堆排序时间复杂度_图解堆结构、堆排序及堆的应用

前言

这次我们介绍另一种时间复杂度为 O(nlogn) 的选择类排序方法叫做堆排序。

我将从以下几个方面介绍:

  • 堆的结构
  • 堆排序
  • 优化的堆排序
  • 原地堆排序
  • 堆的应用

堆的结构

什么是堆?我给出了百度的定义,如下:

堆(Heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵 完全二叉树 的数组对象。

堆总是满足下列性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值。
  • 堆总是一棵完全二叉树。

将根节点最大的堆叫做最大堆,根节点最小的堆叫做最小堆。

下图展示了一个最大堆的结构:

793b8797e183b4e97563ca88a66ff834.png

可见,堆中某个节点的值总是小于等于其父节点的值。

由于堆是一棵完全二叉树,因此我们可以对每一层进行编号,如下:

f67fd43a1707b9f9da88c9ff8a1637f8.png

我们完全可以使用数组存放这些元素,那如何确定存放的位置呢?利用如下公式:

父节点:parent(i) = (i-1)/2

左孩子:leftChild(i) = 2*i+1

右孩子:rightChild(i) = 2*i+2

相关代码如下:

private int parent(int index) {    return (index - 1) / 2;}private int leftChild(int index) {    return index * 2 + 1;}private int rightChild(int index) {    return index * 2 + 2;}

添加元素

向堆中添加元素的步骤如下:

  1. 将新元素放到数组的末尾。
  2. 获取新元素的父亲节点在数组中的位置,比较新元素和父亲节点的值,如果父亲节点的值小于新元素的值,那么两者交换。以此类推,不断向上比较,直到根节点结束。

下图展示了添加元素的过程:

83467ada2791fe5cec60093e52890f20.png

添加元素的过程也叫做 siftUp ,代码如下:

// Array是自己实现的动态数组private Array data;public void add(E e) {    data.addLast(e);    siftUp(data.getSize() - 1);}private void siftUp(int k) {    while (k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0) {        data.swap(k, parent(k));        k = parent(k);    }}

删除元素

删除元素其实就是删除堆顶的元素,步骤如下:

  1. 让数组最后一个元素和数组第一个元素(堆顶元素)交换。
  2. 交换完后,删除数组最后的元素。
  3. 让堆顶元素和左右孩子节点比较,如果堆顶元素比左右孩子节点中最大的元素还要大,那么满足堆的性质,直接退出。否则如果堆顶元素比左右孩子节点中最大的元素小,那么堆顶元素就和最大的元素交换,然后继续重复执行以上操作,只不过这时候把堆顶元素称为父节点更好。

下图展示了删除元素的过程:

b993f0db5ed5c83cbe786081e03e371b.png

删除元素的过程也叫做 siftDown ,代码如下:

// 这里我们不命名为remove,命名为extractMax,抽取堆顶最大元素public E extractMax() {    E ret = findMax();    // 让最后一个叶子节点补到根节点,然后让它下沉    // (为什么是取最后一个叶子节点,因为即使取走最后一个叶子节点,依旧能保持是一棵完全二叉树)    data.swap(0, data.getSize() - 1);    data.removeLast();    siftDown(0);    return ret;}private void siftDown(int k) {    while (leftChild(k) < data.getSize()) {        int j = leftChild(k);        if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0) {            j = rightChild(k);            // data[j]是leftChild和rightChild中的最大值        }        // 如果父节点比左右孩子中的最大值还要大,那么说明没有问题,直接退出        if (data.get(k).compareTo(data.get(j)) >= 0) {            break;        }        // 否则交换        data.swap(k, j);        k = j;    }}

最大堆的完整代码

堆排序

通过上面的介绍,我们应该明白了堆的结构,堆的添加和删除元素操作是如何完成的。那么对于堆排序来说,就是小菜一碟了,因为堆排序就是用到了堆的添加和删除操作,步骤如下:

  1. 将数组中元素一个个添加到堆(最大堆)中。
  2. 添加完成后,每次取出一个元素倒序放入到数组中。

堆排序代码:

public static void sort(Comparable[] arr) {    int n = arr.length;    // MaxHeap是自己实现的最大堆    MaxHeap maxHeap = new MaxHeap<>(n);    for (int i = 0; i < n; i++) {        maxHeap.add(arr[i]);    }    for (int i = n - 1; i >= 0; i--) {        arr[i] = maxHeap.extractMax();    }}

堆排序完整代码

优化的堆排序

在上述的堆排序中,我们在将数组中元素添加到堆时,都是一个个添加,是否有优化的方法呢?答案是有的,我们可以将数组直接转换成堆,这种操作叫做 Heapify 。

Heapify 就是从最后一个节点开始,判断父节点是否比孩子节点大,不是就 siftDown 。 Heapify 操作的时间复杂度是 O(n) ,相比一个个添加的时间复杂度是 O(nlogn) ,可见性能提升了不少。

假设我们有数组: [15, 18, 12, 16, 22, 28, 16, 45, 30, 52] ,下图展示了对其进行 Heapify 的过程。

a170a8d7976f8884b82fb0a707d2f1d5.png

优化的堆排序代码:

public static void sort(Comparable[] arr) {    int n = arr.length;    // MaxHeap是自己实现的最大堆,当传入数组作为构造参数时,会对其进行heapify    MaxHeap maxHeap = new MaxHeap<>(arr);    for (int i = n - 1; i >= 0; i--) {        arr[i] = maxHeap.extractMax();    }}// 构造方法public MaxHeap(E[] arr) {    data = new Array<>(arr);    // 将数组堆化的过程就是从最后一个节点开始,判断父节点是否比子节点大,不是就siftDown    for (int i = parent(arr.length - 1); i >= 0; i--) {        siftDown(i);    }}

优化的堆排序完整代码

原地堆排序

原地堆排序可以让我们的空间复杂度变为 O(1) ,因为不占用新的数组。

原地堆排序类似于堆的删除元素,步骤如下:

HeapifysiftDownsiftDown

下图展示了原地堆排序的过程:

6e991258bf5bf7838477b4ac0e00614a.png

原地堆排序代码:

public static void sort(Comparable[] arr) {    int n = arr.length;    // heapify    for (int i = parent(n-1); i >= 0; i--) {        siftDown(arr, n, i);    }    // 核心代码    for (int i = n - 1; i > 0; i--) {        swap(arr, 0, i);        siftDown(arr, i, 0);    }}private static void swap(Object[] arr, int i, int j) {    Object t = arr[i];    arr[i] = arr[j];    arr[j] = t;}private static void siftDown(Comparable[] arr, int n, int k) {    while (leftChild(k) < n) {        int j = leftChild(k);        if (j + 1 < n && arr[j + 1].compareTo(arr[j]) > 0) {            j = rightChild(k);        }        // 如果父节点比左右孩子中的最大值还要大,那么说明没有问题,直接退出        if (arr[k].compareTo(arr[j]) >= 0) {            break;        }        // 否则交换        swap(arr, k, j);        k = j;    }}

原地堆排序完整代码

堆的应用

优先级队列

一旦我们掌握了堆这个数据结构,那么优先级队列的实现就很简单了,只需要弄清楚优先级队列需要有哪些接口就行。JDK 中自带的 PriorityQueue 就是用堆实现的优先级队列,不过需要注意 PriorityQueue 内部使用的是最小堆。

优先级队列完整代码

Top K 问题

Top K 问题就是求解 前 K 个 最大的元素或者最小的元素。元素个数不确定,数据量可能很大,甚至源源不断到来,但需要知道目前为止前 K 个最大或最小的元素。当然问题还可能变为求解 第 K 个 最大的元素或最小的元素。

通常我们有如下解决方案:

  1. 使用JDK中自带的排序,如 Arrays.sort() ,由于底层使用的快速排序,所以时间复杂度为 O(nlogn) 。但是如果 K 取值很小,比如是 1,即取最大值,那么对所有元素排序就没有必要了。
  2. 使用简单选择排序,选择 K 次,那么时间复杂度为 O(n*K) ,如果 K 大于 logn,那还不如快排呢!

上述两种思路都是假定所有元素已知,如果元素个数不确定,且数据源源不断到来的话,就无能为力了。

下面提供一种新的思路:

我们维护一个长度为 K 的数组,最前面 K 个元素就是目前最大的 K 个元素,以后每来一个新元素,都先找数组中的最小值,将新元素与最小值相比,如果小于最小值,则什么都不变,如果大于最小值,则将最小值替换为新元素。这样一来,数组中维护的永远是最大的 K 个元素,不管数据源有多少,需要的内存开销都是固定的,就是长度为 K 的数组。不过,每来一个元素,都需要找到最小值,进行 K 次比较,是否有办法能减少比较次数呢?

当然,这时候堆就要登场了,我们使用最小堆维护这 K 个元素,每次来新的元素,只需要和根节点比较,小于等于根节点,不需要变化,否则用新元素替换根节点,然后 siftDown 调整堆即可。此时的时间复杂度为 O(nlogK) ,相比上述两种方法,效率大大提升,且空间复杂度也大大降低。

Top K 问题代码:

public class TopK> {    private PriorityQueue p;    private int k;    public TopK(int k) {        this.k = k;        this.p = new PriorityQueue<>(k);    }    public void addAll(Collection extends E> c) {        for (E e : c) {            add(e);        }    }    public void add(E e) {        // 未满k个时,直接添加        if (p.size() < k) {            p.add(e);            return;        }        E head = p.peek();        if (head != null && head.compareTo(e) >= 0) {            // 小于等于TopK中的最小值,不用变            return;        }        // 否则,新元素替换原来的最小值        p.poll();        p.add(e);    }    /**     * 获取当前的最大的K个元素     *     * @param a   返回类型的空数组     * @param      * @return TopK以数组形式     */    public E[] toArray(E[] a) {        return p.toArray(a);    }    /**     * 获取第K个最大的元素     *     * @return 第K个最大的元素     */    public E getKth() {        return p.peek();    }    public static void main(String[] args) {        TopK top5 = new TopK<>(5);        top5.addAll(Arrays.asList(88, 1, 5, 7, 28, 12, 3, 22, 20, 70));        System.out.println("top5:" + Arrays.toString(top5.toArray(new Integer[0])));        System.out.println("5th:" + top5.getKth());    }}

这里我们直接利用 JDK 自带的由最小堆实现的优先级队列 PriorityQueue 。

依此思路,可以实现求前 K 个最小元素,只需要在实例化 PriorityQueue 时传入一个反向比较器参数,然后更改 add 方法的逻辑。

中位数

堆也可以用于求解中位数,数据量可能很大且源源不断到来。

注意:如果元素个数是偶数,那么我们假定中位数取任意一个都可以。

有了上面的例子,这里就很好理解了。我们使用两个堆,一个最大堆,一个最小堆,步骤如下:

  1. 添加的第一个元素作为中位数 m,最大堆维护 <= m 的元素,最小堆维护 >= m 的元素,两个堆都不包含 m。
  2. 当添加第二个元素 e 时,将 e 与 m 比较,若 e <= m,则将其加入到最大堆中,否则加入到最小堆中。
  3. 如果出现最小堆和最大堆的元素个数相差 >= 2,则将 m 加入元素个数少的堆中,然后让元素个数多的堆将根节点移除并赋值给 m。
  4. 以此类推不断更新。

假设有数组 [20, 30, 40, 50, 2, 4, 3, 5, 7, 8, 10] 。

下图展示了整个操作的过程:

bd30b451dd8b097dbe19091b236ee1ef.png

求解中位数的代码:

public class Median> {    /**     * 最小堆     */    private PriorityQueue minP;    /**     * 最大堆     */    private PriorityQueue maxP;    /**     * 当前中位数     */    private E m;    public Median() {        this.minP = new PriorityQueue<>();        this.maxP = new PriorityQueue<>(11, Collections.reverseOrder());    }    private int compare(E e, E m) {        return e.compareTo(m);    }    public void addAll(Collection extends E> c) {        for (E e : c) {            add(e);        }    }    public void add(E e) {        // 第一个元素        if (m == null) {            m = e;            return;        }        if (compare(e, m) <= 0) {            // 小于等于中值,加入最大堆            maxP.add(e);        } else {            // 大于中值,加入最大堆            minP.add(e);        }        if (minP.size() - maxP.size() >= 2) {            // 最小堆元素个数多,即大于中值的数多            // 将 m 加入到最大堆中,然后将最小堆中的根移除赋给 m            maxP.add(m);            m = minP.poll();        } else if (maxP.size() - minP.size() >= 2) {            minP.add(m);            m = maxP.poll();        }    }    public E getMedian() {        return m;    }    public static void main(String[] args) {        Median median = new Median<>();        median.addAll(Arrays.asList(20, 30, 40, 50, 2, 4, 3, 5, 7, 8, 10));        System.out.println(median.getMedian());    }}

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

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

相关文章

恶意软件分析沙箱在网络安全策略中处于什么位置?

恶意软件分析沙箱提供了一种全面的恶意软件分析方法&#xff0c;包括静态和动态技术。这种全面的评估可以更全面地了解恶意软件的功能和潜在影响。然而&#xff0c;许多组织在确定在其安全基础设施中实施沙箱的最有效方法方面面临挑战。让我们看一下可以有效利用沙盒解决方案的…

php websocket 帧封装,swoole websocket封装类和调用

上代码 ws.php/*** ws 优化 基础类库* User: singwa* Date: 18/3/2* Time: 上午12:34*/class Ws {CONST HOST "0.0.0.0";CONST PORT 9512;public $ws null;public function __construct() {$this->ws new swoole_websocket_server("0.0.0.0", 9512)…

夸克浏览器怎么安装脚本_广告看烦了?别砸手机!这五款浏览器能拯救你

哈喽大家好&#xff0c;欢迎来到黑马公社。随着各种良莠不齐的内容开始泛滥&#xff0c;黑马发现自己很难通过网络第一时间找到自己想要的内容。在电脑上&#xff0c;黑马为自己的每个浏览器都安装了不下三个广告屏蔽插件&#xff0c;而在手机上&#xff0c;很难。先不说手机浏…

php 今天 明天 后天 显示10天,【微信小程序】实现含有今天,明天,后天的日期组件...

封面图.JPG前言做过微信小程序的前端er都知道&#xff0c;小程序有个日期组件&#xff0c;叫picker&#xff0c;但是&#xff0c;需求方要求日期和时间都要显示的&#xff0c;用picker组件的话&#xff0c;那就用到两个picker&#xff0c;date和time&#xff0c;就是说要让用户…

php数组实例,php常用数组函数实例小结

本文实例总结了php常用数组函数。分享给大家供大家参考&#xff0c;具体如下&#xff1a;1. array array_merge(array $array1 [, array $array2 [, $array]])函数功能&#xff1a;将一个或多个数组的单元合并起来&#xff0c;一个数组中的值附加在前一个数组的后面。返回结果的…

手机连接投影机的步骤_投影机安装过程详解

投影机安装过程详解一 投影机的安装方式1、桌面摆放桌面投影虽然看起来不是很美观&#xff0c;但可以省去那些繁琐的步骤&#xff0c;只需要准备一张桌子&#xff0c;还可以购买一些专门用来摆放投影机的可移动小车架&#xff0c;把投影机往上一放&#xff0c;连接上线缆就可以…

php memcached close,PHP连接Memcached安装及数据库操作

memcached介绍Memcached是一套开源的高性能分布式内存对象缓存系统,它将所有的数据都存储在内存中,因为在内存中会统一维护一张巨大的Hash表,所以支持任意存储类型的数据。很多网站通过使用 Memcached提高网站的访问速度,尤其是对于大型的需要频繁访问数据的网站。Memcached是典…

坏道修复是不是硬盘东西全部都没有了_硬盘有坏道就不能用了吗?别再吃哑巴亏了,今天跟大家再说一次...

硬盘是电脑的存储硬件&#xff0c;是电脑中核心的硬件之一&#xff0c;目前市场上主要使用的是固态硬盘与机械硬盘两种&#xff0c;固态硬盘的读写速度较快&#xff0c;容量小&#xff0c;价格贵&#xff0c;机械硬盘读写速度慢&#xff0c;容量大价格便宜&#xff0c;现在的电…

html5+php调用android手机图片,html5+exif.js+canvas+php实现手机上传图片,图片损坏无法打开...

上传图片&#xff0c;图片损坏无法打开&#xff0c;图片路径也是正确的&#xff0c;function selectFileImage(fileObj) {var file fileObj.files[0];//图片方向角 added by lzkvar Orientation null;if (file) {console.log("正在上传,请稍后...");var rFilter /…

word 编辑域中的汉字_15条Word常用操作教程,简单实用,纯干货分享,收藏备用!...

点击蓝字关注我们1. 去除页眉横线在页眉插入信息的时候经常会在下面出现一条横线&#xff0c;如果这条横线影响你的视觉。这时你可以采用下述的两种方法去掉&#xff1a;用第一种的朋友比较多&#xff0c;即选中页眉的内容后&#xff0c;选取“格式”选项&#xff0c;选取“边框…

安卓抓包软件_Packet Capture安卓抓包神器介绍及使用教程

除了干货&#xff0c;其他什么也没有源码&#xff5c;资源&#xff5c;软件&#xff5c;教程&#xff5c;揭秘关 注Packet Capture是一款安卓抓包软件&#xff0c;能用来提取用户操作程序内容&#xff0c;Packet Capture可以捕获网络数据包&#xff0c;并记录它们使用中间人技术…

队列处理高并发_高并发场景下缓存处理的一些思路

在实际的开发当中&#xff0c;我们经常需要进行磁盘数据的读取和搜索&#xff0c;因此经常会有出现从数据库读取数据的场景出现。但是当数据访问量次数增大的时候&#xff0c;过多的磁盘读取可能会最终成为整个系统的性能瓶颈&#xff0c;甚至是压垮整个数据库&#xff0c;导致…

pywin32 获取窗口句柄_Excel VBA | 这个窗口居然关不掉

我的目标&#xff1a;让中国的大学生走出校门的那一刻就已经具备这些office技能&#xff0c;让职场人士能高效使用office为其服务。支持我&#xff0c;也为自己加油&#xff01;还有关不掉的窗体&#xff1f;先来看下效果&#xff1a;通过上图&#xff0c;大家很容易看出二者之…

cassss服务未启动_电梯启动死机故障处理方法

电梯情况描述&#xff1a;广东奥的斯&#xff0c;有机房 梯龄5年故障现象描述&#xff1a;现场人员反馈&#xff0c;停梯一晚&#xff0c;第二天开梯&#xff0c;门一开就死机&#xff0c;显示HAD&#xff0c;断电复位后电梯正常维修过程描述&#xff1a;1、到达现场查看历史故…

合振动的初相位推导_基于振动信号的机械设备故障诊断(一)

1.概述振动在旋转机械设备故障中占了很大比重&#xff0c;是影响设备安全&#xff0c;稳定运行的重要因素。振动直接反应了设备的健康状况&#xff0c;是设备安全评估的重要指标。通过对振动分析方法的调查&#xff0c;熟悉一般的振动分析流程及方法&#xff0c;从而对检测设备…

linux 启动db2 服务器,Linux系统设置DB2等服务开机启动的过程

Linux系统中向要设置开机启动&#xff0c;就要通过代码来实现。通过编写脚本能够把服务加到Linux开机启动项中&#xff0c;本文就来介绍一下Linux系统中设置DB2等服务开机启动的过程。1.转到/etc/init.d 目录下。以root身份执行Shell代码cd /etc/init.d2.编写DB2启动脚本Shell代…

spring elasticsearch 按条件删除_SpringBoot2 高级案例(08):整合 ElasticSearch框架,实现高性能搜索引擎...

一、安装和简介ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎&#xff0c;基于RESTful web接口。Elasticsearch是用Java开发的&#xff0c;并作为Apache许可条款下的开放源码发布&#xff0c;是当前流行的企业级搜索引擎。ElasticSe…

linux编译框架的搭建,Linux精华篇—CentOS 7.4下源码编译构建LNMP架构

CentOS 7.4搭建LNMP最新版本LNMP&#xff1a;Linux7.4、ngnix1.13.9、mysql5.7.20、php7.1.10目录&#xff1a;第一部分 准备工作第二部分 安装nginx服务第三部分 安装MySQL数据库第四部分 搭建PHP运行环境第五部分 LNMP架构应用(搭建DISCUZ论坛)第一部分 准备工作一&#xff1…

linux设备资源分配,基于Linux 简化 AMP 配置使其更方便更动态地分配资源

描述嵌入式系统一般分为两大类&#xff1a;需要硬实时性能的&#xff1b;和不需要硬实时性能的。过去&#xff0c;我们不得不做出艰难抉择&#xff1a; 选择实时操作系统的性能还是我们钟爱的 Linux 系统的丰富特性&#xff0c;然后努力弥补不足之处?如今&#xff0c;嵌入式开…

linux qt显示gif图片,QT显示GIF图片

在QT中要显示GIF图片,不能通过单单的添加部件来完成.还需要手动的编写程序.工具:QT Creator新建一个工程,我们先在designer中,添加一个QLabel部件.如下图:将QLabel拉成适当大小.在类cpp函数中添加如下程序:#include "widget.h"#include "ui_widget.h"#incl…