队列的最大值 - 栈的最小值 - LRU缓存机制

本文通过三道LeetCode题目探讨了队列和栈在数据结构中的应用,包括如何使用双端队列找到队列中的最大值,使用辅助栈获取栈的最小值,以及利用队列和哈希表实现LRU缓存机制。总结了每种方法的关键思想和实现细节,展示了数据结构在实际问题中的灵活运用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面,这是leetcode上三道解决方法有共同之处的地方,由于在写代码的时候觉得有相似之处,所以打算拿出来做一个横向的比较和总结

1. 队列的最大值:使用双端队列

这道题比第二题稍微难点,我是先接触了第二题,然后再接触这题,绕了点弯路,以为可以按照第二题的思路来,其实不是

题目描述

剑指 Offer 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]
 

限制:

1 <= push_back,pop_front,max_value的总操作数 <= 10000
1 <= value <= 10^5

首先我们来总结一下双端队列的API,这道题也就用到了几个常用的API,其他的可记可不记,反正打题 本地IDE也会提示

import java.util.*;

public class Main {

    public static Deque<Integer> deque;
    public static void main(String args[]) {
        deque = new Deque<Integer>() {
            @Override
            public void addFirst(Integer integer) {
                // 在队列头部添加元素
            }

            @Override
            public void addLast(Integer integer) {
                // 在队列尾部添加元素
            }

            @Override
            public boolean offerFirst(Integer integer) {
                // 在队列头部添加元素
                return false;
            }

            @Override
            public boolean offerLast(Integer integer) {
                // 在队列尾部添加元素
                return false;
            }

            @Override
            public Integer removeFirst() {
                // 弹出队列头部元素
                return null;
            }

            @Override
            public Integer removeLast() {
                // 弹出队列尾部元素
                return null;
            }

            @Override
            public Integer pollFirst() {
                // 弹出队列头部元素
                return null;
            }

            @Override
            public Integer pollLast() {
                // 弹出队列尾部元素
                return null;
            }

            @Override
            public Integer getFirst() {
                // 获得队列头部元素
                return null;
            }

            @Override
            public Integer getLast() {
                // 获得最后一个元素
                return null;
            }

            @Override
            public Integer peekFirst() {
                // 
                return null;
            }

            @Override
            public Integer peekLast() {
                return null;
            }

            @Override
            public boolean removeFirstOccurrence(Object o) {
                return false;
            }

            @Override
            public boolean removeLastOccurrence(Object o) {
                return false;
            }

            @Override
            public boolean add(Integer integer) {
                return false;
            }

            @Override
            public boolean offer(Integer integer) {
                return false;
            }

            @Override
            public Integer remove() {
                return null;
            }

            @Override
            public Integer poll() {
                return null;
            }

            @Override
            public Integer element() {
                return null;
            }

            @Override
            public Integer peek() {
                return null;
            }

            @Override
            public void push(Integer integer) {

            }

            @Override
            public Integer pop() {
                return null;
            }

            @Override
            public boolean remove(Object o) {
                return false;
            }

            @Override
            public boolean contains(Object o) {
                return false;
            }

            @Override
            public int size() {
                return 0;
            }

            @Override
            public Iterator<Integer> iterator() {
                return null;
            }

            @Override
            public Iterator<Integer> descendingIterator() {
                return null;
            }

            @Override
            public boolean isEmpty() {
                return false;
            }

            @Override
            public Object[] toArray() {
                return new Object[0];
            }

            @Override
            public <T> T[] toArray(T[] a) {
                return null;
            }

            @Override
            public boolean containsAll(Collection<?> c) {
                return false;
            }

            @Override
            public boolean addAll(Collection<? extends Integer> c) {
                return false;
            }

            @Override
            public boolean removeAll(Collection<?> c) {
                return false;
            }

            @Override
            public boolean retainAll(Collection<?> c) {
                return false;
            }

            @Override
            public void clear() {

            }
        }

    }
}

其中 offer/add remove/poll element/peek 之间的区别可以参照这篇

源码中也指出了他们的区别,比如
在这里插入图片描述
接下来上代码

