Android程序员会遇到的算法(part 6 优先级队列PriorityQueue)

Android程序员会遇到的算法(part 6 优先级队列PriorityQueue)

又是隔了四个多月才更新,从十月底来到美国开始上班,中间杂七杂八的事情很多,加上感恩节圣诞节放假出去玩了几趟,一直拖到现在。

这一次我想讲一个比较经典的Java里面的数据结构。PriorityQueue,优先级队列的一些对应的算法题。

优先级队列听起来很唬,其实就是一个帮助大家排序的数据结构而已,只不过它把插入->push,和获取队列头结点->poll()给封装起来了而已。

在很多面试的算法轮中,直接要求面试者去写排序算法的已经很少很少了,第一是排序算法其实写起来其实不简单。。。。。 :joy::joy::joy::joy::joy::joy: ,第二是现在排序算法已经很成熟,问也问不出什么门道来。所以很多情况下面试官会更加倾向于问面试者对于排序场景下,一些子场景的算法。在Java里面,PriorityQueue已经提供了大部分我们需要的api,所以接下里我们就看看有哪些经典的优先级队列的算法题。

1. 会议室问题

会议室问题可以说是排序/优先级队列应用的最具代表性的题目之一了。问题很简单,就是在给定一组会议的数据之后,判断某一个人能否完整的参加完所有会议,或者换个角度,会议安排者最少需要安排多少会议室,才能让所有会议都照常举办(没有会议室冲突)。

假设给定一个数据结构,


public class Interval {
        /**
        会议开始时间
        **/
        int start;
        
        /**
        会议结束时间
        **/
        int end;

        Interval() {
            start = 0;
            end = 0;
        }

        Interval(int s, int e) {
            start = s;
            end = e;
        }
    }


我们要实现一个boolean返回值的方法

public boolean canAttendMeetings(Interval[] intervals)

在给定一个List of Interval的情况下,判断一个人能不能完整的参于所有list里面的会议。比如:

两个箭头线段代表一个会议的跨越时长,在上图里面,两个会议直接没有重叠,正如图中的红线所示,就算红线一直平行的从左往右移动,也不会横截超过一个会议的箭头线段。所以在上图的情况,一个人是可以参与所有会议的。

但是下图所示:

这些情况下,一个人就不能参与所有的会议了,很明显红线可以同时穿过两个会议的箭头线段。

那么判断的方法是什么呢?

以正常的思维去想,肯定会觉得,我们是不是要去写一个循环,按照时间没走一秒就去循环判断所有的会议是不是在这个时间上有会议,如果超过一个就返回false?

这样做是肯定不行的,因为你不确定时间的细粒度,是秒呢?还是毫秒?还是分钟?在不确定这个的情况下,我们是没法写for 循环的。

那么我们可以换一种思路,既然不能for 循环,那能不能把每次某个会议开始或者结束当成一个事件Event,每种事件Event可以分两种类型,一种是开始start,一种是结束end,我们只需要对当前所有的全部事件进行排序之后分析,而不需要对时间本身进行循环。

比如:

按照时间线来排序的话,我们会先后有三个会议,这三个会议的起始start以此排列,从此图的示例我们可以轻松的看出,同时会有三个会议进行。但是理由呢?理由是因为你看到了线段的重叠么?真正的理由是当三个start事件进入之后,我们的第一个end事件才进入。

所以,再对所有事件排序好之后,每当我们有一个start事件,会议室数量就需要+1,每当我们有一个end事件的时候,会议室数量就-1.因为end代表一个会议结束,因此所需要的会议室数量可以减少。

有了这个前提之后,我们就可以写代码了。

先定义一个事件:



 public class TimeEvent {
        /**start类型
        **/
        public static final int start = 0;
        /**end类型
        **/
        public static final int end = 1;
        /**该事件发生的时间
        **/
        public int time;
        /**该事件的类型,是开始还是结束
        **/
        public int type;

        public TimeEvent(int type, int time) {
            this.type = type;
            this.time = time;
        }
    }



public boolean canAttendMeetings(Interval[] intervals) {
        if (intervals.length == 0) {
            return true;
        } else {
            /**
            **定义一个优先级队列,事件按照时间从小到大排列
            **/
            PriorityQueue<TimeEvent> priorityQueue = new PriorityQueue<>(new Comparator<TimeEvent>() {
                @Override
                public int compare(TimeEvent o1, TimeEvent o2) {
                    // TODO Auto-generated method stub
                    if (o1.time == o2.time) {
                        /**
                        **这里两个if暂时可能很难理解,我在下面会解释
                        **/
                        if (o1.type == TimeEvent.start && o2.type == TimeEvent.end) {
                            return 1;
                        }
                        if (o2.type == TimeEvent.start && o1.type == TimeEvent.end) {
                            return -1;
                        }
                    }
                    return o1.time - o2.time;
                }
            });
            for (int i = 0; i < intervals.length; i++) {
                /**
                 **把事件的起始和结束事件创建出来并且放入优先级队列
                 **/
                priorityQueue.add(new TimeEvent(TimeEvent.start, intervals[i].start));
                priorityQueue.add(new TimeEvent(TimeEvent.end, intervals[i].end));
            }

            int max = 0;
            int current = 0;
            while (!priorityQueue.isEmpty()) {
                TimeEvent event = priorityQueue.poll();
                if (event.type == TimeEvent.start) {
                    /**如果是开始事件,会议室数量+1,只要会议室数量大于等于2,返回false
                    /
                    current = current + 1;
                    if (current >= 2) {
                        return false;
                    }
                } else {
                     /**如果是开始事件,会议室数量-1.代表到这个时间为止,一个会议结束了。虽然我们
                     **并不在乎是哪一个会议结束了。
                      **/
                    current = current - 1;
                }
            }
            return true;
        }
    }


