java之双向链表与哈希表实现LRU缓存机制

背景

在研究双向队列之前,是遇到了这么一道题。
146. LRU缓存机制
这道题的思路是HashMap与双向队列的组合。LRU就是删除最近最少使用的数据值。
比如我先打开了支付宝,之后打开了手机淘宝,之后打开微信,此时在后台就是如下排列
在这里插入图片描述
之后我又点击支付宝,在后台中就会来到队友
在这里插入图片描述
假如缓存容量为3的话,此时又打开了时钟,就会把淘宝从缓存中删除,因为他是最近最少使用的应用。
在这里插入图片描述
那么这道题的思路很清晰了,就是使用一个队列,然后只要用户调用get函数去查找某个key(应用),就把这个key(键值对)放在队列头部,当用户向队列put新的键值对的时候,我们就要判断队列的长度是否超过缓存容量,不超过的话就把新的键值对插入队列的头部,超过的话就把队列尾部的键值对弹出,然后将新的键值对插入队列的头部。下面是代码形式的解释。

/* 缓存容量为 2 */
LRUCache cache = new LRUCache(2);
// 你可以把 cache 理解成一个队列
// 假设左边是队头,右边是队尾
// 最近使用的排在队头,久未使用的排在队尾
// 圆括号表示键值对 (key, val)

cache.put(1, 1);
// cache = [(1, 1)]
cache.put(2, 2);
// cache = [(2, 2), (1, 1)]
cache.get(1);       // 返回 1
// cache = [(1, 1), (2, 2)]
// 解释:因为最近访问了键 1,所以提前至队头
// 返回键 1 对应的值 1
cache.put(3, 3);
// cache = [(3, 3), (1, 1)]
// 解释:缓存容量已满,需要删除内容空出位置
// 优先删除久未使用的数据,也就是队尾的数据
// 然后把新的数据插入队头
cache.get(2);       // 返回 -1 (未找到)
// cache = [(3, 3), (1, 1)]
// 解释:cache 中不存在键为 2 的数据
cache.put(1, 4);    
// cache = [(1, 4), (3, 3)]
// 解释:键 1 已存在,把原始值 1 覆盖为 4
// 不要忘了也要将键值对提前到队头

解决方法

解决方法就是双向链表+哈希表,如下图所示。下图出处
在这里插入图片描述
具体为什么这么解决先放一放,先说下双向链表要实现什么功能。
1.在队头插入元素(每次get操作中都要插入数据头部),
2.删除队尾的元素(每次都要弹出队尾的元素)
3.删除元素(当要把当前的键值对插入到头部时,需要先删除之前的那个键值对)。

1.链式节点

这个节点需要包含key和val以及指向前一个节点的last与指向后一个节点的next。

/**
 * 双向链表中的节点类,存储key是因为我们在双向链表删除表尾的值时,只是返回了一个节点,
 * 所以这个节点要包括key值,这样我们的哈希表才可以删除对应key值的映射
 */
class DoubleNode {
    DoubleNode last;
    DoubleNode next;
    int key,val;
    public DoubleNode(int key,int val){
        this.val = val;
        this.key = key;
    }
}

2.双向链表

值得注意的是,我们需要两个链式节点来作为头和尾,同时在构造函数里,我们要创建这两个链式节点,并将二者连起来。
下面通过图例来解释各个方法,首先这是当前的双向链表。
在这里插入图片描述

addFirst方法

可以看到新的节点new要插入头部,首先要把node的next指向1,之后要把1的last指向new,之后把new的last指向head,最后把head的next指向new。O(1)时间复杂度
在这里插入图片描述

remove方法

简单来说就是将head和tail与1的联系删去,残忍。
让1前面的节点的next不指向1,而指向1后面的那个节点
让1后面的节点的last不指向1,而指向1前面的节点,这样就把1从链式结构中剔除出去了。
注意这里没有说head和tail是因为双向链表可能很长,删去1个节点,是连接上他前后的两个节点。
O(1)时间复杂度
在这里插入图片描述

removeLast方法

和remove方法相似,只不过最后一个后面的节点是确定的,就是tail节点。
在这里插入图片描述
通过图中了解了如何写这几个函数,接下来就看完整的代码吧。

/**
 * 双向链表,实现插入队头,删除队尾,删除元素功能
 */
