请定义一个队列并实现函数 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 queue;//保存实际队列数据
private List 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 queue;
private List 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;
}
}