Swift之LRU算法

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来进行连接,从而实现删除的效果。


删除元素节点.jpg

链表基本操作--把当前节点移动到首部,类似于删除效果,先把当前节点删除,再设置当前节点的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中,有需要的同学也可以下载。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343