題目:
請定義一個隊列並實現函數 max_value 得到隊列裏的最大值,要求函數max_value、push_back 和 pop_front 的均攤時間複雜度都是O(1)。
若隊列爲空,pop_front 和 max_value 需要返回 -1
public class MaxQueue{
public MaxQueue(){
}
public int max_value() {
//do something
}
public void push_back(int value) {
//do something
}
public int pop_front() {
//do something
}
}
以上是LeetCode給出的題目,理解一下就是說讓我們自己完成一個隊列類的構建。需要實現push_back方法將value插入隊列的尾部,pop_front方法將隊列頭取出,max_value將隊列中的最大值返回,但不將值移除隊列。
其中有一個時間複雜度的要求,要讓每一個方法的均攤時間複雜度變爲O(1)。這個是本題的唯一難點,其它的問題都是小意思啦。
爲了搞定這個題目我使用了雙List模式:
public class MaxQueue{
private List<Integer> queue;//保存實際隊列數據
private List<Integer> max;//保存隊列中的最大值
public MaxQueue(){
queue = new LinkedList<>();
max = new LinkedList<>();
}
//...省略部分代碼
}
很多小夥伴可能會問爲什麼要用list來保存最大值,直接用int不就好了嗎?
可是事情不簡單吶,比如我將{5,4,3,2,1}依次加入隊列中,那麼此時調用max_value方法,應該返回多少呢?
小夥伴:此時應該返回 5 !
沒毛病,的確是返回5。但是如果我調用一次pop_front,然後在調用一次max_value方法,這個時候應該返回多少呢?
小夥伴:原隊列{5,4,3,2,1},出掉隊列頭就剩下{4,3,2,1},此時的最大值應該是4!
沒錯啦,如果此時你只是用int類型來存儲最大值,那麼在出隊列的時候,就會導致max_value失真呀。
小夥伴:那麼list怎麼樣保證最大值不會失真呢?
祕訣就在push_back方法中:
public class MaxQueue{
//...省略部分代碼
public void push_back(int value) {
queue.add(value);
while(max.size() > 0 && max.get(max.size()-1) < value){
max.remove(max.size()-1);
}
max.add(value);
}
//...省略部分代碼
}
小夥伴:what?一個註釋都沒有,寫的這是啥意思?
莫着急,我舉幾個例子來說一下原理。比如:
{5,4}這個隊列中的最大值是多少?
小夥伴:5!
那通過pop_front方法取出隊列頭之後呢?
小夥伴:4!我知道了通過雙list的方法,對max集合進行排序就可以了,然後我們就實時知道最大值了!
是這樣沒錯,但是題目要求了時間複雜度爲O(1),任何一種排序算法的時間複雜度都不是O(1)能夠完成的
小夥伴:不通過排序算法的話,我們怎麼確定哪個值是最大值嗎?
當然可以,我們在每一個隊列入隊的時候進行一次比較就可以了
比如:queue={5} 此時max={5},因爲隊列中只有一個值,所以這個值肯定是最大值
隨後繼續添加數據入隊,queue={5,4} ,此時我們將max中的已有的值a與新入隊的值b做比較。
如果a > b,那麼max={a,b},如果a < b,那麼max={b},移除a。
根據這條公式,我們將queue中的值代入,得到max={5,4}。
小夥伴:你這樣的算法好像只能適用與隊列的值爲兩個的情況,如果隊列的值多了呢?
比如:queue={5,4,3,6,1},按照這個順序依次入隊、出隊。你應該怎麼處理?
OK,我們先按剛剛的規則走一遍,看看問題出在哪。
1)queue={5}max = {5}
2)queue={5,4}max={5,4}
3)queue={5,4,3}max={5,4,3}
4)queue={5,4,3,6}max={6}
5)...
小夥伴:你等會兒!!!怎麼到了第四步max突然就變成6了呢?而且還移除了其它值?
hhh,這就是隊列的規則啦。你想想queue的入隊規則是什麼?
小夥伴:每一個新加入的值都添加到隊尾
那麼出隊規則呢?
小夥伴:每一個出隊的值,都是當前隊列的隊頭,也就是最先加入隊列的值
沒錯,就是根據這個規則,在queue={5,4,3,6}的時候,無論5、4、3有沒有出棧,當前隊列的最大值都是6。
所以排在6前面且比6小的數,都可以直接忽略了,因爲不存在先讓6出隊,而5、4、3還留在隊內的情況。
小夥伴:那就是說,每添加一個新的數A入隊,就需要將A與max集合中已有的所有值相比較?
是這麼個意思,的確需要進行比較,但不是和所有的值比較。
比如說,queue={5,4,10,6,1,2,3,4},假設隊列按照這樣的順序,那麼max中應該怎麼存放最大值呢?
小夥伴:額,如果max中存放10 的話,當10出隊之後,剩下的數字6最大。如果存放6的話,當6出隊之後4最大...這有什麼規律嗎?
沒錯,存在一條規律。當queue={5,4,10,6,1,2,3,4},標紅的數字,就是max中要依次存放的值。
小夥伴:額,規律是什麼?
存放步驟講解:
1) queue={5} max={5} 因爲5是唯一的數
2) queue={5,4} max={5,4} 因爲在5出棧之後,4就是最大的數(唯一的數)
3) queue={5,4,10} max={10} 因爲10比max中其它值都大,而且它還是最後一位
4) queue={5,4,10,6} max={10,6} 10 還是max中最大的值,但它排在6前面,意思就是說,當10出隊之後,6就是最大的了
5) queue={5,4,10,6,1} max={10,6,1} 原因和上面一樣,10和6依次出隊,1就是最大值了
6) queue={5,4,10,6,1,2} max={10,6,2} 這裏的2入隊後,1不再是隊列的最後一位,當10、6出隊後,最大的數是2哦
7) queue={5,4,10,6,1,2,3} max={10,6,3} 和上面一樣,3取代了2的位置,成爲了隊中最後的大數。
8) queue={5,4,10,6,1,2,3,4} max={10,6,4} 到這裏4又取代了3
小夥伴:所以一個新數A入隊的時候都要和max中最後一個值B做比較?如果B > A,則將A添加至max的隊尾,如果B < A,就將B移除,然後再添加A。
沒錯。。。就是這麼個意思,這樣就能夠保證隊列max的隊頭,永遠是queue中的最大值。
//完整代碼
public class MaxQueue{
private List<Integer> queue;
private List<Integer> max;
public MaxQueue(){
queue = new LinkedList<>();
max = new LinkedList<>();
}
public int max_value() {
return max.size() > 0 ? max.get(0) : -1;
}
public void push_back(int value) {
queue.add(value);
while(max.size() > 0 && max.get(max.size()-1) < value){
max.remove(max.size()-1);
}
max.add(value);
}
public int pop_front() {
int tmp = -1;
if(queue.size() > 0){
tmp = queue.get(0);
queue.remove(0);
if(tmp == max.get(0)){
max.remove(0);
}
}
return tmp;
}
}