手把手教你實現LRU算法(Java版)

一、前言

LRU,全稱Least Recently Used,即最近最少使用算法,怎麼理解?就是使用一個有序固定容量大小的隊列維持一堆數據,當往隊列插入一個不存在的數據時,就會淘汰掉最長時間沒有使用的數據,我們把這個算法成爲LRU算法。

LRU在日常開發中非常常見,而緩存機制就是使用LRU的最佳案例。

二、LRU算法實現

LRU應該支持以下操作: 獲取數據 get 和 寫入數據 put 。

  • 獲取數據 get(key):如果密鑰 (key) 存在於緩存中,則獲取密鑰的值(總是正數),否則返回 -1。
  • 寫入數據 put(key, value):如果密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據之前刪除最近最少使用的數據值,從而爲新的數據值留出空間。

按照上面的說法,我們只要使用一個數據或者一個鏈表就能實現該功能,但是你會發現這樣實現的LRU時間複雜度會非常高,所以作爲有追求的程序員,我們應該實現一個複雜度相對比較好的算法。

1.使用鏈表和哈希表實現LRU

首先我們需要使用鏈表提供一個固定大小、並且是能夠實現有序排列功能的隊列,爲什麼不用數組?因爲數組對於移動數據的時間複雜度太高了,所以不考慮使用數組。

其次我們需要使用哈希表來實現高效的get、put操作,它的平均時間複雜度能達到O(1)。

這種方法在時間複雜度上還是沒有達到最佳,因爲把一個元素從鏈表中刪除,它的時間複雜度是O(n)。在Java中具體實現就可以使用LinkedList和HashMap來實現,具體代碼實現如下:

// 時間複雜度:get達到O(1),set方法O(n)
// 空間複雜度:O(capacity)
class LRUCache {
    private LinkedList<Integer> mQueue;
    private HashMap<Integer, Integer> mMap;
    private int mCapacity;
    public LRUCache(int capacity) {
        this.mCapacity = capacity;
        mQueue = new LinkedList<Integer>();
        mMap = new HashMap<Integer, Integer>();
    }
    
    public int get(int key) {
        Integer result = mMap.get(key);
        if(result == null) {
            return -1;
        } else {
            // 當發現隊列裏面存在數據時,我們需要把它從隊列裏面移動到隊尾
            mQueue.remove((Integer)key);
            mQueue.offer(key);
            return result;
        }
    }
    
    public void put(int key, int value) {
        Integer result = mMap.get(key);
        if(result != null) {
            // 當發現隊列裏面存在該數據時
            // 則把它移動到隊尾
            mQueue.remove((Integer)key);
            mQueue.offer(key);
            mMap.put(key, value);
        } else {
            // 當發現隊列裏面不存在數據時
            if(mQueue.size() >= mCapacity) {
                // 如果隊列大小超過了容量值,就需要把隊頭元素刪掉
                int head = mQueue.poll();
                // 並且從hashMap裏面抹掉
                mMap.remove(head);
            }
            mQueue.offer(key);
            mMap.put(key, value);
        }
    }
}

2.使用雙向鏈表和哈希表來實現LRU

在第一種實現方案中,我們在put方法中的時間複雜度爲O(n),那麼有沒有辦法讓這裏的時間複雜度達到O(1)呢?答案是有的,怎麼實現呢?就是使用一個雙向鏈表加哈希表來實現,如下圖所示。其實在Java中有一種數據結構的底層實現正是如此,即LinkedHashMap,它提供了實現有序的HashMap。

image

例如我們哈希表裏面有三個元素,分別是Node A,Node B,Node C,但是它們之間是無序的,爲了讓它們有序,我們通過在它們存儲的值裏面定義前後指針,從而形成了一個有序的哈希表,這樣就能實現get、put都爲O(1)的LRU算法,具體實現代碼如下:

// 時間複雜度:get、set方法都達到O(1)
// 空間複雜度:O(capacity)
class LRUCache {
	class Node {
		int key;
		int value;
		Node pre;
		Node next;
		
		Node(int key, int value, Node pre, Node next) {
			this.key = key;
			this.value = value;
			this.pre = pre;
			this.next = next;
		}
	}

	private HashMap<Integer, Node> mMap;
	private int mCapacity;
	private Node mHead;
	private Node mTail;

	public LRUCache(int capacity) {
		this.mCapacity = capacity;
		mMap = new HashMap<>();
	}

	public int get(int key) {
		Node node = mMap.get(key);
		if (node == null) {
			return -1;
		} else {
			moveToTail(node);
			return node.value;
		}
	}

	public void put(int key, int value) {
		Node result = mMap.get(key);
		Node newNode = new Node(key, value, null, null);
		if (result == null) {
			// 隊列中不存在該元素
			if (mMap.size() < mCapacity) {
				// 隊列沒有滿
				if (mMap.size() == 0) {
					// 直接插到隊頭
					mHead = newNode;
					mTail = newNode;
					mMap.put(key, newNode);
				} else {
					// 直接插到隊尾
					mTail.next = newNode;
					newNode.pre = mTail;
					mTail = newNode;
					mMap.put(key, newNode);
				}
			} else {
				// 隊列已經滿了
				Node oldNode = removeHead();
				mMap.remove(oldNode.key);
				
				addLast(newNode);
				mMap.put(key, newNode);
			}
		} else {
			result.value = value;
			// 隊列中存在該元素
			moveToTail(result);
		}
	}

	private void moveToTail(Node node) {
		if (mTail != node) {
			Node pre = node.pre;
			Node next = node.next;
			if (pre != null) {
				pre.next = next;
			}
			if (next != null) {
				next.pre = pre;
				if (next.pre == null) {
					mHead = next;
				}
			}
			if (mTail != null) {
				mTail.next = node;
				node.next = null;
				node.pre = mTail;
			}
			mTail = node;
		}
	}
	
	private void addLast(Node newNode) {
		if (mTail == null) {
			mHead = newNode;
		} else {
			mTail.next = newNode;
			newNode.pre = mTail;
		}
		mTail = newNode;
	}

	private Node removeHead() {
		Node deleteNode = mHead;
		if (mHead != null) {
			Node next = mHead.next;
			if (next != null) {
				next.pre = null;
			} else {
				mTail = null;
			}
			mHead = next;
		}
		return deleteNode;
	}

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