1.题目
2.思路
这个题目也常常出现在面试题目中,不能使用自带的哈希链表。需要手动写一个哈希链表。
2.1为什么是哈希+链表?
哈希表的特点:查找块,数据没有固定顺序。(因为每次都要按照某个优先级,所以相对来说是有序)
链表的特点:插入快,删除快,可以有序, 但是查找慢。
哈希+链表 :结合两者的优点 -- 》实现查找快、插入快、删除快、并且有一定的顺序。
2.2为什么是双向链表?而不是单向链表?
单向链表:只有某一个方向的节点next节点,没有前驱节点prev。因为删除节点的时候,也需要操作节点的prev指针。单向链表无法直接获取节点node的前一个节点。
双向链表:每一个节点都有后继结点指针next 和前驱节点指针prev,这样可以保证删除节点的时候可以通过prev指针找到前驱节点。
2.3为什么双向链表中也要存的是key value键值对?不能直接存value值嘛?
因为当map中存的个数size大于缓存的大小capacity时,需要考虑删除map中的一个键值对,才能放入一个新的键值对。这时候必须要从双向链表中得到key,才能执行map.remove(key);
2.4为什么要设置一个假的头部节点head,和一个假的尾部节点tail?
这个原理是普通的单向链表处理的时候,喜欢在最前面加一个哑巴节点dummy(也交哨兵节点)意思差不多。主要是为了方便写这个函数,不需要做特殊的判断,比如一开始为空的时候和有节点的时候,只要有这个假的head 和tail,就可以保证处理流程是一样的。要值得注意的是,head 指向的是链表中真正的头结点, tail的前驱节点指向的是真正的尾部节点。所以在删除尾部节点的时候,一定是remove(tail.prev).
class LRUCache {
class DLinkedNode{
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode(){
}
public DLinkedNode(int _key, int _value){
key = _key;
value = _value;
}
}
Map<Integer, DLinkedNode>map = new HashMap<>();
int size = 0;
int capacity = 0;
// 定义伪头部节点 和 尾部节点 --假的头部和尾部节点,并不是真正的
DLinkedNode head = new DLinkedNode(); // 易错1
DLinkedNode tail = new DLinkedNode();
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = map.get(key);
if(node == null) return -1;
else{
// 删除节点 + 放到 队列头部
removeNode(node);
addTohead(node);
return node.value;
}
}
public void put(int key, int value) {
DLinkedNode node = map.get(key);
if(node == null){
DLinkedNode newNode = new DLinkedNode(key, value);
map.put(key, newNode);
size++;
addTohead(newNode);
if(size > capacity){
// 删除尾部节点 ,删除 <key , node>
DLinkedNode tail = removeTail();
map.remove(tail.key);
size--;
}
}
else{
// 如果已经有了值,直接覆盖掉,移到队列头部
// 相当于,(覆盖)先删除node ,再移动到队列头部
removeNode(node); // 易错2 -容易漏掉删除
addTohead(node);
node.value = value;
}
}
// 删除尾部的节点
public DLinkedNode removeTail(){
DLinkedNode res = tail.prev; //注意这里删除的尾部节点,并不是真正的tail,因为只是假的尾部节点
removeNode(res);// 易错3 -容易直接删除tail节点
return res;
}
// 删除某个节点
public void removeNode(DLinkedNode node){
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 把节点放到队列头部
public void addTohead(DLinkedNode node){
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
}
/**
* 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);
*/
3.结果
4.题外话
LRU(Least Recently Used)本身这是操作系统的页面置换算法,每次要置换页面的时候,选择把最近都没有使用的页面给换掉(优先级低),最近使用到的页面优先级高。在数据库缓存中也常常用到LRU缓存,因为当数据量很大时,如果把全部数据都存下来,内存可能不太够,并且如果全部存下来所有数据,这样查找的时候,时间复杂度也比较大。这时候选取某种优先级策略,每次只存少量优先级高的数据,提高效率,并且减少占用内存。学科之间的知识是可以相通的!感谢旷世面试官。