class DoubleList {
    DoubleNode head;
    DoubleNode tail;
    int size = 0;
    public DoubleList()
    {
        head = new DoubleNode(0,0);
        tail = new DoubleNode(0,0);
        head.next = tail;
        tail.last = head;
    }
    //当一个值被get操作时,就把其放在队头
    public void addFirst(DoubleNode node)
    {
        node.next = head.next;
        head.next.last = node;
        node.last = head;
        head.next = node;
        size++;
    }
    //删除元素
    public void remove(DoubleNode node){
        node.last.next = node.next;
        node.next.last = node.last;
        size--;
    }
    //删除队尾的元素
    public  DoubleNode removeLast(){
        if (size==0)
            return null;
        DoubleNode node = tail.last;
        node.last.next = tail;
        tail.last = node.last;
        size--;
        return node;
    }
    public int size()
    {
        return  size;
    }
    public String toString()
    {
        String s = "";
        if(size==0)
            return  s;
        int length = size;
        DoubleNode node = head;
        while(length>0)
        {
            node = node.next;
            s += node.val;
            length--;
        }
        return s;
    }
}

最终代码

数据域要包括双向链表,一个哈希表。

get方法

首先判断哈希表中是否存在key,如果没有的话就返回-1,如果有的话就先将该key对应的节点插入到队列的队头(调用put方法,传入当前的key,与节点),然后返回该key对应的节点的value值。

put方法

1.首先根据key与节点的value值创建一个新的节点(DoubleNode)对象;
2.之后去判断当前key是否存在于map中,如果存在的话有两种情况1).是用户调用了get当前key,所以要使用put将当前key对应的节点放到队列头部;2).用户正常put(key,value),只不过当前put的key存在。无论是处于哪种情况,都要在双向链表删除原有key对应的节点,然后将新创建的节点插入队头,最后在map中更新key对应的节点(新节点)。
3.如果不存在当前map中,只能是用户put新的key与value,这种情况涉及判断当前链表的数量是否等于上限,如果等于上限,就把队列尾部的元素弹出,同时要将map中key与旧节点之间的映射删去,然后将新节点插入链表队头,最后在map中put key与新节点。

class LRUCache {
    DoubleList cache;
    Map<Integer,DoubleNode>map;
    int capacity;
    public LRUCache(int capacity) {
        this.capacity = capacity;
        cache = new DoubleList();
        map = new HashMap<>();
    }
    
    public int get(int key) {
        if(!map.containsKey(key))
            return -1;
        else{
            DoubleNode node = map.get(key);
            //把当前key对应的那个DoubleNode移动到队头
            put(key,node.val);
            return node.val;
        }
    }
    
    public void put(int key, int value) {
        DoubleNode newnode = new DoubleNode(key,value);
        //要调整位置插入至最前面(可能是调用了get这个key于是我们要把这个DoubleNode放到队尾,也可能是put了一个新值,只不过key相同,这时就要更改下key对应的value,在这里我们是直接换一个DoubleNode去插入)
        if(map.containsKey(key)){
          DoubleNode lastNode = map.get(key);
            cache.remove(lastNode);
            cache.addFirst(newnode);
            //不能忘记更新map
            map.put(key,newnode);
        }else{
            if(cache.size==capacity)
            {
                //这里不光要把队尾去掉,也要把map中的key和DoubleNode的映射去掉
                //这里也是为什么DoubleNode中要有key的原因,
               DoubleNode last  = cache.removeLast();
               map.remove(last.key);
            }
            cache.addFirst(newnode);
            map.put(key,newnode);
        }
    }
}

总结

1.这个题为什么要用双向链表,单向链表不可以么?
原因在于我要删除一个节点的时候,比较方便,因为知道当前节点的上一个节点与下一个节点。直接将二者连接起来即可。
2.为什么链式节点要包含key,如果单单是一个value不可以么
原因在于,当我们删除队尾节点时,我们只能返回这个节点,在map中也需要删除相应映射,所以链式节点必须要有key,否则map不知道去删除谁。
3.为什么map中的数据类型是<Integer,DoubleNode>,而不是<Integer,Integer>
其实问题在于为什么是key,DoubleNode而不是key,value。当执行put操作时,map中存在这个key的时候,我们需要在双向链表中删除掉当前这个节点,所以map中的应该是链式节点,而不是value。

扫描二维码关注公众号,回复: 8822938 查看本文章
发布了55 篇原创文章 · 获赞 28 · 访问量 9247

猜你喜欢

转载自blog.csdn.net/weixin_41796401/article/details/102754057