Android程序員會(huì)遇到的算法(part 6 優(yōu)先級(jí)隊(duì)列PriorityQueue)

Android程序員會(huì)遇到的算法(part 6 優(yōu)先級(jí)隊(duì)列PriorityQueue)

又是隔了四個(gè)多月才更新,從十月底來(lái)到美國(guó)開(kāi)始上班,中間雜七雜八的事情很多,加上感恩節(jié)圣誕節(jié)放假出去玩了幾趟,一直拖到現(xiàn)在。

這一次我想講一個(gè)比較經(jīng)典的Java里面的數(shù)據(jù)結(jié)構(gòu)。PriorityQueue,優(yōu)先級(jí)隊(duì)列的一些對(duì)應(yīng)的算法題。

優(yōu)先級(jí)隊(duì)列聽(tīng)起來(lái)很唬,其實(shí)就是一個(gè)幫助大家排序的數(shù)據(jù)結(jié)構(gòu)而已,只不過(guò)它把插入->push,和獲取隊(duì)列頭結(jié)點(diǎn)->poll()給封裝起來(lái)了而已。

在很多面試的算法輪中,直接要求面試者去寫(xiě)排序算法的已經(jīng)很少很少了,第一是排序算法其實(shí)寫(xiě)起來(lái)其實(shí)不簡(jiǎn)單。。。。。 :joy::joy::joy::joy::joy::joy: ,第二是現(xiàn)在排序算法已經(jīng)很成熟,問(wèn)也問(wèn)不出什么門(mén)道來(lái)。所以很多情況下面試官會(huì)更加傾向于問(wèn)面試者對(duì)于排序場(chǎng)景下,一些子場(chǎng)景的算法。在Java里面,PriorityQueue已經(jīng)提供了大部分我們需要的api,所以接下里我們就看看有哪些經(jīng)典的優(yōu)先級(jí)隊(duì)列的算法題。

1. 會(huì)議室問(wèn)題

會(huì)議室問(wèn)題可以說(shuō)是排序/優(yōu)先級(jí)隊(duì)列應(yīng)用的最具代表性的題目之一了。問(wèn)題很簡(jiǎn)單,就是在給定一組會(huì)議的數(shù)據(jù)之后,判斷某一個(gè)人能否完整的參加完所有會(huì)議,或者換個(gè)角度,會(huì)議安排者最少需要安排多少會(huì)議室,才能讓所有會(huì)議都照常舉辦(沒(méi)有會(huì)議室沖突)。

假設(shè)給定一個(gè)數(shù)據(jù)結(jié)構(gòu),


public class Interval {
        /**
        會(huì)議開(kāi)始時(shí)間
        **/
        int start;
        
        /**
        會(huì)議結(jié)束時(shí)間
        **/
        int end;

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

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


我們要實(shí)現(xiàn)一個(gè)boolean返回值的方法

public boolean canAttendMeetings(Interval[] intervals)

在給定一個(gè)List of Interval的情況下,判斷一個(gè)人能不能完整的參于所有l(wèi)ist里面的會(huì)議。比如:

兩個(gè)箭頭線段代表一個(gè)會(huì)議的跨越時(shí)長(zhǎng),在上圖里面,兩個(gè)會(huì)議直接沒(méi)有重疊,正如圖中的紅線所示,就算紅線一直平行的從左往右移動(dòng),也不會(huì)橫截超過(guò)一個(gè)會(huì)議的箭頭線段。所以在上圖的情況,一個(gè)人是可以參與所有會(huì)議的。

但是下圖所示:

這些情況下,一個(gè)人就不能參與所有的會(huì)議了,很明顯紅線可以同時(shí)穿過(guò)兩個(gè)會(huì)議的箭頭線段。

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

以正常的思維去想,肯定會(huì)覺(jué)得,我們是不是要去寫(xiě)一個(gè)循環(huán),按照時(shí)間沒(méi)走一秒就去循環(huán)判斷所有的會(huì)議是不是在這個(gè)時(shí)間上有會(huì)議,如果超過(guò)一個(gè)就返回false?

這樣做是肯定不行的,因?yàn)槟悴淮_定時(shí)間的細(xì)粒度,是秒呢?還是毫秒?還是分鐘?在不確定這個(gè)的情況下,我們是沒(méi)法寫(xiě)for 循環(huán)的。

