<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
儲存對有限數量值的強參照的快取。 每次存取一個值時,它都會移動到佇列的頭部。 當一個值被新增到一個完整的快取中時,該佇列末尾的值將被逐出並且可能有資格進行垃圾回收。
Least Recently Used , 最近最少使用,是一種常用的演演算法。
LRUCache 是具有一定數量限制的資料結構,該資料結構採用 LRU 演演算法,維護一定數量的快取資料。如果資料結構已經達到了最大數量限制時,有新的資料插入,則就需要根據 LRU 演演算法,刪除最久未使用的資料。
根據它的功能描述,對資料結構的選擇就有了偏向性:
綜合前兩點考慮,LinkedList 配合 HashMap 是一個不錯的選擇,前者不光可以是連結串列結構、還實現了 Deque ,也可以視為佇列、棧結構, 後者提供了更低時間複雜度的檢索。
而在 JDK 中,提供了 LinkedHashMap 用來實現 LRU 快取的功能。
LinkedHashMap 繼承自 HashMap ,並實現了 Map 介面。構造方法包含了一個 accessOrder
引數,該引數會將元素按照存取順序進行排序,非常適合構建 LRUCache 。
LinkedHashMap 與 HashMap 不同之處在於維護了一個**雙向連結串列,該列表串聯起了所有元素。**這個連結串列定義了迭代順序,通常是鍵插入對映的順序(插入順序)。
請注意,如果將鍵重新插入到 Map 中,則插入順序不會受到影響。 (如果在呼叫 m.containsKey(k) 時呼叫 m.put(k, v) 將在呼叫之前立即返回 true,則將鍵 k 重新插入到對映 m 中。)
內部的雙向連結串列結構:
/** * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> head; /** * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> tail;
每次存取元素後,如果啟用存取排序(accessOrder = true
),會更新連結串列中的元素順序:
public V get(Object key) { Node<K,V> e; if ((e = getNode(key)) == null) return null; if (accessOrder) afterNodeAccess(e); return e.value; }
核心的排序邏輯在 afterNodeAccess 方法中,將最新存取的元素移動到了連結串列尾部:
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 雙向連結串列尾部節點 last if (accessOrder && (last = tail) != e) { // 存取的節點標記為 p ,p 的前後節點儲存到 b 、a 中 LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 存取節點的後續節點設定為 null ,因為是最後一個節點,所以後續節點為 null p.after = null; // 處理刪除 e 節點的邏輯 if (b == null) // p 的前面不存在節點,頭節點設定為 a head = a; else // p 的前面存在節點,將 p 的後續節點設定為 p 前一個節點的下一個節點 b -> p -> a 更新為 b -> a (刪除 p) b.after = a; if (a != null) // p 存在後續節點 a , 將 a 的 before 指標更新為 b a.before = b; else // p 不存在後續節點 a , last 指標更新為 b last = b; // 處理尾部節點的邏輯 if (last == null) // last 指標為空,更新頭節點 head = p; else { // last 指標不為空,更新連結串列順序為 last p.before = last; last.after = p; } tail = p; ++modCount; } }
所以這是一種十分適合 LRUCache 的資料結構,Android 中的 LRUCache 類就是通過 LinkedHashMap 來實現的。
public class LruCache<K, V> { private final LinkedHashMap<K, V> map; //... }
LruCache 是 Android SDK 中提供一個類,來自於 android.util
。
LruCache 中有兩個核心方法,get 和 put ,再加上容量變化處理方法,構成了完善的 LRU 機制。它的內部的資料結構是 LinkedHashMap ,通過屬性控制容量:
public class LruCache<K, V> { private final LinkedHashMap<K, V> map; private int size; private int maxSize; private int putCount; private int createCount; private int evictionCount; private int hitCount; private int missCount; public LruCache(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } this.maxSize = maxSize; // 啟用了 存取排序 accessOrder = true this.map = new LinkedHashMap<K, V>(0, 0.75f, true); } //... }
當重新設定 LruCache 的容量時,可以通過 resize 分發重新設定容量:
public void resize(int maxSize) { if (maxSize <= 0) { throw new IllegalArgumentException("maxSize <= 0"); } synchronized (this) { this.maxSize = maxSize; } trimToSize(maxSize); }
容量處理伴隨著刪除最久未使用的元素:
// 刪除最老的元素,直到剩餘元素的總數等於或低於請求的大小。 public void trimToSize(int maxSize) { while (true) { K key; V value; synchronized (this) { if (size < 0 || (map.isEmpty() && size != 0)) { throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!"); } if (size <= maxSize) { break; } Map.Entry<K, V> toEvict = map.eldest(); if (toEvict == null) { break; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true, key, value, null); } }
在這個方法中,通過 map.eldest()
獲取到了存活最久的元素,它的實現是:
// in LinkedHashMap public Map.Entry<K, V> eldest() { return head; }
最後的 entryRemoved 方法的作用是,通過呼叫 remove 移除或通過呼叫 put 替換時,將一個值被移除以騰出空間,將呼叫此方法。 預設實現什麼也不做。
get 方法根據 key 檢索 LinkedHashMap 中是否存在對應的元素,或者是否可以根據 create 方法建立元素。如果存在或可根據 create 方法建立,則將這個元素移動到佇列頭部,否則返回 null。
public final V get(K key) { // key 空檢查 if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { mapValue = map.get(key); // 從 map 中讀取元素 if (mapValue != null) { // 快取中存在元素,直接返回 hitCount++; return mapValue; } missCount++; } // 快取中不存在對應 key 的元素,建立一個新元素 V createdValue = create(key); if (createdValue == null) { // 未快取且無法建立,返回 null return null; } // 建立成功,存入到 map ,如果 key 已存在對應值,優先更新為之前的值 synchronized (this) { createCount++; mapValue = map.put(key, createdValue); // 存入新的元素,並獲取前一個 key 對應的值 mapValue。 if (mapValue != null) { map.put(key, mapValue); } else { size += safeSizeOf(key, createdValue); // 新元素導致當前容量 + 1 } } if (mapValue != null) { // key 對應的元素已存在 // 當一個值被逐出以騰出空間、通過呼叫 remove 移除或通過呼叫 put 替換時,將呼叫此方法。 預設實現什麼也不做。 entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { // key 對應的元素不存在 trimToSize(maxSize); // 擴容 return createdValue; //返回最新插入的元素 } }
這裡的 create 方法預設返回 null , 供子類實現:
protected V create(K key) { return null; }
最後的 entryRemoved 方法的作用是,通過呼叫 remove 移除或通過呼叫 put 替換時,將一個值被移除以騰出空間,將呼叫此方法。 預設實現什麼也不做。
protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
方法中的兩個引數的含義:
get 方法中通過操作 LinkedHashMap ,呼叫 LinkedHashMap 的 get 和 put 方法,會在 LinkedHashMap 內部完成排序邏輯。
LRUCache 的 put 方法用來更新資料:
public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { putCount++; size += safeSizeOf(key, value); // 當前容量 + 1 previous = map.put(key, value); // 取出先前的值 if (previous != null) { size -= safeSizeOf(key, previous); // map 中已存在 key 的情況下,保證 size 不變 } } // 先前存在元素,執行元素移除後的自定義操作 if (previous != null) { entryRemoved(false, key, previous, value); } // 容量處理 trimToSize(maxSize); return previous; }
這裡也有一個問題,如果 map 中已存在 key ,僅是更新資料,這裡沒有涉及到排序的問題。
為什麼這麼說呢?是因為 LinkedHashMap 並沒有定義 put 方法,需要檢視 HashMap 中的 put 方法:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
HashMap 中的 put 方法中真正邏輯是 putVal 方法,在 putVal 方法中呼叫了存取元素後的處理方法 afterNodeAccess 方法,而這個方法的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // ... if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 【*】 return oldValue; } // ... }
afterNodeAccess 方法在 HashMap 中是空實現,通過備註可以理解,這些方法專門為 LinkedHashMap 預留的:
// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }
而 afterNodeAccess 在 LinkedHashMap 中的實現,和 LinkedHashMap 的 get 方法中呼叫的排序方法是同一個。所以 put 方法也會對元素進行排序。
與 put 方法相同,remove 方法也是來自於父類別 HashMap:
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
removeNode 中進行移除操作,並呼叫了 afterNodeRemoval 方法:
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // ... if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); // 【*】 return node; } } return null; }
afterNodeRemoval 方法的實現在 LinkedHashMap 中,操作雙向連結串列刪除當前元素:
void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; }
在使用 LruCache 類時,可以自行定義最大快取容量,並自行計算物件的快取。例如,初始化一個最大容量為 4M ,用來儲存 Bitmap 的 LruCache :
int cacheSize = 4 * 1024 * 1024; // 4MiB LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) { protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }
最大容量為 cacheSize ,對於每一個新物件,通過 sizeOf 方法來計算這個物件的大小。
android.util.LruCache
類是 Android SDK 中的一個 LRU 快取實現,它的內部資料結構採用的是 LinkedHashMap 。accessOrder = true
,來啟用存取順序記錄邏輯。afterNodeAccess
方法中,當超過容量限制時,會刪除連結串列中 head 節點。每次存取資料,會將節點移動到隊尾。android.util.LruCache
時,通過 cacheSize 引數配合重寫 sizeOf 方法實現自定義容量計算邏輯的 LruCache 。最後再來聊一下位元組面試頻率比較高的一道演演算法題,實現一個 LruCache ,通過上面的瞭解我們也知道最優解就是通過一個 雜湊表 + 一個 雙向連結串列 來實現。
class LruCache(private val capacity: Int) { data class DLinkedNode( var key: Int? = null, var value: Int? = null, var prev: DLinkedNode? = null, var next: DLinkedNode? = null ) private val cache = HashMap<Int, DLinkedNode>() private var size = 0 private var head = DLinkedNode() private var tail = DLinkedNode() fun get(key: Int): Int { val node = cache[key] ?: return -1 // 如果 key 存在,先通過雜湊表定位,再移到頭部 moveToHead(node) return node.value ?: -1 } fun put(key: Int, value: Int) { val node = cache[key] if (node == null) { // 如果 key 不存在,建立一個新的節點 val newNode = DLinkedNode(key, value) // 新增進雜湊表 cache[key] = newNode // 新增至雙向連結串列的頭部 addToHead(newNode) ++size; if (size > capacity) { // 如果超出容量,刪除雙向連結串列的尾部節點 val tail = removeTail() // 刪除雜湊表中對應的項 cache.remove(tail?.key) --size; } } else { // 如果 key 存在,先通過雜湊表定位,再修改 value,並移到頭部 node.value = value; moveToHead(node); } } private fun addToHead(node: DLinkedNode?) { node?.prev = head node?.next = head.next head.next?.prev = node head.next = node } private fun removeNode(node: DLinkedNode?) { node?.prev?.next = node?.next node?.next?.prev = node?.prev } private fun moveToHead(node: DLinkedNode?) { removeNode(node) addToHead(node) } private fun removeTail(): DLinkedNode? { val res = tail.prev removeNode(res) return res } }
時間複雜度:對於 put 和 get 都是 O(1) 。
空間複雜度:O(capacity),因為雜湊表和雙向連結串列最多儲存 capacity + 1 個元素。
另一種利用 LinkedHashMap 的解法就比較簡單了:
class LruCacheByLinkedHashMap(val capacity: Int): LinkedHashMap<Int, Int>(capacity, 0.75f, true) { override fun get(key: Int): Int { return getOrDefault(key, -1) } override fun put(key: Int, value: Int): Int? { return super.put(key, value) } override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, Int>?): Boolean { return size > capacity } }
只需要繼承 LinkedHashMap ,並設定好初始容量、啟用存取順序排序,然後實現 removeEldestEntry ,這個方法在 put 呼叫時,會檢查刪除最老的元素,返回值為判斷條件的結果。
以上就是java面試LruCache 和 LinkedHashMap及演演算法實現的詳細內容,更多關於java面試LruCache LinkedHashMap演演算法的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45