基於 HashMap 和 雙向鏈表實現 LRU 算法

LRU原理

假設內存只能容納3個頁大小,按照 7 0 1 2 0 3 0 4 的次序訪問頁。假設內存按照棧的方式來描述訪問時間,在上面的,是最近訪問的,在下面的是,最遠時間訪問的,LRU就是這樣工作的。

但是如果讓我們自己設計一個基於 LRU 的緩存,這樣設計可能問題很多,這段內存按照訪問時間進行了排序,會有大量的內存拷貝操作,性能差。

那麼如何設計一個LRU緩存,使得放入和移除都是 O(1) ?

我們需要把訪問次序維護起來但是不能通過內存中的真實排序來反應,有一種方案就是使用雙向鏈表

基於 HashMap 和 雙向鏈表實現 LRU 算法

設計思路

使用 HashMap 存儲 key,使得 save 和 get key的時間複雜度都是 O(1),將HashMap 的 value 指向雙向鏈表實現的 LRU 的 Node 節點。如下圖所示:

原理演示

LRU 存儲是基於雙向鏈表實現的。其中 head 代表表頭,tail 代表尾部。預先設置 LRU 的容量,若存滿,可通過 O(1) 的時間淘汰掉雙向鏈表的尾部。每次新增和訪問數據,把新的節點增加到鏈表頭部(都可以通過 O(1)的效率),或者把已經存在的節點移動到鏈表頭部。

下面展示了capacity=3 的LRU,在存儲和訪問過程中的變化。爲了簡化圖複雜度,圖中沒有展示 HashMap部分的變化,僅僅演示了上圖 LRU 雙向鏈表的變化。

我們對這個LRU緩存的操作序列如下:

save("key1", 7)

save("key2", 0)

save("key3", 1)

save("key4", 2)

get("key2")

save("key5", 3)

get("key2")

save("key6", 4)

相應的 LRU 雙向鏈表部分變化如下:

核心步驟總結

  1. save(key, value)——首先在 HashMap 找到 key 對應的節點,(a)如果節點存在,更新節點的值,並把這個節點移動到隊頭。(b)如果不存在,需構造新的節點,並且嘗試把節點塞到隊頭。(c)如果LRU空間不足,則通過 tail 淘汰掉隊尾的節點,同時在 HashMap 中移除 key。
  2. get(key)——通過 HashMap 找到 LRU 鏈表節點,因爲根據LRU 原理,這個節點是最新訪問的,所以要把節點插入到隊頭,然後返回緩存的值。
  3. 完整基於 Java 的代碼參考如下

    class DLinkedNode {
    	String key;
    	int value;
    	DLinkedNode pre;
    	DLinkedNode post;
    }

    LRU Cache

    public class LRUCache {
       
        private Hashtable<Integer, DLinkedNode>
                cache = new Hashtable<Integer, DLinkedNode>();
        private int count;
        private int capacity;
        private DLinkedNode head, tail;
    
        public LRUCache(int capacity) {
            this.count = 0;
            this.capacity = capacity;
    
            head = new DLinkedNode();
            head.pre = null;
    
            tail = new DLinkedNode();
            tail.post = null;
    
            head.post = tail;
            tail.pre = head;
        }
    
        public int get(String key) {
    
            DLinkedNode node = cache.get(key);
            if(node == null){
                return -1; // should raise exception here.
            }
    
            // move the accessed node to the head;
            this.moveToHead(node);
    
            return node.value;
        }
    
    
        public void set(String key, int value) {
            DLinkedNode node = cache.get(key);
    
            if(node == null){
    
                DLinkedNode newNode = new DLinkedNode();
                newNode.key = key;
                newNode.value = value;
    
                this.cache.put(key, newNode);
                this.addNode(newNode);
    
                ++count;
    
                if(count > capacity){
                    // pop the tail
                    DLinkedNode tail = this.popTail();
                    this.cache.remove(tail.key);
                    --count;
                }
            }else{
                // update the value.
                node.value = value;
                this.moveToHead(node);
            }
        }
        /**
         * Always add the new node right after head;
         */
        private void addNode(DLinkedNode node){
            node.pre = head;
            node.post = head.post;
    
            head.post.pre = node;
            head.post = node;
        }
    
        /**
         * Remove an existing node from the linked list.
         */
        private void removeNode(DLinkedNode node){
            DLinkedNode pre = node.pre;
            DLinkedNode post = node.post;
    
            pre.post = post;
            post.pre = pre;
        }
    
        /**
         * Move certain node in between to the head.
         */
        private void moveToHead(DLinkedNode node){
            this.removeNode(node);
            this.addNode(node);
        }
    
        // pop the current tail.
        private DLinkedNode popTail(){
            DLinkedNode res = tail.pre;
            this.removeNode(res);
            return res;
        }
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章