那么我們可以換一種思路,既然不能for 循環(huán),那能不能把每次某個(gè)會(huì)議開(kāi)始或者結(jié)束當(dāng)成一個(gè)事件Event,每種事件Event可以分兩種類(lèi)型,一種是開(kāi)始start,一種是結(jié)束end,我們只需要對(duì)當(dāng)前所有的全部事件進(jìn)行排序之后分析,而不需要對(duì)時(shí)間本身進(jìn)行循環(huán)。

比如:

按照時(shí)間線來(lái)排序的話,我們會(huì)先后有三個(gè)會(huì)議,這三個(gè)會(huì)議的起始start以此排列,從此圖的示例我們可以輕松的看出,同時(shí)會(huì)有三個(gè)會(huì)議進(jìn)行。但是理由呢?理由是因?yàn)槟憧吹搅司€段的重疊么?真正的理由是當(dāng)三個(gè)start事件進(jìn)入之后,我們的第一個(gè)end事件才進(jìn)入。

所以,再對(duì)所有事件排序好之后,每當(dāng)我們有一個(gè)start事件,會(huì)議室數(shù)量就需要+1,每當(dāng)我們有一個(gè)end事件的時(shí)候,會(huì)議室數(shù)量就-1.因?yàn)閑nd代表一個(gè)會(huì)議結(jié)束,因此所需要的會(huì)議室數(shù)量可以減少。

有了這個(gè)前提之后,我們就可以寫(xiě)代碼了。

先定義一個(gè)事件:



 public class TimeEvent {
        /**start類(lèi)型
        **/
        public static final int start = 0;
        /**end類(lèi)型
        **/
        public static final int end = 1;
        /**該事件發(fā)生的時(shí)間
        **/
        public int time;
        /**該事件的類(lèi)型,是開(kāi)始還是結(jié)束
        **/
        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 {
            /**
            **定義一個(gè)優(yōu)先級(jí)隊(duì)列,事件按照時(shí)間從小到大排列
            **/
            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) {
                        /**
                        **這里兩個(gè)if暫時(shí)可能很難理解,我在下面會(huì)解釋
                        **/
                        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++) {
                /**
                 **把事件的起始和結(jié)束事件創(chuàng)建出來(lái)并且放入優(yōu)先級(jí)隊(duì)列
                 **/
                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) {
                    /**如果是開(kāi)始事件,會(huì)議室數(shù)量+1,只要會(huì)議室數(shù)量大于等于2,返回false
                    /
                    current = current + 1;
                    if (current >= 2) {
                        return false;
                    }
                } else {
                     /**如果是開(kāi)始事件,會(huì)議室數(shù)量-1.代表到這個(gè)時(shí)間為止,一個(gè)會(huì)議結(jié)束了。雖然我們
                     **并不在乎是哪一個(gè)會(huì)議結(jié)束了。
                      **/
                    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;
}


其實(shí)是處理這樣的一種邊界情況:

當(dāng)后一個(gè)事件的start和前一個(gè)事件的end是同一時(shí)間的時(shí)候,這種情況算是需要兩個(gè)會(huì)議室還是一個(gè)?

答案是看情況。。。。。

假如題目要求這種情況只需要一個(gè)會(huì)議室,那么,假如兩個(gè)TimeEvent,分別是start與end,time也相同,我們必須優(yōu)先處理end,因?yàn)橄忍幚韊nd,我們的會(huì)議室數(shù)量就會(huì)先做-1.

按照?qǐng)D中的例子會(huì)議室數(shù)量會(huì):1,0,1,0這樣的變化。

假如題目要求這種情況只需要兩個(gè)個(gè)會(huì)議室,那么,假如兩個(gè)TimeEvent,分別是start與end,time也相同,我們必須優(yōu)先處理start,因?yàn)橄忍幚韘tart,我們的會(huì)議室數(shù)量就會(huì)先做+1.