上面代码里面注释的这一段:


if (o1.type == TimeEvent.start && o2.type == TimeEvent.end) {
    return 1;
}
if (o2.type == TimeEvent.start && o1.type == TimeEvent.end) {
    return -1;
}


其实是处理这样的一种边界情况:

当后一个事件的start和前一个事件的end是同一时间的时候,这种情况算是需要两个会议室还是一个?

答案是看情况。。。。。

假如题目要求这种情况只需要一个会议室,那么,假如两个TimeEvent,分别是start与end,time也相同,我们必须优先处理end,因为先处理end,我们的会议室数量就会先做-1.

按照图中的例子会议室数量会:1,0,1,0这样的变化。

假如题目要求这种情况只需要两个个会议室,那么,假如两个TimeEvent,分别是start与end,time也相同,我们必须优先处理start,因为先处理start,我们的会议室数量就会先做+1.

按照图中的例子会议室数量会:1,2,1,0这样的变化。

两种情况会议室的峰值不一样。所以再回到上段代码,相信你可以理解代码中的if对应哪种情况了吧?

2. 合并线段的问题

假设给定一组线段,要求把重叠在一起的线段整合成新的线段返回,比如:

一种情况


Screen Shot 2019-01-06 at 5.28.09 PM.png

第二种情况


Screen Shot 2019-01-06 at 5.27.54 PM.png

第三种情况,没变化:

这里的解题思路和上面一样,先把每个线段安装开始时间排序,不同的是,每次在处理当前线段的时候,需要和上一个线段做对比,看看有没有重叠,如果重叠了,则需要删除上一个线段并且生成新的线段。

这里,一个栈Stack可以完美的处理!

image

步骤如下,

1.线段在优先级队列里面排好序

2.每次提取队列第一个线段

3.与stack中的栈顶线段作对比,

4.如果有重叠,pop栈顶线段,生成新的线段放入栈顶,重复第一步

我们每次只需要处理栈顶线段的原因是,如果栈顶线段和栈顶线段之前的栈内线段有冲突的话,我们在之前的一步已经处理好了,所以当前队列的第一个线段,是绝对不可能和非栈顶线段有重叠的。

代码如下:

public List<Interval> insert(List<Interval> intervals, Interval newInterval) {
        /**
        **用优先级队列把所有线段排好序,按照起始时间
        **/
        PriorityQueue<Interval> priorityQueue = new PriorityQueue<Interval>(new Comparator<Interval>() {
            public int compare(Interval o1, Interval o2) {
                return o1.start - o2.start;
            };
        });
        for (int i = 0; i < intervals.size(); i++) {
            priorityQueue.add(intervals.get(i));
        }
        priorityQueue.add(newInterval);

        /**
        **用栈保存处理过的线段
        **/
        Stack<Interval> stack = new Stack<>();
        stack.push(priorityQueue.remove());
        /**
        **while循环处理线段
        **/
        while (!stack.isEmpty() && !priorityQueue.isEmpty()) {
            Interval inItem = priorityQueue.remove();
            Interval originalItem = stack.pop();
            /**
            **当线段不完全重叠的时候,取两者的最小开始时间和最大结束时间,生成新的线段
            **/
            if (inItem.start <= originalItem.end && inItem.end > originalItem.end) {
                stack.push(new Interval(originalItem.start, inItem.end));
                /**
            **当线段完全重叠的时候,取两者的中覆盖面最大的那一线段
            **/
            } else if (inItem.start <= originalItem.end && originalItem.end >= inItem.end) {
                stack.push(originalItem);
            } 
               /**
            **当线段没有重叠的时候,两者一起入栈
            **/
            else {
                stack.push(originalItem);
                stack.push(inItem);
            }
        }
         /**
            **因为stack的输出是倒着来的,所以翻转一次。。。
            **/
        Stack<Interval> stack2 = new Stack<>();
        while (!stack.isEmpty()) {
            stack2.push(stack.pop());
        }
        ArrayList<Interval> arrayList = new ArrayList<>();
        while (!stack2.isEmpty()) {
            arrayList.add(stack2.pop());
        }
        return arrayList;

    }

PS:其实笔者在写完之后才发现其实用一个LinkedList来代替代码中的stack更好一些。。。。可以不需要翻转一次。读者可以自行摸索。。。

2. 城市天际线问题

最后一个问题留给读者们自己去思考,城市天际线问题:

image

在给出若干组城市建筑的坐标和高度之后,返回最后应该画出来的天际线的样子,这题也是需要对数据进行排序,按照事件来处理的题目。属于稍微复杂一点的问题,但是原则还是一样,需要用到优先级队列来处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容