1. 程式人生 > 其它 >LRU (最近最少使用演算法) 快取

LRU (最近最少使用演算法) 快取

技術標籤:演算法

目標

1. 該快取通過構造引數限制最大快取數量 capacity,當快取容量 size 達到上限時,它應該在寫入新資料之前刪除最久未使用的資料值,從而為新的資料值留出空間。

2. GET 以及 PUT 操作都滿足時間複雜度O(1)。

思路

為了知道最不經常使用快取資料是哪個,我們可以用連結串列來儲存,接近頭部的是最近剛使用的,接近尾部的是最不經常使用的。該快取還要刪除最近不經常使用的節點,需要使用雙向連結串列更方便。

GET 以及 PUT 都要滿足時間複雜度O(1),唯一可以想到的就是HashMap了。

在刪除尾部節點時,也要刪除 HashMap 裡的鍵值對,所以,節點中不僅要儲存value,也需要儲存 key。

為了操作方便,我們給雙向連結串列給定一個預設的head節點和一個tail節點。

HashMap在這裡主要角色是快取容器,雙向連結串列則是用來記錄快取資料是否最近使用過。

具體操作

對於 get 操作,先判斷 key 是否存在 HashMap中

1. key 存在,通過 HashMap 獲取到對應的 node, 把 node 移動到第一個節點 (head.next),並返回該節點儲存的值。

2. key不存在,則返回null。

對於 put 操作,先判斷 key 是否存在 HashMap中

1. 如果 key 存在,通過 HashMap 獲取到對應的 node,更新該節點的值。並且把node移動到第一個節點 (head.next)。

2. 如果key不存在:

  • 如果 size < capacity,用 key 和 value 生成一個新的node,插入到雙向連結串列的頭部,並將容量加一。
  • 如果 size >=capacity, 把尾部的節點(tail.pre)刪除掉。然後用 key 和 value生成一個新的 node,插入到雙向連結串列的頭部。

實現

import java.util.HashMap;
import java.util.Map;

public class LRUCache<K, V> {

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

    class CacheNode {
        CacheNode pre;
        CacheNode next;
        K key;
        V value;
        public CacheNode() {}
        public CacheNode(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.size = 0;
        head.next = tail;
        tail.pre = head;
    }

    public V get(K key) {
        CacheNode node = map.get(key);
        if (node == null) {
            return null;
        }
        moveToHead(node);
        return node.value;
    }

    public void put(K key, V value) {
        CacheNode node = map.get(key);
        if (node == null) {
            CacheNode newNode = new CacheNode(key, value);
            addToHead(newNode);
            map.put(key, newNode);
            size++;
            if (size > capacity) {
                removeTail();
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }

    private void removeTail() {
        CacheNode lastNode = tail.pre;
        map.remove(lastNode.key);
        removeNode(lastNode);
        size--;
    }

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

    private void removeNode(CacheNode node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    private void addToHead(CacheNode node) {
        node.next = head.next;
        head.next.pre = node;
        node.pre = head;
        head.next = node;
    }
}

如果需要在多執行緒下使用快取,則還需要在 get 和 put 的方法上加上 synchronized 修飾符,保證執行緒安全。

    public synchronized V get(K key) {
        CacheNode node = map.get(key);
        if (node == null) {
            return null;
        }
        moveToHead(node);
        return node.value;
    }

    public synchronized void put(K key, V value) {
        CacheNode node = map.get(key);
        if (node == null) {
            CacheNode newNode = new CacheNode(key, value);
            addToHead(newNode);
            map.put(key, newNode);
            size++;
            if (size > capacity) {
                removeTail();
            }
        } else {
            node.value = value;
            moveToHead(node);
        }
    }