【Leetcode】LFU/LRU缓存——自定义双向链表

在做题之前首先对LFU算法和LRU算法有一定的了解。

1. LFU算法

1.1 原理

LFU(Least Frequently Used)算法,即最少访问算法,根据访问缓存的历史频率来淘汰数据,核心思想是“如果数据在过去一段时间被访问的次数很少,那么将来被访问的概率也会很低”。

1.2 数据结构

一般会维护两个数据结构:

  • 哈希:用来提供对外部的访问,查询效率更高;
  • 双向链表或队列:维护了对元素访问次数的排序

缓存操作导致的链表变化:

  • 添加新元素:新元素访问次数为1,放到队尾;

  • 缓存淘汰:从队尾开始淘汰,因为队尾元素的访问次数最少;

  • 访问缓存:访问缓存会增加元素的访问次数,所以元素在队列或双向链表中的位置会重新排序

2. LRU算法

2.1 原理

LRU(Least Recently Used)算法, 即最近最久未使用算法,它的设计原理就是,当数据在最近一段时间经常被访问,那么它在以后也会经常被访问。这就意味着,如果经常访问的数据,我们需要然其能够快速命中,而不常访问的数据,我们在容量超出限制内,要将其淘汰。

2.2 数据结构
  • HashMap:用于快速查找到结点所在位置,然后将使用到的结点放在对头,这样最近最少使用的结点自然就落入到队尾。
  • 双向链表:提供了良好的灵活性,两边可达。

3. 算法实现:LFU缓存

3.1 题目描述

题目链接:460. LFU 缓存

在这里插入图片描述
在这里插入图片描述

3.2 思路分析
(1)LinkedHashSet实现双向链表+哈希表

HashMap<Integer, Node> cache 存缓存的内容; min 是最小访问频次; HashMap<Integer, LinkedHashSet<Node>> freqMap 存每个访问频次对应的 Node 的双向链表(为了方便,直接用了 JDK 现有的 LinkedHashSet,其实现了 1 条双向链表贯穿哈希表中的所有 Entry,支持以插入的先后顺序对原本无序的 HashSet 进行迭代)

class LFUCache {
    Map<Integer, Node> cache;  // 存储缓存的内容
    Map<Integer, LinkedHashSet<Node>> freqMap; // 存储每个频次对应的双向链表
    int size;
    int capacity;
    int min; // 存储当前最小频次

    public LFUCache(int capacity) {
        cache = new HashMap<> (capacity);
        freqMap = new HashMap<>();
        this.capacity = capacity;
    }
    
    public int get(int key) {
        Node node = cache.get(key);
        if (node == null) {
            return -1;
        }
        freqInc(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (capacity == 0) {
            return;
        }
        Node node = cache.get(key);
        if (node != null) {
            node.value = value;
            freqInc(node);
        } else {
            if (size == capacity) {
                Node deadNode = removeNode();
                cache.remove(deadNode.key);
                size--;
            }
            Node newNode = new Node(key, value);
            cache.put(key, newNode);
            addNode(newNode);
            size++;     
        }
    }

    void freqInc(Node node) {
        // 从原freq对应的链表里移除, 并更新min
        int freq = node.freq;
        LinkedHashSet<Node> set = freqMap.get(freq);
        set.remove(node);
        if (freq == min && set.size() == 0) { 
            min = freq + 1;
        }
        // 加入新freq对应的链表
        node.freq++;
        LinkedHashSet<Node> newSet = freqMap.get(freq + 1);
        if (newSet == null) {
            newSet = new LinkedHashSet<>();
            freqMap.put(freq + 1, newSet);
        }
        newSet.add(node);
    }

    void addNode(Node node) {
        LinkedHashSet<Node> set = freqMap.get(1);
        if (set == null) {
            set = new LinkedHashSet<>();
            freqMap.put(1, set);
        } 
        set.add(node); 
        min = 1;
    }

    Node removeNode() {
        LinkedHashSet<Node> set = freqMap.get(min);
        Node deadNode = set.iterator().next();
        set.remove(deadNode);
        return deadNode;
    }
}

class Node {
    int key;
    int value;
    int freq = 1;

    public Node() {}
    
    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}
(2)自定义双向链表

HashMap<Integer, Node> cache 存缓存的内容; min 是最小访问频次; HashMap<Integer, DoublyLinkedList>freqMap 存每个访问频次对应的 Node 的双向链表(与写法 1 一样,只不过将 JDK 自带的 LinkedHashSet 双向链表实现改成了自定义的双向链表 DoublyLinkedList,减少了一些哈希相关的耗时)

class LFUCache {
    Map<Integer, Node> cache; // 存储缓存的内容
    Map<Integer, DoublyLinkedList> freqMap; // 存储每个频次对应的双向链表
    int size;
    int capacity;
    int min; // 存储当前最小频次