按照?qǐng)D中的例子會(huì)議室數(shù)量會(huì):1,2,1,0這樣的變化。

兩種情況會(huì)議室的峰值不一樣。所以再回到上段代碼,相信你可以理解代碼中的if對(duì)應(yīng)哪種情況了吧?

2. 合并線段的問(wèn)題

假設(shè)給定一組線段,要求把重疊在一起的線段整合成新的線段返回,比如:

一種情況


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

第二種情況


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

第三種情況,沒(méi)變化:

這里的解題思路和上面一樣,先把每個(gè)線段安裝開(kāi)始時(shí)間排序,不同的是,每次在處理當(dāng)前線段的時(shí)候,需要和上一個(gè)線段做對(duì)比,看看有沒(méi)有重疊,如果重疊了,則需要?jiǎng)h除上一個(gè)線段并且生成新的線段。

這里,一個(gè)棧Stack可以完美的處理!

image

步驟如下,

1.線段在優(yōu)先級(jí)隊(duì)列里面排好序

2.每次提取隊(duì)列第一個(gè)線段

3.與stack中的棧頂線段作對(duì)比,

4.如果有重疊,pop棧頂線段,生成新的線段放入棧頂,重復(fù)第一步

我們每次只需要處理?xiàng)m斁€段的原因是,如果棧頂線段和棧頂線段之前的棧內(nèi)線段有沖突的話,我們?cè)谥暗囊徊揭呀?jīng)處理好了,所以當(dāng)前隊(duì)列的第一個(gè)線段,是絕對(duì)不可能和非棧頂線段有重疊的。

代碼如下:

public List<Interval> insert(List<Interval> intervals, Interval newInterval) {
        /**
        **用優(yōu)先級(jí)隊(duì)列把所有線段排好序,按照起始時(shí)間
        **/
        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);

        /**
        **用棧保存處理過(guò)的線段
        **/
        Stack<Interval> stack = new Stack<>();
        stack.push(priorityQueue.remove());
        /**
        **while循環(huán)處理線段
        **/
        while (!stack.isEmpty() && !priorityQueue.isEmpty()) {
            Interval inItem = priorityQueue.remove();
            Interval originalItem = stack.pop();
            /**
            **當(dāng)線段不完全重疊的時(shí)候,取兩者的最小開(kāi)始時(shí)間和最大結(jié)束時(shí)間,生成新的線段
            **/
            if (inItem.start <= originalItem.end && inItem.end > originalItem.end) {
                stack.push(new Interval(originalItem.start, inItem.end));
                /**
            **當(dāng)線段完全重疊的時(shí)候,取兩者的中覆蓋面最大的那一線段
            **/
            } else if (inItem.start <= originalItem.end && originalItem.end >= inItem.end) {
                stack.push(originalItem);
            } 
               /**
            **當(dāng)線段沒(méi)有重疊的時(shí)候,兩者一起入棧
            **/
            else {
                stack.push(originalItem);
                stack.push(inItem);
            }
        }
         /**
            **因?yàn)閟tack的輸出是倒著來(lái)的,所以翻轉(zhuǎn)一次。。。
            **/
        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:其實(shí)筆者在寫(xiě)完之后才發(fā)現(xiàn)其實(shí)用一個(gè)LinkedList來(lái)代替代碼中的stack更好一些。。。??梢圆恍枰D(zhuǎn)一次。讀者可以自行摸索。。。

2. 城市天際線問(wèn)題

最后一個(gè)問(wèn)題留給讀者們自己去思考,城市天際線問(wèn)題:

image

在給出若干組城市建筑的坐標(biāo)和高度之后,返回最后應(yīng)該畫(huà)出來(lái)的天際線的樣子,這題也是需要對(duì)數(shù)據(jù)進(jìn)行排序,按照事件來(lái)處理的題目。屬于稍微復(fù)雜一點(diǎn)的問(wèn)題,但是原則還是一樣,需要用到優(yōu)先級(jí)隊(duì)列來(lái)處理。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容