美文网首页
Swift -- LRU算法实现和简单的缓存示例

Swift -- LRU算法实现和简单的缓存示例

作者: 奇董 | 来源:发表于2017-12-15 18:12 被阅读111次

    双链表

    image.png

    来看双向链表的实现
    首先定义Node

    /// 双向列表的节点
    class linkedNode<T> {
        var value: T
        
        var previous: linkedNode?
        var next: linkedNode?
        
        init(_ value: T) {
            self.value = value
        }
    }
    

    List

    class linkedList<T> {
        typealias Node = linkedNode<T>
        
         private var head: Node?
         private var tail: Node?
        
        /// 判断是否为空
        var isEmpty: Bool {
            return head == nil
        }
        /// 获取头节点
        var first: Node?{
            return head
        }
        ///获取尾节点
        var last: Node? {
            return tail
        }
        ///获取 链表数量
        var count: Int = 0
        // 下标语法糖
        subscript(index: Int) -> T? {
            var i = index
            var next = head
            
            while i > 0 {
                i -= 1
                next = next?.next
            }
            
            return next?.value
        }
    }
    

    尾插法
    1 tail.next = new Node
    2 new Node.previous = tail
    这里主要注意 头尾节点的问题

    /// 尾插
        func append(_ value: T) {
            let newNode = Node(value)
            
            if let lastNode = tail {
                lastNode.next = newNode
                newNode.previous = lastNode
                
                tail = newNode
            }else {
                head = newNode
                tail = newNode
            }
            
            //修改数量
            count += 1
        }
    

    中插


    image.png
     ///插入 node
        func insert(value: T, atIndex index: Int) {
            let (pre,next) = nodesBeforeAndAfter(index: index)
            
            let newNode = Node(value)
            
            pre?.next = newNode
            next?.previous = newNode
            
            newNode.previous = pre
            newNode.next = next
            
            if pre == nil {
                head = newNode
            }
            
            if count == index - 1 {
                tail = newNode
            }
            
            count += 1
            
            
        }
    ///获取某节点的头结点和尾节点
        private func nodesBeforeAndAfter(index: Int) -> (Node?,Node?) {
            
            var next = head
            var previous: Node?
            var i = index
            
            while i > 0 && next?.next != nil{
                i -= 1
                
                previous = next
                next = next?.next
                
            }
            
            return (previous,next)
            
        }
    

    删除节点

    ///删除最后一个
        func removeLast() {
            removeNode(node: last!)
        }
        /// 移除某一个node
        func removeNode(node: Node) {
            let pre = node.previous
            let next = node.next
            
            pre?.next = next
            next?.previous = pre
            
            if pre == nil {
                head = node.next
            }
            if next == nil {
                tail = node.previous
            }
            count -= 1
        }
    

    移动到头结点

    /// node 移动到头节点
        func moveToHead(node: Node) {
            let pre = node.previous
            let next = node.next
            
            pre?.next = next
            next?.previous = pre
            
            node.next = head
            head?.previous = node
            
            head = node
            
            if pre == nil {
                return
            }
            if next == nil {
                tail = pre
            }
        }
    

    LRU

    准备工作差不多了
    我来看下LRU的定义


    image.png
    1. 新数据插入到链表头部;

    2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

    3. 当链表满的时候,将链表尾部的数据丢弃。

    【命中率】

    当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

    【复杂度】

    实现简单。

    【代价】

    命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

    LRU 实践

    我们客户端关于LRU算法 最先想到,肯定是图片缓存系统
    为了方便
    我们存的数据 为["1": "我是一张图片"] 这种【String: Stirng】
    首先我们定义一个简单的缓存类

    /// LRU 实现
    class CacheLRU<T> {
        var limit: Int
        var cache: linkedList<T>
        init(cacheNumber: Int) {
            self.limit = cacheNumber
            self.cache = linkedList<T>()
        }
        
        //增
        func add(some: T) {
            if cache.count == limit {
                cache.removeLast()
            }
            cache.append(some)
        }
        //删
        func clearCache() {
            cache.removeAll()
        }
        
        //移
        func move(value: linkedNode<T>) {
            cache.moveToHead(node: value)
        }
    }
    // 1 先从缓存系统查看是否有缓存
    extension linkedList where T == Dictionary<String, String> {
        func contains(name: String) -> Node? {
            var next = head
            var index = 0
            while index < count {
                if next?.value[name] != nil {
                    return next
                }
                index += 1
                next = next?.next
            }
            return nil
        }
    }
    extension CacheLRU where T == Dictionary<String, String> {
        func fetchImage(name: String) {
            let node = cache.contains(name: name)
            
            if let dic = node?.value {
                // 1.内存存在 直接取内存中的数据
                print(dic[name] ?? "😁")
                // 2.lru 访问的数据成为头结点
                move(value: node!)
            }else {
                //网络获取 并且 add
            }
        }
    }
    

    我们来看结果
    1 创建缓存类 存贮图片
    这里设置存贮上线为2张图片(实际肯定是内存为标准)

    let cacheMnager = CacheLRU<Dictionary<String,String>>.init(cacheNumber: 2)
    let image1 = ["1": "我是一张图片"]
    let image2 = ["2": "我是一张图片"]
    let image3 = ["3": "我是一张图片"]
    cacheMnager.add(some: image1)
    cacheMnager.add(some: image2)
    cacheMnager.add(some: image3)
    

    因为上线是2 导致 第二张图片被清除


    image.png

    2 访问已有图片

    cacheMnager.fetchImage(name: "3")
    

    访问已有图片的 已有图片的node 会移到头部


    image.png

    LRU-K

    image.png

    LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

    实现上: LRU-K 的其实就是 在LRU的基础上多维护一条历史链表。链表里面的数据多一个访问次数属性。当访问次数打到K次。就存到缓存队列里 这里和LRU一样

    【命中率】

    LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。

    【复杂度】

    LRU-K队列是一个优先级队列,算法复杂度和代价比较高。

    【代价】

    由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。

    LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。

    我们看代码实现

    class CacheLRU_K<T,E> {
        let cache: CacheLRU<E>
        let cacheHistory: CacheLRU<T>
        let limit: Int
        let K: Int
        
        init(limit: Int,K: Int) {
            self.K = K
            self.limit = limit
            self.cache = CacheLRU.init(cacheNumber: limit)
            self.cacheHistory = CacheLRU.init(cacheNumber: limit)
        }
    }
    extension linkedList where T == Dictionary<String,Any> {
        func contains(name: String) -> T? {
            var next = head
            var index = 0
            while index < count {
                if next?.value[name] != nil {
                    return next?.value
                }
                index += 1
                next = next?.next
            }
            return nil
        }
    }
    extension CacheLRU_K where T == Dictionary<String,Any>, E == [String: String] {
        func add(some: T) {
            
            var data: [String: Any] = some
            let name = some.first!.key
            let temp = cacheHistory.cache.contains(name: name)
            if let temp = temp {
                data = temp
            }
            
            if data["number"] == nil {
                data["number"] = 0
            }
            
            var number = data["number"] as! Int
            data["number"] = number + 1
            number += 1
            if number == K {
                data["number"] = nil
                let temp = data as! [String: String]
                cache.add(some: temp)
                //cacheHistory remove
                //下班了 以后补上0.0
            }else {
                
                if cacheHistory.cache.count == limit {
                    cacheHistory.cache.removeLast()
                }
                cacheHistory.add(some: data)
            }
        }
        
        func fetchImage(name: String) {
            let node = cache.cache.contains(name: name)
            
            if let dic = node?.value {
                // 1.内存存在 直接取内存中的数据
                print(dic[name] ?? "😁")
                // 2.lru 访问的数据成为头结点
                cache.move(value: node!)
            }else {
                //这里和lru 不同
                //网络请求
                // lru_k add
            }
        }
    }
    

    这里 基本和LRU 一样就是维护一条历史队列
    这里初始化 K 为2 意思
    只有连续add 2次才能真正的假如到缓存链表中

    let manager = CacheLRU_K<[String:Any],[String:String]>.init(limit: 2, K: 2)
    let image11 = ["1": "我是一张图片"]
    manager.add(some: image11)
    

    缓存和 历史缓存的结果


    image.png

    继续add

    manager.add(some: image11)
    

    这时候内存中就存在了我要要缓存的数据


    image.png

    历史缓存中就应该删除这条记录

    下班了 溜了。。实现过程可能没讲清楚。但是代码都注释的听明白的。有时间补上

    相关文章

      网友评论

          本文标题:Swift -- LRU算法实现和简单的缓存示例

          本文链接:https://www.haomeiwen.com/subject/buvdwxtx.html