Java實現LRU緩存算法

最困難的事情就是認識自己!

個人博客,歡迎訪問!

前言:

什麼是LRU算法:LRU是Least Recently Used的縮寫,即最近最久未使用,是一種操作系統中常用的頁面置換算法。

應用場景:

知道了什麼是LRU後,我們再來聊下它的使用場景;在工作中,對於Redis我們一定是比較熟悉的,它是一個內存數據庫;因爲它是內存數據庫,並且內存的空間是有限的,如果Redis中數據量很大的話,內存就可能被佔滿,但是此時如果還有數據存入Redis的話,那該怎麼辦呢?這就是由Redis的的內存淘汰策略所決定的。


LRU最近最久未使用算法就是Redis的內存淘汰策略之一。 

示例:

// 當前緩存的容量爲2
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

設計LRU算法的數據結構:

1、要求:
①、首先支持查詢數據get和寫入數據put;
②、滿足時間複雜度爲O(1);

2、思路:
由題目中要求的O(1)時間複雜度想到緩存可以想到用一個map來存儲key、value結點,最近最久未使用到的(緩存數據)放到最後,最新訪問的(緩存數據)放到最前面,可以考慮用雙向鏈表來實現,這樣,這個map的key對應的是緩存的Key, value對應的是雙向鏈表的一個節點,即鏈表的節點同時存在map的value中。

這樣,當新插入一個節點時,它應該在這個雙向鏈表的頭結點處,同時把這個節點的key和這個節點put到map中保留下來。當LRU緩存鏈表容量達到最大又要插入新節點時,把鏈表的尾節點刪除掉,同時在map中移除該節點對應的key。

雙向鏈表中節點的數據結構:

public class DoubleLinkedListNode {
	
	String key;
	Object value;
	// 頭指針
	DoubleLinkedListNode pre;
	// 尾指針
	DoubleLinkedListNode next;
	
	
	public DoubleLinkedListNode(String key, Object value) {
        this.key = key;
        this.value = value;
    }

}

由此可以抽象出LRU緩存算法的數據結構:雙向鏈表+HashMap。

數據結構邏輯圖如下所示:

代碼奉上:

import java.util.HashMap;


public class LRUCache {

	private HashMap<String, DoubleLinkedListNode> map = new HashMap<String, DoubleLinkedListNode>();
	// 頭結點
	private DoubleLinkedListNode head;
	// 尾節點
	private DoubleLinkedListNode tail;
	// 雙向鏈表的容量
	private int capacity;
	// 雙向鏈表中節點的數量
	private int size;

	public LRUCache(int capacity) {
		this.capacity = capacity;
		size = 0;
	}

	/**
	 * @Description: 將節點設置爲頭結點
	 * @param node
	 */
	public void setHead(DoubleLinkedListNode node) {
		// 節點的尾指針執行頭結點
		node.next = head;
		// 節點的頭指針置爲空
		node.pre = null;
		if (head != null) {
			// 將頭結點的頭指針執行節點
			head.pre = node;
		}
		head = node;
		if (tail == null) {
			// 如果雙向鏈表中還沒有節點時,頭結點和尾節點都是當前節點
			tail = node;
		}
	}

	/**
	 * @Description:將雙向鏈表中的節點移除
	 * @param node
	 */
	public void removeNode(DoubleLinkedListNode node) {
		DoubleLinkedListNode cur = node;
		DoubleLinkedListNode pre = cur.pre;
		DoubleLinkedListNode post = cur.next;
		// 如果當前節點沒有頭指針的話,說明它是鏈表的頭結點
		if (pre != null) {
			pre.next = post;
		} else {
			head = post;
		}
		// 如果當前節點沒有尾指針的話,說明當前節點是尾節點
		if (post != null) {
			post.pre = pre;
		} else {
			tail = pre;
		}
	}

	/**
	 * @Description:從緩存Cache中get
	 * @param key
	 * @return
	 */
	public Object get(String key) {
		// 使用hashmap進行查詢,時間複雜度爲O(1),如果進行鏈表查詢,需要遍歷鏈表,時間複雜度爲O(n)
		if (map.containsKey(key)) {
			DoubleLinkedListNode node = map.get(key);
			// 將查詢出的節點從鏈表中移除
			removeNode(node);
			// 將查詢出的節點設置爲頭結點
			setHead(node);
			return node.value;
		}
		// 緩存中沒有要查詢的內容
		return null;
	}

