此文章為本人翻譯的譯文,版權為原作者所有。
英文原文:How To Implement Cache LRU With Swift
本文代碼地址MarcoSantarossa/CacheLRU.swift,個人覺得本文的實現不是很好,我寫了新的版本,戳這里LRUCache
介紹
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
}
-
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方法來get
和set
元素:
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
}
}
}
- 創建一個對象來包裝要存儲在列表中的
key
和value
。 - 如果鏈表已經存儲了該特定key的元素,更新該值并將其移動到列表的開頭。否則,創建一個新節點并將其添加為列表的頭部。
- 如果超過了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的版本使用雙鏈表,所以我拋棄了這種的作法。