LeetCode解题记录之队列最大值

题目:

请定义一个队列并实现函数 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,106,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;
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章