哈希表(3)_LC:146

2.LC146(LRU缓存机制):哈希表 + 双向链表

LRU(Least Recently Used):

最近最少使用,是一种常用的页面置换算法(分页是磁盘和内存间传输数据块的最小单位),选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。(LRU操作的是一短内存)

 

 

146. LRU 缓存机制 - 力扣(LeetCode) (leetcode-cn.com)

 

难度:中等

运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制 。

实现 LRUCache 类:

  • LRUCache(int capacity):以正整数作为容量 capacity 初始化 LRU 缓存
  • int get(int key):如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value):如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

 

进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?

示例:

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4

提示:

1 <= capacity <= 3000

0 <= key <= 3000

0 <= value <= 10^4

最多调用 3 * 10^4 次 get 和 put

 

思路:

使用双向链表 + HashMap:

双向链表:用于记录数据(key, value)被操作的顺序(操作有:存放put和查找get),双向链表的首尾为(head, tail)

HashMap:用于查询链表中是否存在我们需要找到的数据,HashMap可以使得get和put操作(key, value)的时间复杂度为O(1)

LRU的整体结构为:HashMap,双向链表DoubleLinkList也中有key、value属性

因此put及get操作的整体顺序为:对于一个key,先用HashMap查询是否存在,然后再对key对应的value,即DoubleLinkList,操作双向链表中对应节点的位置和key、value值

LRU的结构图如下:

LRU中数据的存储和对使用时间新旧的维护是由 双向链表 实现的:

  1. 在链表头的是最新使用的。
  2. 在尾部的是最旧的,也是下次要清除的
  3. 如果加入的值是链表内存在的则要移动到头部。

HashMap是来配合双向链表,用于减少时间复杂度的:它是可以在O(1)的时间判断出链表中某个值是否存在。(否则需要遍历双向链表,时间复杂度为O(n), n为链表长度)。使用HashMap判断某个值存在后,可以在O(1)的时间获得它在双链表中的节点,以此节点的父节点。

两个问题:

  • 可用队列吗?不行
    • 队列只能做到先进先出,但是重复用到中间的数据时无法把中间的数据移动到顶端。
  • 可以用单链表吗?不行
    • 单链表能实现将新来的数据放头部,最久不用的在尾部删除。
    • 但删除和查询的时候需要遍历整个链表,效率很低。这时HashMap的作用就出来了,可以在O(1)的时间里判断key的值是否存在,value直接存储链表的节点对象,能直接定位删除对应的节点(将此节点的父节点指向此节点的子节点,并将此节点的子节点指向此节点的父节点)。要通过一个节点直接获得其父节点的话,通过单链表是不行的。这时双向链表的作用就体现出来了,能直接定位一个节点的父节点, 效率就很高。而且由于双向链表有尾指针,所以剔除最后的尾节点也十分方便。

代码:

class DoubleLinkList{
    //不要设成private权限,否则在类LRUCache中无法调用这些属性
    int key;
    int value;
    DoubleLinkList prev;
    DoubleLinkList next;

    public DoubleLinkList(int key, int value){
        this.key = key;
        this.value = value;
    }
}

class LRUCache {
    private int capacity;
    private HashMap<Integer, DoubleLinkList> map = null;
    private DoubleLinkList head = new DoubleLinkList(0,0); //链表头结点
    private DoubleLinkList tail = new DoubleLinkList(0,0); //链表尾结点
                                                           //一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器
                                                           //因此这里要加上0,0
    public LRUCache(int capacity) {
        map = new HashMap<>();
        //这两行必须写,否则无法构造出双向链表的结构!!
        head.next = tail;
        tail.prev = head;
        this.capacity = capacity; //任意漏掉
    }

    public int get(int key) {
        if(map.containsKey(key)){
            DoubleLinkList temp = map.get(key); //从map中得到key对应的value,value就是双向链表中的一个结点
            RemoveNodeToFront(temp); //将该结点移至双向链表的最前端(head结点的后一位)
            return temp.value;
        }else{
            return -1;
        }
    }

    public void put(int key, int value) {
        DoubleLinkList newFront = new DoubleLinkList(key, value);
        //put过程:
        //先查看双向链表的结点中是否有key这个结点:
        //如果有,直接替换,然后将该结点插入到head结点后面
        //如果没有,则判断map的大小和容量的关系:
        //当<容量时,直接将新结点插入到head结点的后一位
        //当>=容量时,将tail的前一个结点删除,然后将新结点插入到head结点的后面
        if(map.containsKey(key)){
            DoubleLinkList temp = map.get(key);
            temp.value = value;
            RemoveNodeToFront(temp);
            map.put(key, temp); //不要写成:map.put(key, value);
        }else{
            if(map.size() < capacity){ //因为要留一个位置给新结点,所以不能等于capacity
                RemoveNewNodeToFront(newFront);
                map.put(key, newFront); //不要写成:map.put(key, value);
            }else{
                DoubleLinkList lastNode = tail.prev;
                DeleteLastNode(lastNode);
                RemoveNewNodeToFront(newFront);
                map.remove(lastNode.key); //将双向链表中移除的结点,在map中也同样移除
                map.put(key, newFront); //不要写成:map.put(key, value);
            }
        }
    }

    //将双向链表中的某个有效结点(即除head和tail之外的结点)移至双向链表head结点的后一位
    private void RemoveNodeToFront(DoubleLinkList node){
        node.prev.next = node.next;
        node.next.prev = node.prev;
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    //将新添加到双向链表中的结点移至双向链表head结点的后一位
    private void RemoveNewNodeToFront(DoubleLinkList node){
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    //删除双向链表中tail的前一个结点
    private void DeleteLastNode(DoubleLinkList node){
        node.prev.next = tail;
        node.next.prev = node.prev;
    }
}

/**
 * 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);
 */

复杂度分析:

时间复杂度:对于 put 和 get 都是 O(1)

空间复杂度:O(capacity),因为哈希表和双向链表最多都需要O(capacity)空间复杂度

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值