import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;

class MaxQueue {
    public Queue<Integer> queue;

    //使用一个双端的辅助队列,这个队列并不是单纯用来存队列的最大值
    //这是一个状态队列,从队头到尾部是递减的
    //只要塞入一个元素,应该保证队列尾部的最后一个元素是 大于 当前要塞入的元素的
    //否则就要将队列尾部的元素依次弹出来


    //之所以这么设计是因为 对于下面这样一个  queue 来说
    // 7 4 3 2 5
    // 它的 assist 最后是这样的
    // 7 5
    // 对于 74325这个序列来说,最后 432 是被7和5 所屏蔽的,因此在assist中可以去掉 432

    //接下来我们来验证一下:
    //对于队列 7 4 3 2 5来说,弹出 5 的时候,assist中的 5 也会被弹出
    //因此后续队列中的最大值就是 7 了
    public Deque<Integer> assist;
    public MaxQueue() {
        this.queue = new LinkedList<Integer>();
        this.assist = new LinkedList<Integer>();
    }

    public int max_value() {
        if(!this.assist.isEmpty()){
            return assist.peekFirst();
        }
        return -1;
    }

    public void push_back(int value) {
        // 加入一个元素 
        // 如果 辅助数组中队尾元素比 要塞入的value小,那么就依次弹出这些小的队尾value
        while(!assist.isEmpty() && value > assist.peekLast()){
            // 双端队列要从 队尾弹出
            assist.pollLast();
        }
        // 最后再插入队尾
        this.assist.offerLast(value);
        this.queue.offer(value);
    }

    public int pop_front() {
        // 从队列中弹出一个元素
        if(queue.isEmpty()){
            return -1;
        }
        int val = queue.peek();
        if(val == assist.peekFirst()){//如果辅助数组的队头等于  要弹出元素的value ,那么也要跟着弹出
            assist.pollFirst();
        }
        this.queue.poll();
        return val;
    }
}

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue obj = new MaxQueue();
 * int param_1 = obj.max_value();
 * obj.push_back(value);
 * int param_3 = obj.pop_front();
 */

2. 栈的最小值:使用辅助栈

这道题我是使用比较方便易懂的做法,并且也算是能得到不错的结果

在这里插入图片描述
题目描述

面试题 03.02. 栈的最小值
请设计一个栈,除了常规栈支持的pop与push函数以外,还支持min函数,该函数返回栈元素中的最小值。执行push、pop和min操作的时间复杂度必须为O(1)。


示例:


MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

代码

class MinStack {
    public Stack<Integer> stack;
    public Stack<Integer> assist;

    /** initialize your data structure here. */
    public MinStack() {
        this.stack = new Stack<>();
        this.assist = new Stack<>();
    }
    
    public void push(int x) {
        this.stack.push(x);
        // 注意必须是 等于也要push进去,这是个坑
        if(assist.isEmpty() || x <= assist.peek()){ //这里也可以直接使用getMin来做比较
            this.assist.push(x);
        }
    }
    
    public void pop() {
        if(!this.stack.isEmpty()){
            int value = this.stack.peek();
            if(value == this.assist.peek()){
                this.assist.pop();
            }
            this.stack.pop();
        }
    }
    
    public int top() {
        if(this.stack.isEmpty()){
            // 如果是空的
            return -1;
        }
        return this.stack.peek();
    }
    