    public LFUCache(int capacity) {
        cache = new HashMap<> (capacity);
        freqMap = new HashMap<>();
        this.capacity = capacity;
    }
    
    public int get(int key) {
        Node node = cache.get(key);
        if (node == null) {
            return -1;
        }
        freqInc(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (capacity == 0) {
            return;
        }
        Node node = cache.get(key);
        if (node != null) {
            node.value = value;
            freqInc(node);
        } else {
            if (size == capacity) {
                DoublyLinkedList minFreqLinkedList = freqMap.get(min);
                cache.remove(minFreqLinkedList.tail.pre.key);
                minFreqLinkedList.removeNode(minFreqLinkedList.tail.pre); // 这里不需要维护min, 因为下面add了newNode后min肯定是1.
                size--;
            }
            Node newNode = new Node(key, value);
            cache.put(key, newNode);
            DoublyLinkedList linkedList = freqMap.get(1);
            if (linkedList == null) {
                linkedList = new DoublyLinkedList();
                freqMap.put(1, linkedList);
            }
            linkedList.addNode(newNode);
            size++;  
            min = 1;   
        }
    }

    void freqInc(Node node) {
        // 从原freq对应的链表里移除, 并更新min
        int freq = node.freq;
        DoublyLinkedList linkedList = freqMap.get(freq);
        linkedList.removeNode(node);
        if (freq == min && linkedList.head.post == linkedList.tail) { 
            min = freq + 1;
        }
        // 加入新freq对应的链表
        node.freq++;
        linkedList = freqMap.get(freq + 1);
        if (linkedList == null) {
            linkedList = new DoublyLinkedList();
            freqMap.put(freq + 1, linkedList);
        }
        linkedList.addNode(node);
    }
}

class Node {
    int key;
    int value;
    int freq = 1;
    Node pre;
    Node post;

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

class DoublyLinkedList {
    Node head;
    Node tail;

    public DoublyLinkedList() {
        head = new Node();
        tail = new Node();
        head.post = tail;
        tail.pre = head;
    }

    void removeNode(Node node) {
        node.pre.post = node.post;
        node.post.pre = node.pre;
    }

    void addNode(Node node) {
        node.post = head.post;
        head.post.pre = node;
        head.post = node;
        node.pre = head;
    }
}
(3)存储频次的HashMap改为直接用双向链表

class LFUCache {
  Map<Integer, Node> cache;  // 存储缓存的内容,Node中除了value值外,还有key、freq、所在doublyLinkedList、所在doublyLinkedList中的postNode、所在doublyLinkedList中的preNode,具体定义在下方。
  DoublyLinkedList firstLinkedList; // firstLinkedList.post 是频次最大的双向链表
  DoublyLinkedList lastLinkedList;  // lastLinkedList.pre 是频次最小的双向链表,满了之后删除 lastLinkedList.pre.tail.pre 这个Node即为频次最小且访问最早的Node
  int size;
  int capacity;

  public LFUCache(int capacity) {
​    cache = new HashMap<> (capacity);
​    firstLinkedList = new DoublyLinkedList();
​    lastLinkedList = new DoublyLinkedList();
​    firstLinkedList.post = lastLinkedList;
​    lastLinkedList.pre = firstLinkedList;this.capacity = capacity;
  }
  
  public int get(int key) {Node node = cache.get(key);if (node == null) {return -1;}
    // 该key访问频次+1freqInc(node);return node.value;
  }
  
  public void put(int key, int value) {if (capacity == 0) {return;}Node node = cache.get(key);
    // 若key存在,则更新value,访问频次+1if (node != null) {
​      node.value = value;freqInc(node);} else {
      // 若key不存在if (size == capacity) {// 如果缓存满了,删除lastLinkedList.pre这个链表(即表示最小频次的链表)中的tail.pre这个Node(即最小频次链表中最先访问的Node),如果该链表中的元素删空了,则删掉该链表。
​        cache.remove(lastLinkedList.pre.tail.pre.key);
​        lastLinkedList.removeNode(lastLinkedList.pre.tail.pre);
​        size--;if (lastLinkedList.pre.head.post == lastLinkedList.pre.tail) {removeDoublyLinkedList(lastLinkedList.pre);}}
      // cache中put新Key-Node对儿,并将新node加入表示freq为1的DoublyLinkedList中,若不存在freq为1的DoublyLinkedList则新建。Node newNode = new Node(key, value);
​      cache.put(key, newNode);if (lastLinkedList.pre.freq != 1) {DoublyLinkedList newDoublyLinedList = new DoublyLinkedList(1);addDoublyLinkedList(newDoublyLinedList, lastLinkedList.pre);
​        newDoublyLinedList.addNode(newNode);} else {
​        lastLinkedList.pre.addNode(newNode);}
​      size++;}
  }

