【日拱一卒】鏈表——如何實現lru
LRU
Redis的記憶體淘汰機制好幾種,如ttl、random、lru。
lru(less recently used)即最近最少使用策略,表示在最近一段時間內最少被使用到的Redis鍵,如果遇到記憶體不足,會有限淘汰這部分鍵來騰出更多空間。
今天就來說說lru這種淘汰策略是如何通過鏈表這種結構實現的。
難點
在鏈表結構中,如何表示最近訪問的節點和如何表示最久沒有訪問的節點?
如何判定一個鏈表中是否存在要查找的節點?
如何向鏈表結構插入節點,並放在最近最新的節點位置?
如果在鏈表中刪除一個節點?
思路
結合上面的難點,我們可以構建一個可以解決問題的鏈表模型。
如何表示最近訪問的節點和如何表示最久沒有訪問的節點
可以設計一個雙向鏈表,頭結點表示最近訪問的節點,尾結點表示最久沒有訪問的節點。使用雙向鏈表是為了查找和定位更加方便。
如何判定一個鏈表中是否存在要查找的節點
解決這個問題,最直接的思路就是遍歷整個鏈表,依次匹配如果找到相同的值,對應的節點就是待查找的節點,如果遍歷完整個鏈表,還是沒有找到,表示該鏈表不存在該節點。
還有一種思路是將鏈表的所有節點存放到一個map結合中,查找的時候直接通過map的key進行查找即可。
如何向鏈表結構插入節點,並放在最近最新的節點位置
結合前面幾篇,我們知道,鏈表的插入和刪除是非常方便的,但是在lru問題背景下,如果插入節點並保證是最新的位置呢?顯然最新的節點是要放到頭結點的。
另外需要注意的點是,插入之前需要先查找這個節點是否存在鏈表中,如果存在需要先刪除。
如果在鏈表中刪除一個節點
刪除一個節點的前置步驟應該是先判定一個節點是否存在鏈表中,如果存在刪除即可,如果不存在則無需刪除。
通過以上幾個問題,我們大概可以構想出幾個原子函數
- 初始化雙向鏈表結構
- 查找指定節點
- 插入指定節點
- 刪除指定節點
下面我們主要看如何實現這幾個函數就可以了,主要程式碼如下
type LRUCache struct { Cap int Map map[int]*Node Head *Node Last *Node } type Node struct { Val int Key int Pre *Node Next *Node } func Constructor(capacity int) LRUCache { cache := LRUCache{ Cap: capacity, Map: make(map[int]*Node, capacity), Head: &Node{}, Last: &Node{}, } cache.Head.Next = cache.Last cache.Last.Pre = cache.Head return cache } func (this *LRUCache) Get(key int) int { node, ok := this.Map[key] if !ok { return -1 } this.remove(node) this.setHeader(node) return node.Val } func (this *LRUCache) Put(key int, value int) { node, ok := this.Map[key] if ok { this.remove(node) } else { if len(this.Map) == this.Cap { delete(this.Map, this.Last.Pre.Key) this.remove(this.Last.Pre) } node = &Node{Val: value, Key: key} this.Map[node.Key] = node } node.Val = value this.setHeader(node) } func (this *LRUCache) setHeader(node *Node) { this.Head.Next.Pre = node node.Next = this.Head.Next this.Head.Next = node node.Pre = this.Head } func (this *LRUCache) remove(node *Node) { node.Pre.Next = node.Next node.Next.Pre = node.Pre }
初始化的雙向鏈表如上圖所示,一個節點包括數據部分data,前繼節點pre和後繼節點next。
所有節點數據放入map集合中。
Get()方法會在map中查找,如果不存在,則直接返回。如果存在,則調用remove先刪除該節點,再調用setHeader將節點放入頭結點。
Put()方法會首先在map中查找對應節點,如果找到,則先調用remove刪除方法刪除改節點,並調用setHeader方法將節點放入頭結點。
至此一個lru的淘汰策略使用一個雙向鏈表就實現了。
鏈表總結
前面幾篇,分別介紹了通過鏈表結構如何實現鏈表反轉、判斷鏈表是否有環、鏈表結構的迴文判斷、有序鏈表的合併以及本篇的lru實現。
鏈表具備插入刪除方便,但是查找效率較低的特性。
以下是對於鏈表結構的梳理
如果您覺得閱讀本文對您有幫助,請點一下「推薦」按鈕,您的「推薦」將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。