用Swift實現Cache LRU (譯文)

此文章為本人翻譯的譯文,版權為原作者所有。
英文原文:How To Implement Cache LRU With Swift

本文代碼地址MarcoSantarossa/CacheLRU.swift,個人覺得本文的實現不是很好,我寫了新的版本,戳這里LRUCache

image

介紹

Cache LRU(最近最少使用)和字典很相似。通過Key-Value的方式存儲。字典和Cache之間的區別在于后者的容量有限。每次達到容量時,Cache都會刪除最近最少使用的數據。

在本文中,我們將看看如何使用Swift實現Cache LRU。

內容

  • 入門指南
  • 雙鏈表
  • Cache LRU
  • 總結

入門指南

首先,我們必須了解應該用什么數據結構來實現Cache LRU。有不同的方式來實現它。在這個版本中使用:

  • 雙鏈表:這是核心。我們需要這個鏈表來緩存元素。不使用數組,因為它更慢。Cache LRU策略是每次把最近使用的元素移到頭部。但是如果我們將數組中的元素移到數組中的索引0處,需要對所有其他元素執行右移。
  • Dictionary<Key, ListNode>:使用雙鏈表的問題是它的查找的時間復雜度是O(n)。可以使用一個字典來解決這個瓶頸 - 它的查找時間復雜度是O(1)。我們使用這個字典來存儲列表的節點。

在下一節中,將看到如何實現雙鏈表。如果你已經知道了,可以直接跳到Cache LRU部分。

雙鏈表

對于本文,我們不需要實現完整的雙向鏈表。 出于這個原因,只實現Cache中使用到的的方法。

第一步是創建一個類DoublyLinkedList,它接受一個泛型T來存儲節點:

final class DoublyLinkedList<T> {   

}

然后為節點創建一個類:

final class DoublyLinkedList<T> {
 
    final class Node<T> {
        var payload: T
        var previous: Node<T>?
        var next: Node<T>?
 
        init(payload: T) {
            self.payload = payload
        }
    }
}

在這里使用一個嵌套的Node類。 如果你使用的是早于3.1的Swift版本,則必須在DoublyLinkedList之外創建此類。 Swift 3.1支持具有泛型的嵌套類。

然后,設置鏈表的存儲最大量:

private(set) var count: Int = 0

鏈表上的操作有時實現起來很困難。可以存儲第一個和最后一個元素,讓一切更輕松:

private var head: Node<T>?
private var tail: Node<T>?

現在開始實現鏈表中的方法:

addHead

我們需要一個向鏈表中添加新元素的方法,這個新元素就是最近使用的元素。

func addHead(_ payload: T) -> Node<T> {
    let node = Node(payload: payload)
    defer {
        head = node
        count += 1
    }
 
    guard let head = head else {
        tail = node
        return node
    }
 
    head.previous = node
 
    node.previous = nil
    node.next = head
 
    return node
}

moveToHead

Cache LRU需要我們將最近使用的元素放在列表頭部。 因此需要一個方法把節點移動到頭部:

func moveToHead(_ node: Node<T>) {
    guard node !== head else { return }
    let previous = node.previous
    let next = node.next
 
    previous?.next = next
    next?.previous = previous
 
    node.next = head
    node.previous = nil
 
    if node === tail {
        tail = previous
    }
 
    self.head = node
}

removeLast

當Cache已滿時,需要一個方法來刪除最久未使用的元素

func removeLast() -> Node<T>? {
    guard let tail = self.tail else { return nil }
 
    let previous = tail.previous
    previous?.next = nil
    self.tail = previous
 
    if count == 1 {
        head = nil
    }
 
    count -= 1
 
    // 1
    return tail
}
  1. tail的值與self.tail不同。

最后,可以為Node類型添加一個別名,以便在Cache實現中使用:

typealias DoublyLinkedListNode<T> = DoublyLinkedList<T>.Node<T>

鏈表實現的最終版本應該是這樣的:

typealias DoublyLinkedListNode<T> = DoublyLinkedList<T>.Node<T>
 
final class DoublyLinkedList<T> {
    final class Node<T> {
        var payload: T
        var previous: Node<T>?
        var next: Node<T>?
 
        init(payload: T) {
            self.payload = payload
        }
    }
 
    private(set) var count: Int = 0
 
    private var head: Node<T>?
    private var tail: Node<T>?
 
    func addHead(_ payload: T) -> Node<T> {
        let node = Node(payload: payload)
        defer {
            head = node
            count += 1
        }
 
        guard let head = head else {
            tail = node
            return node
        }
 
        head.previous = node
 
        node.previous = nil
        node.next = head
 
        return node
    }
 
    func moveToHead(_ node: Node<T>) {
        guard node !== head else { return }
        let previous = node.previous
        let next = node.next
 
        previous?.next = next
        next?.previous = previous
 
        node.next = head
        node.previous = nil
 
        if node === tail {
            tail = previous
        }
 
        self.head = node
    }
 
    func removeLast() -> Node<T>? {
        guard let tail = self.tail else { return nil }
 
        let previous = tail.previous
        previous?.next = nil
        self.tail = previous
 
        if count == 1 {
            head = nil
        }
 
        count -= 1
 
        return tail
    }
}

