LRU演算法簡單實現

来源:https://www.cnblogs.com/afei688/archive/2022/10/20/16810640.html
-Advertisement-
Play Games

LRU:最近最少使用緩存 LRU是Least Recently Used的縮寫,即最近最少使用,是一種常用的頁面置換演算法,選擇最近最久未使用的頁面予以淘汰。該演算法賦予每個頁面一個訪問欄位,用來記錄一個頁面自上次被訪問以來所經歷的時間 t,當須淘汰一個頁面時,選擇現有頁面中其 t 值最大的,即最近最少 ...


LRU:最近最少使用緩存

LRU是Least Recently Used的縮寫,即最近最少使用,是一種常用的頁面置換演算法,選擇最近最久未使用的頁面予以淘汰。該演算法賦予每個頁面一個訪問欄位,用來記錄一個頁面自上次被訪問以來所經歷的時間 t,當須淘汰一個頁面時,選擇現有頁面中其 t 值最大的,即最近最少使用的頁面予以淘汰。(引自百度百科)

運用所掌握的數據結構,設計和實現一個 LRU (Least Recently Used,最近最少使用) 緩存機制

實現 LRUCache 類:

  • LRUCache(int capacity) 以正整數作為容量 capacity 初始化 LRU 緩存
  • int get(int key) 如果關鍵字 key 存在於緩存中,則返回關鍵字的值,否則返回 -1
  • void put(int key, int value) 如果關鍵字已經存在,則變更其數據值;如果關鍵字不存在,則插入該組「關鍵字-值」。當緩存容量達到上限時,它應該在寫入新數據之前刪除最久未使用的數據值,從而為新的數據值留出空間。

解題思路

LRU緩存機制對應的結構其實就是一個雙向鏈表,由於get和put方法必須是\(O(1)\)的時間複雜度,可以使用一個哈希表快速定位,找出緩存項在雙向鏈表中的位置,隨後將其移動到雙向鏈表的頭部(最近使用的排在隊頭,最久未使用的排在隊尾),即可在 \(O(1)\) 的時間內完成 get 或者 put 操作。

因此,LRU 演算法的核心數據結構就是哈希鏈表,即雙向鏈表和哈希表的結合體。

對於get操作,首先在哈希map中判斷key是否存在:

  • 如果key不存在,直接返回-1;
  • 如果key存在,則 key 對應的節點就是最近被使用的節點。通過哈希表定位到該節點在雙向鏈表中的位置,並將其移動到雙向鏈表的頭部,最後返回該節點的值。

對於put操作,首先也要在哈希 map 中判斷 key 是否存在:

  • 如果 key 不存在,使用 keyvalue 創建一個新的節點,在雙向鏈表的頭部添加該節點,並將 key 和該節點添加進哈希表中。然後判斷雙向鏈表是否滿了,如果超出了容量,則刪除雙向鏈表的尾部節點(把最久未使用的尾部結點給刪了,給新節點騰地兒),並刪除哈希表中對應的鍵值。最後把新加入的key添加到雙向鏈表的頭部和哈希表中;
  • 如果 key 存在,則與 get 操作類似,通過查詢哈希表得到key在雙向鏈表的位置,刪除該結點。最後把新加入的key添加到雙向鏈表的頭部和哈希表中;

在雙向鏈表初始化時,使用一個啞元頭部(dummy head)和啞元尾部(dummy tail)標記占位,這樣在添加節點和刪除節點的時候就不需要檢查相鄰的節點是否存在,防止空指針。

代碼實現

自定義雙向鏈表結構(面試推薦✔)

class LRUCache {
    
    // 自定義結點類型
    static class Node {
        private int key, val;
        private Node next, prev;
        
        public Node(int k, int v) {
            this.key = k;
            this.val = v;
        }
    }
    
    // 自定義雙向鏈表
    static class DoubleList {
        private Node head, tail;
        // 構造雙向鏈表(head和tail是啞元結點,占位用的)
        public DoubleList() {
            this.head = new Node(-1, -1);
            this.tail = new Node(-1, -1);
            head.next = tail;
            tail.prev = head;
        }
        
        // 在鏈表頭部添加結點(頭插法)
        public void addFirst(Node node) {
            node.prev = head;
            node.next = head.next;
            head.next.prev = node;
            head.next = node;
        }
        
        // 刪除指定結點Node
        public void remove(Node node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
        
        // 刪除鏈表尾節點,並返回該結點
        public Node removeLast() {
            Node node = tail.prev;
            remove(node);
            return node;
        }
    }
    
    private HashMap<Integer, Node> map;		// 輔助map
    private DoubleList cache;
    private int cap;	// 雙向鏈表的最大容量

    public LRUCache(int capacity) {
		map = new HashMap<>();
        cache = new DoubleList();
        this.cap = capacity;
    }
    
    public int get(int key) {
		if(! map.containsKey(key))	return -1;	// map中不存在key,get不到了
        Node node = map.get(key);
        cache.remove(node);
        cache.addFirst(node);
        return node.val;
    }
    
    public void put(int key, int value) {
        // 要添加的結點封裝成一個node
        Node node = new Node(key, value);
        if(map.containsKey(key)) {
            cache.remove(map.get(key));		// 已經有這個key了,舊值刪了,新值頭部添加操作在底下
        } else if(map.size() >= cap) {
            // 雙端鏈表滿了,則把最久未使用的尾部結點給刪了,給新節點騰地兒
            Node lastNode = cache.removeLast();
            map.remove(lastNode.key);
        }
        // 鏈表和map同步添加
        cache.addFirst(node);
        map.put(key, node);
    }
    