	/**
	 * @Description:將key-value存儲set到緩存Cache中
	 * @param key
	 * @param value
	 */
	public void set(String key, Object value) {
		if (map.containsKey(key)) {
			DoubleLinkedListNode node = map.get(key);
			node.value = value;
			removeNode(node);
			setHead(node);
		} else {
			// 如果緩存中沒有詞key-value
			// 創建一個新的節點
			DoubleLinkedListNode newNode = new DoubleLinkedListNode(key, value);
			// 如果鏈表中的節點數小於鏈表的初始容量(還不需要進行數據置換)則直接將新節點設置爲頭結點
			if (size < capacity) {
				setHead(newNode);
				// 將新節點放入hashmap中,用於提高查找速度
				map.put(key, newNode);
				size++;
			} else {
				// 緩存(雙向鏈表)滿了需要將"最近醉酒未使用"的節點(尾節點)刪除,騰出新空間存放新節點
				// 首先將map中的尾節點刪除
				map.remove(tail.key);
				// 移除尾節點並重新置頂尾節點的頭指針指向的節點爲新尾節點
				removeNode(tail);
				// 將新節點設置爲頭節點
				setHead(newNode);
				// 將新節點放入到map中
				map.put(key, newNode);
			}
		}
	}

	/**
	 * @Description: 遍歷雙向鏈表
	 * @param head
	 *            雙向鏈表的 頭結點
	 */
	public void traverse(DoubleLinkedListNode head) {
		DoubleLinkedListNode node = head;
		while (node != null) {
			System.out.print(node.key + "  ");
			node = node.next;
		}
		System.out.println();
	}

	// test
	public static void main(String[] args) {
		System.out.println("雙向鏈表容量爲6");
		LRUCache lc = new LRUCache(6);

		// 向緩存中插入set數據
		for (int i = 0; i < 6; i++) {
			lc.set("test" + i, "test" + i);
		}

		// 遍歷緩存中的數據,從左到右,數據越不經常使用
		System.out.println("第一次遍歷雙向鏈表:(從頭結點遍歷到尾節點)");
		lc.traverse(lc.head);

		// 使用get查詢緩存中數據
		lc.get("test2");

		// 再次遍歷緩存中的數據,從左到右,數據越不經常使用,並且此次發現剛剛操作的數據節點位於鏈表的頭結點了。
		System.out.println();
		System.out.println("get查詢 test2節點後 ,第二次遍歷雙向鏈表:");
		lc.traverse(lc.head);

		// 再次向緩存中插入數據,發現緩存鏈表已經滿了,需要將尾節點移除
		lc.set("sucess", "sucess");

		/**
		 * 再次遍歷緩存中的數據,從左到右,數據越不經常使用,並且此次發現剛剛set操作時由於鏈表滿了, 就將尾節點test0
		 * 移除了,並且將新節點置爲鏈表的頭結點。
		 */
		System.out.println();
		System.out.println("put插入sucess節點後,第三次遍歷雙向鏈表:");
		lc.traverse(lc.head);
	}

}

運行結果展示:

雙向鏈表容量爲6
第一次遍歷雙向鏈表:(從頭結點遍歷到尾節點)
test5  test4  test3  test2  test1  test0  

get查詢 test2節點後 ,第二次遍歷雙向鏈表:
test2  test5  test4  test3  test1  test0  

put插入sucess節點後,第三次遍歷雙向鏈表:
sucess  test2  test5  test4  test3  test1  

不要忘記留下你學習的足跡 [點贊 + 收藏 + 評論]嘿嘿ヾ

一切看文章不點贊都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!開個玩笑,動一動你的小手,點贊就完事了,你每個人出一份力量(點贊 + 評論)就會讓更多的學習者加入進來!非常感謝! ̄ω ̄=

參考資料:

1、記一次阿里面試,我掛在了 最熟悉不過的LRU 緩存算法設計上。。。。。

2、【LeetCode】146. LRU緩存機制

3、LRU Cache leetcode java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章