每日一道算法题——LRU缓存机制

点击查看原题——146. LRU缓存机制

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

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

进阶:

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

LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

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)]
cache.put(3, 3);    // 该操作会使得密钥 2 作废
//cache[(3,3),(1,1)]
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 该操作会使得密钥 1 作废
//cache [(4,4),(3,3)]
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
// cache[(3,3),(4,4)]
cache.get(4);       // 返回  4
//cache[(4,4),(3,3)]

分析

LRU算法实际上是:最近操作的数据位于数据结构的队首。并且put和get操作的时间复杂度均是O(1)。

实际上这个Cache的特点:查找快、删除快、插入快、有顺序。

  1. HashMap除了无序,其他3个特点均满足;
  2. 链表是有序的,并且删除和插入的时间复杂度均为O(1)。虽然HashMap可以快速定位到Node节点,但单链表中只保存了一个指针,无法进行删除操作。所以使用双向链表,即使定位到一个节点,也可以确定其前节点和后节点。
LRU的核心思想.png

故最终的存储结构是哈希表与双向链表的结合

  • 哈希表定位Node节点:解决链表的查找缓慢问题;
  • 双向链表存储Value:解决哈希表的无序问题;

哈希表存储key,用于定位到链表汇总的value。那么Node中为什么还要存储key呢?

因为Node自我删除的时候,要去Map中删除对应的key。但若是Node只保存Value,没有保存key。找不到对应的Map数据。即Node节点也需要映射map。

代码实现

1. 创建Node节点。

public class LRUCache {
    static class Node {

        //链表数据域为key和val
        private int key, val;
        private Node next, prev;

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

2. 创建双向链表,并实现其中的API方法

public class LRUCache {
    static class DoubleList {
        private Node head, tail;  //头节点,尾节点
        private int size;
        //构建双向链表
        public DoubleList() {
            this.head = new Node(0, 0);
            this.tail = new Node(0, 0);
            head.next = tail;
            tail.prev = head;
            this.size = 0;
        }
        //链表头部插入数据
        public void addFirst(Node node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
            size++;
        }
        //删除链表中的node节点
        public void remove(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
            size--;
        }
        //删除链表的最后一个节点,并返回该节点
        public Node removeLast() {
            if (tail.prev == head) {
                return null;
            }
            //保存尾指针
            Node lastNode = tail.prev;
            tail.prev.prev.next = tail;
            tail.prev = tail.prev.prev;
            size--;
            return lastNode;
        }
        //返回链表长度
        public int size() {
            return size;
        }
    }
}

3. 实现LRU缓存

public class LRUCache {
    //记录 节点的位置
    HashMap map = new HashMap<>(16);
    //真正的缓存
    DoubleList cache = new DoubleList();
    //最大容量
    private int cap;
    public LRUCache(int capacity) {
        cap = capacity;
    }
    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        int val = map.get(key).val;
        //使用put方法将数据提前,先删后头插
        put(key, val);
        return val;
    }
    public void put(int key, int value) {
        //先new出新节点
        Node node = new Node(key, value);
        //若包含节点,则删除节点后,插入到头部
        if (map.containsKey(key)) {
            //删除Node节点
            cache.remove(map.get(key));
            cache.addFirst(node);
            //更新map中的值
            map.put(key, node);
        } else {
            //若没有包含头节点
            if (cap == cache.size()) {
                //删除链表的最后一个元素
                Node last = cache.removeLast();
                //注:若Node中值存储value,那么此处不能映射到map。
                map.remove(last.key);
            }
            //插入链表头部
            cache.addFirst(node);
            map.put(key, node);
        }
    }
}

实际上在Java中LinkedHashMap便是使用哈希表+双向链表实现的LRU思想。

你可能感兴趣的:(每日一道算法题——LRU缓存机制)