    // 測試
    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(2);	// 初始化雙向鏈表和map
        lruCache.put(1, 1);
        lruCache.put(2, 2);
        lruCache.get(1);
        lruCache.put(3, 3);
        lruCache.get(2);
        System.out.println(lruCache.map);
    }
}

Java 語言中,同樣有類似的數據結構 LinkedHashMap,內部已經封裝好添加查詢的方法。但是面試時不推薦使用,還是推薦使用上面這種方式。

class LRUCache {

    private int cap;
    private LinkedHashMap<Integer, Integer> cache;

    public LRUCache(int capacity) {
        this.cap = capacity;
        this.cache = new LinkedHashMap<>()
    }

    // 使用了key,就得設這個key為最近使用
    public int get(int key) {
        if(! cache.containsKey(key)) {
            return -1;
        }
        makeRecently(key);
        return cache.get(key);
    }

    public void put(int key, int value) {
        // key 存在於鏈表中
        if(cache.containsKey(key)) {
            cache.put(key, value);    // 修改 key 的值
            makeRecently(key);      // 將 key 變為最近使用
            return;
        }
        // 插入元素前需要判斷鏈表容量是否已滿,淘汰最久未使用的key
        if(cache.size() >= this.cap) {
            // 鏈表頭部就是最久未使用的 key
            int oldestKey = cache.keySet().iterator().next();
            cache.remove(oldestKey);
        }
        // 將新的 key 添加鏈表尾部
        cache.put(key, value);
    }

    // 設置key為最近使用,key先刪了移到鏈表尾
    private void makeRecently(int key) {
        int val = cache.get(key);
        // 刪除 key,重新插入到隊尾
        cache.remove(key);
        cache.put(key, val);
    }

    // 測試
    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(2);
        lruCache.put(1,1);
        lruCache.put(2,2);
        lruCache.get(1);
        lruCache.put(3,3);
        lruCache.get(2);
        System.out.println(cache);
    }

}

複雜度

  • 時間複雜度:put 和 get 操作的平均時間複雜度都為\(O(1)\)
  • 空間複雜度:\(O(capacity)\),哈希表和雙向鏈表中最多存儲 capacity 個元素。

參考

labuladong:https://leetcode.cn/problems/lru-cache/solutions/12711/lru-ce-lue-xiang-jie-he-shi-xian-by-labuladong/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在上一篇中,我們一起分析了 VS Code 整體的代碼架構,瞭解了 VS Code 是由前後端分離的方式開發的。且無論前端是基於 electron 還是 web,後端是本地還是雲端,其調用方式並無不同。 這樣的架構下,前後端的通信方式是如何實現的呢?本篇我們將一起來探究 VS Code For We ...
  • 每年的蘋果新產品發佈,其官網都會配套更新相應的單頁滾動產品介紹頁。其中的動畫特效都非常有意思,今年 iPhone 14 Pro 的介紹頁不例外。 最近,剛好有朋友問到,其對官網的一段文字特效特別感興趣,看適用簡單卻不知從何下手,我們來看看: 整個動畫大致是,隨著頁面的向下滾動,整個文字從無到出現,再 ...
  • call,apply,bind作為改變this指向的法寶,那麼它們是怎麼做到的呢,接下來嘗試邊分析、邊構造: 我們先來構造一個mycall骨架,把功能添加到原型鏈 讓函數依附於某個對象,並且以對象方法的方式執行,以此來改變this指向,需要註意: 為了避免不必要的覆蓋,我們使用Symbol作為key ...
  • 近期,數據中心系統負荷大,mysql伺服器的CPU動輒達到90%以上。代碼和數據表存在很大優化空間。 這裡分享一個定時任務批量處理數據的優化過程。 先介紹定時任務 先介紹下麵2張數據表 欄位 數據量 platform_order 平臺交易訂單表 有超過50多個欄位。 包括 主鍵自增id、客戶id、客 ...
  • 前言 嗨嘍~大家好呀,這裡是魔王吶 ! 國企文員和游戲陪玩兩個職業間,你會選擇哪個? 00後李明的答案是後者。 今年3月,某二本院校應屆畢業生李明,兜兜轉轉,沒有找到特別合心的工作 卻憑著還不錯的游戲技術,成為了全職的游戲陪玩。 “按單收費,大概一單大概兩三百元,按時長收費,一小時50到100元”, ...
  • 滿漢樓02 4.功能實現04 4.6顯示所有菜品 4.6.1思路分析 創建一個菜單表menu,在Domain層創建與菜單表對應的Javabean-Menu類,在DAO層創建MenuDAO,完成對menu表的增刪改查,在Service層創建一個和menu表相關的service類,service類提供給 ...
  • 在筆者之前的文章`《驅動開發:內核特征碼搜索函數封裝》`中我們封裝實現了特征碼定位功能,本章將繼續使用該功能,本次我們需要枚舉內核`LoadImage`映像回調,在Win64環境下我們可以設置一個`LoadImage`映像載入通告回調,當有新驅動或者DLL被載入時,回調函數就會被調用從而執行我們自己... ...
  • 集合 Scala的集合有三大類: 序列Seq、集Set、映射Map 所有的集合都擴展自Iterable特質。對於幾乎所有的集合類 Scala都同時提供了可變和不可變的版本 可變集合 可以在適當的地方被更新或擴展。這意味著你可以修改,添加,移除一個集合的元素。 不可變集合 永遠不會改變。不過,你仍然可 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...