Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
今天做两道设计题,分别对应LeetCode 146.LRU缓存和LeetCode 155.最小栈。具体题目可点击链接。
解题思路
146. LRU缓存
对于146这题,要求设计一个满足LRU
缓存约束的数据结构,并且存入元素和获取元素都是的时间复杂度。获取元素为,很容易想到数组,而插入元素这则是链表,有什么数据结构结合了这两点吗,那就是Map
。因此本题主要数据结构就是使用HashMap
存储元素(关于HashMap
的底层原理,看这里),但如何记录每个key
的使用次数呢?这才是关键。
这里采用的是双向链表存储这种使用关系,双向链表的头部指向的是最近使用的节点,每次元素使用都采用头插法,这样可以保证头部指向的节点必定是最近使用的,而尾部则是最近最少使用的。当元素数量大于容量的时候,我们只需要通过尾部节点获取节点的key
,之后删除即可。因此我们需要设计的双向链表结构为:
class DoubleList{
private int key;
private int value;
DoubleList pre;
DoubleList next;
public DoubleList(){}
public DoubleList(int key, int value){
this.key = key;
this.value = value;
}
注意此处的key
必须要,因为需要根据尾部节点来找map
中的key
。代码如下:
class DoubleList{
private int key;
private int value;
DoubleList pre;
DoubleList next;
public DoubleList(){}
public DoubleList(int key, int value){
this.key = key;
this.value = value;
}
}
private HashMap<Integer, DoubleList> map;
private int capacity;
DoubleList head;
DoubleList tail;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
head = new DoubleList();
tail = new DoubleList();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
if(!map.containsKey(key)){
return -1;
}else{
removeNode(map.get(key));
addNode(map.get(key));
return map.get(key).value;
}
}
public void put(int key, int value) {
DoubleList node = map.get(key);
if(node == null){
if(map.size()>=capacity){
DoubleList tailNode = tail.pre;
removeNode(tailNode);
map.remove(tailNode.key);
DoubleList newNode = new DoubleList(key, value);
addNode(newNode);
map.put(key, newNode);
}else {
DoubleList newNode = new DoubleList(key, value);
addNode(newNode);
map.put(key, newNode);
}
}else {
removeNode(node);
DoubleList newNode = new DoubleList(key, value);
addNode(newNode);
map.put(key, newNode);
}
}
// 靠近头的是最近使用的 靠近尾则是最近最少未使用
public void addNode(DoubleList node){
node.next = head.next;
head.next.pre = node;
head.next = node;
node.pre = head;
}
public void removeNode(DoubleList node){
node.pre.next = node.next;
node.next.pre = node.pre;
}
155.最小栈
对于155题,push
和 pop
以及 top
在stack
中都包含原型函数可以调用,因此本题的关键是如何获取最小值。
简单的思路是我们可以每次push
都保存当前值和最小值,这样每次通过栈顶就可以得到最小值,而其它的操作都是栈的基本使用,代码如下:
private Stack<int[]> stack;
public MinStack() {
stack = new Stack<>();
}
public void push(int val) {
if(!stack.isEmpty()){
int min = Math.min(val, stack.peek()[1]);
stack.push(new int[]{val, min});
}else {
stack.push(new int[]{val, val});
}
}
public void pop() {
stack.pop();
}
public int top() {
return stack.peek()[0];
}
public int getMin() {
return stack.peek()[1];
}
还有一种思路是使用一个额外的栈来保存最小元素,每次入栈出栈都同时对两个栈进行操作,而最小栈的栈顶也对应当前栈的最小值,代码如下:
private Stack<Integer> stack;
private Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int val) {
if(!stack.isEmpty()){
int min = Math.min(val, minStack.peek());
stack.push(val);
minStack.push(min);
}else {
stack.push(val);
minStack.push(val);
}
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}