数据结构与算法——堆的应用

后端君
• 阅读 1065
1. 概述

前面说完了堆这种数据结构,并且讲到了它很经典的一个应用:堆排序,其实堆这种数据结构还有其他很多的应用,今天就一起来看看,主要有下列内容:

  • 优先级队列
  • 求 Top K 问题
  • 求中位数
2. 优先级队列

优先级队列是一种特殊的队列,前面学习队列的时候,说到队列满足 先进先出,后进后出 的特点,优先级队列则不是这样。优先级队列中的数据,出队的顺序是有优先级的,优先级高的,先出队列。

而堆其实就可以看作是一个优先级队列,因为堆顶元素总是数据中最大或最小的元素,每次出队列都可以看作取出堆顶元素。

如果你熟悉 Java 语言,则或多或少听说或是使用过 PriorityQueue 这个容器,在《Java 核心技术·卷 I》中,说到 PriorityQueue 就是优先级队列,并且它基于一种很优雅的数据结构——堆。

接下来就小试牛刀,举一个具体的例子来看看优先级队列的应用。例如我们需要合并 10 个有序的小文件,小文件中存储的是有序的字符串数据。借助优先级队列,我们可以很高效的解决这个问题。

我们从每个文件中读取第一个字符串存入优先级队列中,那么每次出队列,都是最小的那个元素。将出队列的数据存储到一个大文件中,然后继续从文件中读取一个字符串存入队列,然后继续出队列,一直循环这个操作。

当然,这主要是针对数据文件较大的情况,如果数据不多,那么直接将全部的数据存入队列,然后依次出队列就可以了,具体问题具体分析。

3. Top K 问题

这样的问题其实非常的常见了,在一组数据当中 ,我们需要求得其前 K 大的数据。

这分为了两种情况,一是针对 静态数据 ,即数据不会发生变化。我们可以维护一个大小为 K 的小顶堆,然后依次遍历数组,如果数组数据比堆顶元素大,则插入到堆中,如果小,则不做处理。遍历完之后,则堆中存在的数据就是 Top K 了。我用代码模拟了这个过程:

public class GetTopK {
    public static void main(String[] args) {
        int[] num = {2, 34, 45, 56, 76, 65, 678, 33, 888, 678, 98, 0, 7};

        //求 Top 3
        Queue<Integer> queue = new PriorityQueue<>(3);
        queue.add(num[0]);
        queue.add(num[1]);
        queue.add(num[2]);

        for (int i = 3; i < num.length; i++) {
            int small = queue.peek();
            if (num[i] > small){
                queue.poll();
                queue.add(num[i]);
            }
        }
        System.out.println(queue.toString());
    }
}

第二种情况,是动态的数据集合,数据会有增加、删除的情况,如果新增一个元素,将其和堆顶元素进行比较,如果数据比堆顶元素大,则插入到堆中,如果小,则不做处理。这样的话,无论数据怎样变化,我们都能够随时拿到 Top K,而不用因为数据的变化重新组织堆。

4. 求中位数

顾名思义,中位数就是一组数据中最中间的那个数据,只不过注意,数据需要有序排列。针对一个大小为 n 的数据集,如果 n 为偶数,那么中位数有两个,分别是 n/2 和 n/2 + 1 这两个数据,我们可以随机取其中一个;如果 n 为奇数,则 n/2 + 1 这个数为中位数。

如果是一个静态的数据,那么可直接排序然后求中位数,但是如果数据有变化,这样每次排序的成本太高了。所以,可以借助堆来实现求中位数的功能。

我们可以维护一个大顶堆,一个小顶堆,小顶堆中存储后 n/2 个数据,大顶堆中存储前面剩余的数据。如果 n 是偶数,则两个堆中存储的都是相同个数的数据,如果 n 为奇数,则大顶堆中要多一个数据。结合下图你就很容易明白了:

数据结构与算法——堆的应用

如果有数据插入的情况,如果数据小于等于大顶堆顶元素,则插入到大顶堆中,如果数据大于等于小顶堆顶元素,则插入到小顶堆中。只不过可能会出现一个问题,就是堆中的数据不满足均分情况,那么我们需要移动两个堆中的元素,反正需要保证 大顶堆的元素个数和小顶堆的元素个数要么相等,或者大顶堆中多一个。

我用代码简单模拟了整个实现:


    public class GetMiddleNum {
        public static void main(String[] args) {
            //原始数据
            Integer[] num = {12, 34, 6, 43, 78, 65, 42, 33, 5, 8};
            //排序后存入ArrayList中
            Arrays.sort(num);
            ArrayList<Integer> data = new ArrayList<>(Arrays.asList(num));
            //大顶堆
            Queue<Integer> bigQueue = new PriorityQueue<>((o1, o2) -> {
                if (o1 <= o2) return 1;
                else return -1;
            });
            //小顶堆
            Queue<Integer> smallQueue = new PriorityQueue<>();
    
            int n = data.size();
            int i;
            if (n % 2 == 0) i = n / 2;
            else i = n / 2 + 1;
    
            //后 n/2 的数据存入到小顶堆中
            for (int j = i; j < n; j++) {
                smallQueue.add(data.get(j));
            }
            //前面的数据存入到大顶堆中
            for (int j = 0; j < i; j++) {
                bigQueue.add(data.get(j));
            }
    
            //插入数据,需要做单独的处理
            insert(data, 99, bigQueue, smallQueue);
            insert(data, 3, bigQueue, smallQueue);
            insert(data, 1, bigQueue, smallQueue);
    
            //大顶堆的堆顶元素就是中位数
            System.out.println("The middle num = " + bigQueue.peek());
        }
    
        private static void insert(List<Integer> list, int value, Queue<Integer> bigQueue, Queue<Integer> smallQueue){
            list.add(value);
            if (value <= bigQueue.peek())
                bigQueue.add(value);
            if (value >= smallQueue.peek())
                smallQueue.add(value);
    
            while (smallQueue.size() > bigQueue.size())
                bigQueue.add(smallQueue.poll());
            while (bigQueue.size() - smallQueue.size() > 1)
                smallQueue.add(bigQueue.poll());
        }
    }
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java 数据结构
简要本文主要介绍数据结构以及在Java中有哪些直接可用的数据结构(不涉及并发编程使用场景)。常见的数据结构下面直接介绍的常见的数据结构:数组(Array)、栈(Stack)、队列(Queue)、链表(LinkedList)、树(Tree)、堆(Heap)、散列表(Hash)、图(Graph)数组(Array)
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Stella981 Stella981
3年前
PriorityBlockingQueue 介绍
PriorityBlockingQueue是一个基于优先级堆的无界的并发安全的优先级队列(FIFO),队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的Comparator进行排序,具体取决于所使用的构造方法。实现原理PriorityBlockingQueue通过使用堆这种数据结构实现将队列中的元素按照某种排序规则进行排序,从而改变先进先
Wesley13 Wesley13
3年前
Java数据结构和算法(十五)——无权无向图
前面我们介绍了树这种数据结构,树是由n(n0)个有限节点通过连接它们的边组成一个具有层次关系的集合,把它叫做“树”是因为它看起来像一棵倒挂的树,包括二叉树、红黑树、234树、堆等各种不同的树,有对这几种树不了解的可以参考我前面几篇博客。而本篇博客我们将介绍另外一种数据结构——图,图也是计算机程序设计中最常用的数据结构之一,从数学意义上讲
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
4个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(