leetcode三道题目:队列的最大值 - 栈的最小值 - LRU缓存机制
写在前面,这是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缓存机制。