    public int getMin() {
        if(this.assist.isEmpty()){
            return -1;//题目的数据应该是没有出现过这种情况,因为 -1 是我自己杜撰的
            // 官方题解也没有写判空
        }
        return this.assist.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(x);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

3. LRU缓存机制:使用队列和哈希表

题目描述

面试题 03.02. 栈的最小值
请设计一个栈,除了常规栈支持的pop与push函数以外,还支持min函数,该函数返回栈元素中的最小值。执行push、pop和min操作的时间复杂度必须为O(1)。

示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

看了官方的题解说明之后,分析这道题的思路:

  • 首先要实现,最近最少使用(缩写就是LRU)的消息,可以使用队列来实现,也就是说,每使用一次,就提到前面来,这样最少使用的消息就排挤到后面去了,需要拿到最近最少使用的消息就直接从队列的另一头直接拿就好
  • 其次是时间复杂度的O(1)要求,并且题目已经明确暗示了key/value这种结构,我们可以联想到哈希表,因为哈希表的时间复杂度就刚好能符合我们的要求
  • 最后是LRU在操作系统中很多时候都能用来归档内存,就是将最近最少使用的元素删除或者直接归档,所以在塞入新的消息的时候应该注意容量的限制

通过以上的分析,也就是说

  • 为了实现LRU,需要使用链表
  • 为了实现O(1),需要用hashmap,并且value 就存放在 hashmap中,key就放一份在 链表 中
  • 在实现细节上,需要使用虚拟头尾节点,这样插入删除的时候就可以省去很多的判空操作

上代码

import java.util.HashMap;
import java.util.Map;

//虽然要实现的函数只有几个,但是这其中有很多过程都是可以复用的
//所以多实现几个子函数,可以让编程的时候思路更加流畅

class LRUCache {

    class Node {
        int key;
        int value;
        Node prev;
        Node next;
        public Node(){

        }
        public Node(int key, int value){
            this.key = key;
            this.value = value;
        }
    }
    //capacity 表示这个LRU系统能存多少消息
    private int capacity;
    //size 是表示当前存的消息个数
    private int size;
    //虚拟的头结点 ,尾结点
    private Node head,tail;

    private Map<Integer, Node> cache = new HashMap<Integer, Node>();

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.next  = head;
    }

    public int get(int key) {
        Node node = cache.get(key);//根据传入的值来在hashmap中get
        if(node == null){
            return -1;
        }
        moveTohead(node);
        return node.value;
    }

    public void put(int key, int value) {
        Node node = cache.get(key);
        if(node == null){//如果没有找到就要新new一个
            Node newnode = new Node(key,value);
            cache.put(key,newnode);
            addTohead(newnode);//并且这是一个新访问的节点,应该加在最前面
            this.size ++;//不要忘了消息的个数应该累加

            if(size > this.capacity){//同时添加一个节点总是要判断一下容量是否超载
                Node tmp = removeTail();//删除一个节点

                // ------------- 这是一个坑来着,除了移除消息之外,还需要对hashmap做出调整
                cache.remove(tmp.key);
                this.size --;//
            }


        }else{//如果已经原来有key了,那就在原来的基础上修改值
            node.value = value;//设置新的value
            moveTohead(node);//并且要移动到队列的头部去
        }
    }

    /*
    * 后面这几个方法都是不需要判空的,交给外部的函数去判断
    *
    *
    * */
    private void addTohead(Node node){
        // 添加节点到队头
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(Node node){
        //移除这个节点
        node.prev.next  = node.next;
        node.next.prev = node.prev;
    }

    //如果有元素是从队列中再次拿出来访问的,此时就需要将节点移动到队头
    private void moveTohead(Node node){
        // 将这个节点移动到队列的前面
        removeNode(node);//移除
        addTohead(node);//然后添加
    }

    private Node removeTail(){
        Node res = tail.prev;
        removeNode(res);
        return res;//顺便返回这个值
    }


}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

在这里插入图片描述

4.总结

做完这三道题,就来对他们之间做一个横向对比,其中第二题最简单,然后是第一题,最难是第三题(这题边界还是挺多要注意的);

其中第二题是使用了一个辅助栈,原理也很简单,就是来一个塞一个,并且看看辅助栈中栈顶的元素是否小于当前要插入的值,如果是,辅助栈也要添加相应的元素。

但是第一题的思路就有所不同,维护一个队列的最大值,相当于维护一个递减的队列,只要保持这个辅助队列是一个递减的队列——也就是每次往队列里塞一个元素,都需要看是否大于辅助队列中队尾的元素,如果是,就依次将辅助队列中所有小于这个要插入的value的所有 元素 都给弹出来。然后再插入。

第三题核心也是对队列的一个操作,并且需要对解题思路有一定的抽象和解构,解构成简单的代码模块,再组织成一个LRU缓存机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值