Cache LRU

現在是時候實現Cache了。 我們可以開始創建一個新的CacheLRU類:

泛型Key必須是Hashable類型,因為它是存儲在雙鏈表中的值的key。

Cache和字典一樣是通過Key-Value方式存儲數據。不幸的是,我們的雙鏈表值只能是payload,而不能是一個key。 為了解決這個問題,可以創建一個包含value和key的結構體。鏈表節點將存儲包含value和key的對象CachePayload:

final class CacheLRU<Key: Hashable, Value> {
 
    private struct CachePayload {
        let key: Key
        let value: Value
    }
}

然后,我們應該添加兩個數據結構 - 一個雙鏈表和一個字典:

private let list = DoublyLinkedList<CachePayload>()
private var nodesDict = [Key: DoublyLinkedListNode<CachePayload>]()

正如在介紹中看到的那樣,Cache LRU的容量有限。 我們可以通過init方法把容量傳給一個私有屬性:

private let capacity: Int
 
init(capacity: Int) {
    self.capacity = max(0, capacity)
}

使用max方法來避免capacity小于0。

現在實現兩個Cache方法來getset元素:

setValue

通過set方法,可以為某個key添加/更新元素。這個值作為最近使用的元素移動到鏈表的開頭:

func setValue(_ value: Value, for key: Key) {
    // 1
    let payload = CachePayload(key: key, value: value)
 
    // 2
    if let node = nodesDict[key] {
        node.payload = payload
        list.moveToHead(node)
    } else {
        let node = list.addHead(payload)
        nodesDict[key] = node
    }
 
    // 3
    if list.count > capacity {
        let nodeRemoved = list.removeLast()
        if let key = nodeRemoved?.payload.key {
            nodesDict[key] = nil
        }
    }
}  
  1. 創建一個對象來包裝要存儲在列表中的keyvalue
  2. 如果鏈表已經存儲了該特定key的元素,更新該值并將其移動到列表的開頭。否則,創建一個新節點并將其添加為列表的頭部。
  3. 如果超過了Cache容量,刪除鏈表最后一個元素。

getValue

使用get方法,可以拿到特定key的元素。 每次使用一個元素時,它都會作為最近使用的元素移動到鏈表的頭部:

func getValue(for key: Key) -> Value? {
    guard let node = nodesDict[key] else { return nil }
 
    list.moveToHead(node)
 
    return node.payload.value
}

Cache最終版本是這樣的:

final class CacheLRU<Key: Hashable, Value> {
 
    private struct CachePayload {
        let key: Key
        let value: Value
    }
 
    private let capacity: Int
    private let list = DoublyLinkedList<CachePayload>()
    private var nodesDict = [Key: DoublyLinkedListNode<CachePayload>]()
 
    init(capacity: Int) {
        self.capacity = max(0, capacity)
    }
 
    func setValue(_ value: Value, for key: Key) {
        let payload = CachePayload(key: key, value: value)
 
        if let node = nodesDict[key] {
            node.payload = payload
            list.moveToHead(node)
        } else {
            let node = list.addHead(payload)
            nodesDict[key] = node
        }
 
        if list.count > capacity {
            let nodeRemoved = list.removeLast()
            if let key = nodeRemoved?.payload.key {
                nodesDict[key] = nil
            }
        }
    }
 
    func getValue(for key: Key) -> Value? {
        guard let node = nodesDict[key] else { return nil }
 
        list.moveToHead(node)
 
        return node.payload.value
    }
}

我們可以像這樣使用這個Cache:

let cache = CacheLRU<Int, String>(capacity: 2)
 
cache.getValue(for: 5) // nil
 
cache.setValue("One", for: 1)
cache.setValue("Eleven", for: 11)
cache.setValue("Twenty", for: 20)
 
cache.getValue(for: 1) // nil. We exceeded the capacity with the previous `setValue`  and `1` was the last element.
cache.getValue(for: 11) // Eleven

總結

這就是我們的Cache LRU了。

如今,我們應用程序有很多內存可用。 盡管如此,可能仍需要一個容量有限的緩存來節省內存空間。例如,當我們必須緩存像圖像那樣耗費空間的對象時。

Update:
我發現Array比鏈表快。因為Cache LRU的版本使用雙鏈表,所以我拋棄了這種的作法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • Collection & Map Collection 子類有 List 和 Set List --> Array...
    任教主來也閱讀 3,188評論 1 9
  • 關于Mongodb的全面總結 MongoDB的內部構造《MongoDB The Definitive Guide》...
    中v中閱讀 32,001評論 2 89
  • 從 YYCache 源碼 Get 到如何設計一個優秀的緩存 來源:Lision 前言 iOS 開發中總會用到各種緩...
    今天lgw閱讀 6,055評論 1 22
  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網絡請求組件 FMDB本地數據庫組件 SD...
    陽明AGI閱讀 16,003評論 3 119
  • 目前更新到十集,我看了第一集就停不下來,一口氣看完,現在苦哈哈地等更新。 怎么說呢,豆瓣和知乎上的評價都不看好,雖...
    是唯伊不是唯一閱讀 397評論 3 0