  /**
   * node的访问频次 + 1
   */
  void freqInc(Node node) {// 将node从原freq对应的双向链表里移除, 如果链表空了则删除链表。DoublyLinkedList linkedList = node.doublyLinkedList;DoublyLinkedList preLinkedList = linkedList.pre;
​    linkedList.removeNode(node);if (linkedList.head.post == linkedList.tail) {removeDoublyLinkedList(linkedList);}// 将node加入新freq对应的双向链表,若该链表不存在,则先创建该链表。
​    node.freq++;if (preLinkedList.freq != node.freq) {DoublyLinkedList newDoublyLinedList = new DoublyLinkedList(node.freq);addDoublyLinkedList(newDoublyLinedList, preLinkedList);
​      newDoublyLinedList.addNode(node);} else {
​      preLinkedList.addNode(node);}
  }

  /**
   * 增加代表某1频次的双向链表
   */
  void addDoublyLinkedList(DoublyLinkedList newDoublyLinedList, DoublyLinkedList preLinkedList) {
​    newDoublyLinedList.post = preLinkedList.post;
​    newDoublyLinedList.post.pre = newDoublyLinedList;
​    newDoublyLinedList.pre = preLinkedList;
​    preLinkedList.post = newDoublyLinedList; 
  }

  /**
   * 删除代表某1频次的双向链表
   */
  void removeDoublyLinkedList(DoublyLinkedList doublyLinkedList) {
​    doublyLinkedList.pre.post = doublyLinkedList.post;
​    doublyLinkedList.post.pre = doublyLinkedList.pre;
  }
}



class Node {
  int key;
  int value;
  int freq = 1;
  Node pre; // Node所在频次的双向链表的前继Node 
  Node post; // Node所在频次的双向链表的后继Node
  DoublyLinkedList doublyLinkedList;  // Node所在频次的双向链表

  public Node() {}

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

class DoublyLinkedList {
  int freq; // 该双向链表表示的频次
  DoublyLinkedList pre;  // 该双向链表的前继链表(pre.freq < this.freq)
  DoublyLinkedList post; // 该双向链表的后继链表 (post.freq > this.freq)
  Node head; // 该双向链表的头节点,新节点从头部加入,表示最近访问
  Node tail; // 该双向链表的尾节点,删除节点从尾部删除,表示最久访问

  public DoublyLinkedList() {
​    head = new Node();
​    tail = new Node();
​    head.post = tail;
​    tail.pre = head;
  }

  public DoublyLinkedList(int freq) {
​    head = new Node();
​    tail = new Node();
​    head.post = tail;
​    tail.pre = head;this.freq = freq;
  }

  void removeNode(Node node) {
​    node.pre.post = node.post;
​    node.post.pre = node.pre;
  }

  void addNode(Node node) {
​    node.post = head.post;
​    head.post.pre = node;
​    head.post = node;
​    node.pre = head;
​    node.doublyLinkedList = this;
  }
}
4.1 题目描述

题目链接:146. LRU 缓存

在这里插入图片描述

4.2 思路分析

首先,实现本题的两种操作,需要用到一个哈希表,一个双向链表。

(1)LinkedHashMap实现双向链表+哈希表

在Java语言中,有一种结合了哈希表和双向链表的数据结构 LinkedHashMap,只需要短短几行就可以实现本题。

实现代码如下:

class LRUCache {
    int capacity;
    LinkedHashMap<Integer, Integer> cache;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        cache = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return cache.size() > capacity;
            }
        };
    }
    
    public int get(int key) {
        return cache.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        cache.put(key, value);
    }
}
(2)自定义双向链表

但面试官一般希望同学能够自己实现一个简单的双向链表。这里就实现双向链表,HashMap还是直接调的数据结构。

LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。

  • 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
  • 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。

这样以来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)的时间内完成 get 或者 put 操作。具体的方法如下:

  • 对于 get 操作,首先判断 key 是否存在

    • 如果 key 不存在,则返回 −1;
    • 如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
  • 对于 put 操作,首先判断 key 是否存在

    • 如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;

    • 如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。

上述各项操作中,访问哈希表的时间复杂度为 O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)。而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1) 时间内完成。

class LRUCache {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;
        public DLinkedNode() {}
        public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
    }
    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }
    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

参考:

Java 13ms 双100% 双向链表 多解法超全😂

官方题解:LRU缓存机制

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值