LRU快取機制
題目來源:
力扣(LeetCode)
題目名稱:
LRU快取機制
題目描述:
運用你所掌握的資料結構,設計和實現一個 LRU (最近最少使用) 快取機制。它應該支援以下操作: 獲取資料 get 和 寫入資料 put 。
獲取資料 get(key)——如果關鍵字 (key) 存在於快取中,則獲取關鍵字的值(總是正數),否則返回-1。寫入資料 put(key, value)——如果關鍵字已經存在,則變更其資料值;如果關鍵字不存在,則插入該組「關鍵字/值」。當快取容量達到上限時,它應該在寫入新資料之前刪除最久未使用的資料值,從而為新的資料值留出空間。
進階: 你是否可以在 O(1) 時間複雜度內完成這兩種操作?
題目分析:
根據題目中所描述的儲存key-value對,我們應該想到雜湊表,因為雜湊表可以在O(1)時間內通過鍵找到值。題目的難點在於要維護資料訪問及插入的時間順序,對於有出入順序的問題,首先我們要想到棧、佇列以及連結串列,但是題目要求每次get()或put()更新資料後,該資料要被設定為最新訪問的資料,所以這就意味著兩點:
① 資料能夠被隨機訪問
② 需要把資料插入到頭部或尾部
我們知道連結串列可以快速移動其中的節點位置,而棧和佇列不能,所以我們選擇使用連結串列,但是如何實現隨機訪問呢?
考慮到雜湊表可以實現時間複雜度為O(1)的隨機訪問,如果雜湊表的value包含我們要訪問資料的位置資訊,那麼我們就可以在O(1)時間內快速訪問連結串列。
最後,由於連結串列記錄了訪問的時間順序,所以連結串列中的資訊必須儲存key,這樣才能通過鍵找到雜湊表的專案,進而實現刪除要求。
綜上所述,LRU 快取機制可以通過雜湊表輔以雙向連結串列實現,我們用一個雜湊表和一個雙向連結串列維護所有在快取中的鍵值對。
演算法設計:
雙向連結串列按照被使用的順序儲存了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。
在雙向連結串列的實現中,使用一個偽頭部(head)和偽尾部(tail)標記界限,這樣在新增節點和刪除節點的時候就不需要檢查相鄰的節點是否存在。
雜湊表即為普通的雜湊對映(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) 時間內完成。
示例:
LRUCache cache = new LRUCache( 2 /* 快取容量 */ ); cache.put(1, 1); cache.put(2, 2); cache.get(1); // 返回 1 cache.put(3, 3); // 該操作會使得關鍵字 2 作廢 cache.get(2); // 返回 -1 (未找到) cache.put(4, 4); // 該操作會使得關鍵字 1 作廢 cache.get(1); // 返回 -1 (未找到) cache.get(3); // 返回 3 cache.get(4); // 返回 4
程式執行過程:
演算法實現:
package com.ruoli; /** * * @author ss * */ import java.util.HashMap; import java.util.Map; class LRUCache{ // 連結串列節點 class DlinkedNode{ int key; int value; DlinkedNode next; DlinkedNode prev; public DlinkedNode() {} public DlinkedNode(int key, int value) { this.key = key; this.value = value; } } private Map<Integer, DlinkedNode> cache = new HashMap<Integer, LRUCache.DlinkedNode>(); private int capacity; private int size; private DlinkedNode head, tail; public LRUCache(int capacity){ this.size = 0; this.capacity = capacity; // 初始化偽頭部和偽尾部節點 this.head = new DlinkedNode(); this.tail = new DlinkedNode(); head.next = tail; tail.prev = head; } public int get(int key) { DlinkedNode node = cache.get(key); // 如果cache中沒有,則返回-1 if (node == null) { return -1; }else { // 如果有,則將節點移動到連結串列頭 moveToHead(node); } return node.value; } public void put(int key, int value) { DlinkedNode node = cache.get(key); // 如果雜湊cache中沒有 if (node == null) { // 建立此節點 DlinkedNode newNode = new DlinkedNode(key, value); // 新增資訊到雜湊表 cache.put(key, newNode); // 將節點插入到連結串列頭 addToHead(newNode); size++; // 如果此時超出了cache的容量 if (size > capacity) { // 移除連結串列尾節點 DlinkedNode temp = removeTail(); // 根據尾節點的key,刪除對應的雜湊表項 cache.remove(temp.key); size--; } }else { node.value = value; moveToHead(node); } } // 移動到連結串列頭 private void moveToHead(DlinkedNode node) { removeNode(node); addToHead(node); } // 在連結串列頭插入節點 private void addToHead(DlinkedNode node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } // 移除尾結點 private DlinkedNode removeTail() { DlinkedNode node = tail.prev; removeNode(node); return node; } // 刪除節點 private void removeNode(DlinkedNode node) { node.prev.next = node.next; node.next.prev = node.prev; } } public class Hundred_146 { public static void main(String[] args) { LRUCache cache = new LRUCache( 2 /* 快取容量 */ ); cache.put(1, 1); cache.put(2, 2); System.out.println(cache.get(1)); // 返回 1 cache.put(3, 3); // 該操作會使得關鍵字 2 作廢 System.out.println(cache.get(2)); // 返回 -1 (未找到) cache.put(4, 4); // 該操作會使得關鍵字 1 作廢 System.out.println(cache.get(1)); // 返回 -1 (未找到) System.out.println(cache.get(3)); // 返回 3 System.out.println(cache.get(4)); // 返回 4 } }
執行結果:
&n