1、LRU是什么?
LRU(Least recently used,最近最少使用)。它是按照一个非常著名的计算机操作系统基础理论得来的:最近使用的页面数据会在未来一段时期内仍然被使用,已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。基于这个思想,会存在一种缓存淘汰机制,每次从内存中找到最久未使用的数据然后置换出来,从而存入新的数据!它的主要衡量指标是使用的时间,附加指标是使用的次数。
2、LRU的实现
2.1、利用双链表实现
双链表有一个特点就是链表方向是双向的。每个数据元素节点有2个指针(pre前一节点/next后一节点),从双向链表的任意一点开始,都可以很方便的访问前节点和后节点。实现主要涉及添加、访问、修改、删除操作。
添加:新元素,直接放在表头,其他元素顺序往下移动
访问:除头节点外,如果是其他节点,直接将数据移到头部
修改:修改原值之后,除头节点外,如果是其他节点,直接将数据移到头部
删除:直接删除节点,其他节点顺序前移
2.2、Swift代码实现
2.2.1、定义基本元素节点
class Node: NSObject {
// 上一个节点
var pre: Node?
// 下一个节点
var next: Node?
var key: AnyHashable
var value: Any
init(value: Any, key: AnyHashable) {
self.value = value
self.key = key
super.init()
}
override var description: String {
return "\(key):\(value)"
}
}
2.2.2、定义链表以及链表的基本操作
定义链表
class LinkMap: NSObject {
var headNode: Node?
var tailNode: Node?
var dict = [AnyHashable: Node]()
var totalCount: UInt64 = 0
}
链表基本操作--插入新元素
/// 插入新元素
///
/// - Parameter node: 元素节点
func insert(_ node: Node) {
totalCount += 1
dict[node.key] = node
if let head = headNode {
node.next = head
head.pre = node
// 重新赋值头节点
headNode = node
} else {
headNode = node
tailNode = node
}
}
链表基本操作--删除元素,我们需要做的是把该节点的前一节点的next指向当前节点下一个节点,再把当前节点的下一节点的pre指向当前节点的前一个节点,这样听起来可能有点绕,我们来画图来看:
/// 删除元素
///
/// - Parameter node: 元素节点
func removeNode(_ node: Node) {
totalCount -= 1
dict.removeValue(forKey: node.key)
if let _ = node.pre {
node.pre?.next = node.next
}
if let _ = node.next {
node.next?.pre = node.pre
}
if headNode == node {
headNode = node.next
}
if tailNode == node {
tailNode = node.pre
}
}
假设现在要删除3这个元素节点,我们第一步要做的就是把3的pre节点4的next指针指向3的下一个节点2,再把3的下一个节点2的pre指针指向3的上一个节点4,这样3就消失了,从4和2之间断开了,4和2再也不需要3来进行连接,从而实现删除的效果。
链表基本操作--把当前节点移动到首部,类似于删除效果,先把当前节点删除,再设置当前节点的next为头结点,当前节点的pre设置为nil, 头结点的pre设置为当前节点。最后将头结点设置为当前节点。
/// 把当前节点移动到首部
///
/// - Parameter node: 元素节点
func moveNodeToHead(_ node: Node) {
if headNode == node {
return
}
// 删除当前节点
if tailNode == node {
tailNode = node.pre
tailNode?.next = nil
} else {
node.next?.pre = node.pre
node.pre?.next = node.next
}
// 把当前节点移动到头部
node.next = headNode
node.pre = nil
headNode?.pre = node
// 重新赋值头节点
headNode = node
}
链表基本操作--删除尾节点
/// 删除尾节点
func removeTailNode() -> Node? {
totalCount -= 1
if let tail = tailNode {
let key = tail.key
dict.removeValue(forKey: key)
}
if headNode == tailNode {
return nil
} else {
tailNode = tailNode?.pre
tailNode?.next = nil
return tailNode
}
}
2.2.3、操作链表
初始化cache,定义链表最大容量
init(limitCount: UInt64 = 100) {
self.limitCount = limitCount
super.init()
}
添加元素--先判断链表中是否有该元素,有个话,则移动到头部,否则插入新元素。如果链表个数超过限制个数,删除尾节点元素
func setobject(_ value: Any, forKey key: AnyHashable) {
lock.lock()
if let node = lru.dict[key] {
// 存在节点,则把节点移到头部
lru.moveNodeToHead(node)
} else {
// 不存在节点,则插入新的节点
let node = Node(value: value, key: key)
lru.insert(node)
}
if lru.totalCount > limitCount {
// 数量超过限制,则删除尾节点
let _ = lru.removeTailNode()
}
lock.unlock()
}
读取元素--若存在元素,读取完,则移动到头部
func objc(forKey key: AnyHashable) -> Any? {
lock.lock()
var node: Node?
node = lru.dict[key]
if let node = node {
lru.moveNodeToHead(node)
}
lock.unlock()
return node?.value
}
2.3、示例
let cache = Cache(limitCount: 5)
cache.setobject("a", forKey: "1")
cache.setobject("b", forKey: "2")
cache.setobject("c", forKey: "3")
cache.setobject("d", forKey: "4")
cache.setobject("e", forKey: "5")
print("原链表:\(cache.description)") // 5:e 4:d 3:c 2:b 1:a
let value = cache.objc(forKey: "3") ?? "zzzz"
print("value = \(value)")
print("取值之后链表:\(cache.description)") // 3:c 5:e 4:d 2:b 1:a
cache.setobject("f", forKey: "6")
print("新增元素之后链表:\(cache.description)") // 6:f 3:c 5:e 4:d 2:b
cache.removeObjc(forKey: "4")
print("删除元素之后链表:\(cache.description)") // 6:f 3:c 5:e 2:b
看打印结果发现,无论添加和获取元素之后整个链表都会将最近访问的元素移动到顶点,这样保证了我们每次取到的最热点的数据,与我们所预想的一致。这个就是LRU重要思想。
总结
该文讲述了LRU的算法基本实现,理解LRU算法思路,也有助于理解双向链表。在实际开发中也可以运用到合适的业务场景中来。有空闲时间的同学,也可以阅读YYMemoryCache源码,该文中的Demo也上传到了GitHub中,有需要的同学也可以下载。