用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阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容