【日拱一卒】鏈表——如何實(shí)現(xiàn)lru

LRU

Redis的內(nèi)存淘汰機(jī)制好幾種,如ttl、random、lru。

lru(less recently used)即最近最少使用策略,表示在最近一段時(shí)間內(nèi)最少被使用到的Redis鍵,如果遇到內(nèi)存不足,會(huì)有限淘汰這部分鍵來騰出更多空間。

今天就來說說lru這種淘汰策略是如何通過鏈表這種結(jié)構(gòu)實(shí)現(xiàn)的。

難點(diǎn)

在鏈表結(jié)構(gòu)中,如何表示最近訪問的節(jié)點(diǎn)和如何表示最久沒有訪問的節(jié)點(diǎn)?

如何判定一個(gè)鏈表中是否存在要查找的節(jié)點(diǎn)?

如何向鏈表結(jié)構(gòu)插入節(jié)點(diǎn),并放在最近最新的節(jié)點(diǎn)位置?

如果在鏈表中刪除一個(gè)節(jié)點(diǎn)?

思路

結(jié)合上面的難點(diǎn),我們可以構(gòu)建一個(gè)可以解決問題的鏈表模型。

如何表示最近訪問的節(jié)點(diǎn)和如何表示最久沒有訪問的節(jié)點(diǎn)

可以設(shè)計(jì)一個(gè)雙向鏈表,頭結(jié)點(diǎn)表示最近訪問的節(jié)點(diǎn),尾結(jié)點(diǎn)表示最久沒有訪問的節(jié)點(diǎn)。使用雙向鏈表是為了查找和定位更加方便。

如何判定一個(gè)鏈表中是否存在要查找的節(jié)點(diǎn)

解決這個(gè)問題,最直接的思路就是遍歷整個(gè)鏈表,依次匹配如果找到相同的值,對(duì)應(yīng)的節(jié)點(diǎn)就是待查找的節(jié)點(diǎn),如果遍歷完整個(gè)鏈表,還是沒有找到,表示該鏈表不存在該節(jié)點(diǎn)。

還有一種思路是將鏈表的所有節(jié)點(diǎn)存放到一個(gè)map結(jié)合中,查找的時(shí)候直接通過map的key進(jìn)行查找即可。

如何向鏈表結(jié)構(gòu)插入節(jié)點(diǎn),并放在最近最新的節(jié)點(diǎn)位置

結(jié)合前面幾篇,我們知道,鏈表的插入和刪除是非常方便的,但是在lru問題背景下,如果插入節(jié)點(diǎn)并保證是最新的位置呢?顯然最新的節(jié)點(diǎn)是要放到頭結(jié)點(diǎn)的。

另外需要注意的點(diǎn)是,插入之前需要先查找這個(gè)節(jié)點(diǎn)是否存在鏈表中,如果存在需要先刪除。

如果在鏈表中刪除一個(gè)節(jié)點(diǎn)

刪除一個(gè)節(jié)點(diǎn)的前置步驟應(yīng)該是先判定一個(gè)節(jié)點(diǎn)是否存在鏈表中,如果存在刪除即可,如果不存在則無需刪除。

通過以上幾個(gè)問題,我們大概可以構(gòu)想出幾個(gè)原子函數(shù)

  • 初始化雙向鏈表結(jié)構(gòu)
  • 查找指定節(jié)點(diǎn)
  • 插入指定節(jié)點(diǎn)
  • 刪除指定節(jié)點(diǎn)

下面我們主要看如何實(shí)現(xiàn)這幾個(gè)函數(shù)就可以了,主要代碼如下

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
}

初始化的雙向鏈表如上圖所示,一個(gè)節(jié)點(diǎn)包括數(shù)據(jù)部分data,前繼節(jié)點(diǎn)pre和后繼節(jié)點(diǎn)next。

所有節(jié)點(diǎn)數(shù)據(jù)放入map集合中。

Get()方法會(huì)在map中查找,如果不存在,則直接返回。如果存在,則調(diào)用remove先刪除該節(jié)點(diǎn),再調(diào)用setHeader將節(jié)點(diǎn)放入頭結(jié)點(diǎn)。

Put()方法會(huì)首先在map中查找對(duì)應(yīng)節(jié)點(diǎn),如果找到,則先調(diào)用remove刪除方法刪除改節(jié)點(diǎn),并調(diào)用setHeader方法將節(jié)點(diǎn)放入頭結(jié)點(diǎn)。

至此一個(gè)lru的淘汰策略使用一個(gè)雙向鏈表就實(shí)現(xiàn)了。

鏈表總結(jié)

前面幾篇,分別介紹了通過鏈表結(jié)構(gòu)如何實(shí)現(xiàn)鏈表反轉(zhuǎn)、判斷鏈表是否有環(huán)、鏈表結(jié)構(gòu)的回文判斷、有序鏈表的合并以及本篇的lru實(shí)現(xiàn)。

鏈表具備插入刪除方便,但是查找效率較低的特性。

以下是對(duì)于鏈表結(jié)構(gòu)的梳理


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容