算法通关村第五关-黄金挑战LRU问题

大家好我是苏麟 , 今天聊聊LRU问题 , 相信学过操作系统的小伙伴并不陌生 .

LRU问题

LRU的含义

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

图解 :

算法通关村第五关-黄金挑战LRU问题_第1张图片

如果再有其他元素就依次类推。

LRU缓存

描述 :

设计和构建一个“最近最少使用”缓存,该缓存会删除最近最少使用的项目。缓存应该从键映射到值(允许你插入和检索特定键对应的值),并在初始化时指定最大容量。当缓存被填满时,它应该删除最近最少使用的项目。

它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

题目 :

LeetCode LRU缓存 :

面试题 16.25. LRU 缓存

算法通关村第五关-黄金挑战LRU问题_第2张图片

分析 :

目前公认最好的方式是使用Hash+双链表 :

Hash的作用是 用来做到O(1)访问元素,哈希表就是普通的哈希映射 (HashMap),通过缓存数据的键映射到其在双向链表中的位置。Hash里的数据是key-value结构。value就是我们自己封装的node , key则是键值,也就是在Hash的地址 .

算法通关村第五关-黄金挑战LRU问题_第3张图片

这里的头节点 , 尾节点起到辅助的作用 , 并没有什么实际意义 , 相当于虚拟节点 . 

算法通关村第五关-黄金挑战LRU问题_第4张图片

举例 :

我们先看容量为3的例子,首先缓存了1,此时结构如图a所示。之后再缓存2和3,结构如b所示。

算法通关村第五关-黄金挑战LRU问题_第5张图片

之后 4再进入,此时容量已经不够了,只能将最远未使用的元素1删掉,然后将4插入到链表头部。此时就变成了上图c的样子。
接下来假如又访问了一次2,会怎么样呢? 此时会将2移动到链表的首部,也就是下图d的样子。

算法通关村第五关-黄金挑战LRU问题_第6张图片

之后假如又要缓存5呢? 此时就将tail指向的3删除,然后将5插入到链表头部。也就是上图e的样子。上面的方案要实现是非常容易的 . 要注意链表的几个操作 :

.假如容量没满,则将新元素直接插入到链表头就行了

2.如果容量够了,新的元素到来,则将tail指向的表尾元素删除就行了

3.假如要访问已经存在的元素,则此时将该先从链表中删除,再插入到表头就行了

再看Hash的操作:
1.Hash没有容量的限制,凡是被访问的元素都会在Hash中有个标记,key就是我们的查询条件,而value就是链表的结点的引用,可以不用访问链表直接定位到某个结点,然后就可以执行我们在上一节提到的方法来删除对应的结点
2.这里双向链表的删除好理解,那HashMap中是如何删除的呢? 其实就是将node变成为null。这样get(key)的时候返回的是null,就实现了删除的功能
上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成[删除该节点]和[在双向链表的头部添加节点]两步操作,都可以在 O(1)时间内完成

方法 : 

  • moveNode() 删除节点
  • removetail() 删除尾节点
  • addNode() 添加头节点
  • moveToHead() 移动到头节点
  • get() 得到节点的值
  • put() 插入节点

解析 :

class LRUCache {

    class Node{
        int key;
        int val;
        Node pre;
        Node next;

        public Node(){

        }
        public Node(int key , int val){
            this.key = key;
            this.val =val;
        }
    }

    private Map map = new HashMap<>();
    private Node head;
    private Node tail;
    private int size;
    private int capacity;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        head = new Node();
        tail = new Node();
        head.next = tail;
        tail.pre = head;
    }
    
    public int get(int key) {
        Node temp = map.get(key);
        if(temp == null){
            return -1;
        }
        moveToHead(temp);
        return temp.val;
    }
    
    public void put(int key, int value) {
        Node temp = map.get(key);
        if(temp == null){
            Node node = new Node(key,value);
            map.put(key,node);
            addNode(node);
            size++;
            if(size > capacity){
                Node delete = removetail();
                map.remove(delete.key);
                size--;
            }
        }else{
            temp.val = value;
            moveToHead(temp);
        }
    }
    
    //移动到头节点
    public void moveToHead(Node move){
        moveNode(move);
        addNode(move);
    }

    //添加头节点
    public void addNode(Node temp){
        temp.next = head.next;
        temp.pre = head;
        head.next.pre = temp;
        head.next = temp;
    }


    //删除尾节点并返回删除的节点
    public Node removetail(){
        Node temp = tail.pre;
        moveNode(temp);
        return temp;
    }

    //删除节点
    public void moveNode(Node temp){
        temp.pre.next = temp.next;
        temp.next.pre = temp.pre;
    }
}

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

这期就到这里 , 下期见!

你可能感兴趣的:(算法村,java,数据结构,算法,链表,hash)