Android程序員面試會遇到的算法系列:java
又是隔了四個多月才更新,從十月底來到美國開始上班,中間雜七雜八的事情不少,加上感恩節聖誕節放假出去玩了幾趟,一直拖到如今。this
這一次我想講一個比較經典的Java裏面的數據結構。PriorityQueue,優先級隊列的一些對應的算法題。spa
優先級隊列聽起來很唬,其實就是一個幫助你們排序的數據結構而已,只不過它把插入->push,和獲取隊列頭結點->poll()給封裝起來了而已。
在不少面試的算法輪中,直接要求面試者去寫排序算法的已經不多不多了,第一是排序算法其實寫起來其實不簡單。。。。。 :joy::joy::joy::joy::joy::joy: ,第二是如今排序算法已經很成熟,問也問不出什麼門道來。因此不少狀況下面試官會更加傾向於問面試者對於排序場景下,一些子場景的算法。在Java裏面,PriorityQueue已經提供了大部分咱們須要的api,因此接下里咱們就看看有哪些經典的優先級隊列的算法題。
會議室問題能夠說是排序/優先級隊列應用的最具表明性的題目之一了。問題很簡單,就是在給定一組會議的數據以後,判斷某一我的可否完整的參加完全部會議,或者換個角度,會議安排者最少須要安排多少會議室,才能讓全部會議都照常舉辦(沒有會議室衝突)。
假設給定一個數據結構,
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對應哪一種狀況了吧?
假設給定一組線段,要求把重疊在一塊兒的線段整合成新的線段返回,好比:
一種狀況
第二種狀況
第三種狀況,沒變化:
這裏的解題思路和上面同樣,先把每一個線段安裝開始時間排序,不一樣的是,每次在處理當前線段的時候,須要和上一個線段作對比,看看有沒有重疊,若是重疊了,則須要刪除上一個線段而且生成新的線段。
這裏,一個棧Stack能夠完美的處理!
步驟以下,
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更好一些。。。。能夠不須要翻轉一次。讀者能夠自行摸索。。。
最後一個問題留給讀者們本身去思考,城市天際線問題:
在給出若干組城市建築的座標和高度以後,返回最後應該畫出來的天際線的樣子,這題也是須要對數據進行排序,按照事件來處理的題目。屬於稍微複雜一點的問題,可是原則仍是同樣,須要用到優先級隊列來處理。