返回

算法实践:LRU 与 LFU 缓存机制的探究

IOS

算法的本质在于平衡资源的分配和获取,旨在寻找一种方式来优化有限资源的利用率。在计算机科学中,缓存是一种技术,用于存储临时数据,以便快速访问。当数据被请求时,缓存会首先检查其是否已存储。如果数据已被缓存,则直接从缓存中提取。否则,数据将从原始数据源中获取并存储在缓存中,以便下次请求时直接获取。

LRU(Least Recently Used)最近最少使用算法

LRU算法是一种缓存淘汰算法,它通过记录数据最近被访问的时间来确定哪些数据应该被淘汰。当缓存已满,并且需要腾出空间以存储新数据时,LRU算法会淘汰最近最少使用的数据。LRU算法的实现通常使用双向链表,链表中的每个节点都存储一个数据项和一个指向该数据项最近使用时间的指针。当数据被访问时,其对应的节点会被移动到链表的头部,表示该数据已被最近使用。当需要淘汰数据时,链表的尾部节点将被移除。

LFU(Least Frequently Used)最不常用算法

LFU算法是一种缓存淘汰算法,它通过记录数据被访问的频率来确定哪些数据应该被淘汰。当缓存已满,并且需要腾出空间以存储新数据时,LFU算法会淘汰最不常使用的数据。LFU算法的实现通常使用哈希表,哈希表的每个键都是一个数据项,而每个值都是一个表示该数据项被访问频率的计数器。当数据被访问时,其对应的计数器将递增。当需要淘汰数据时,哈希表中具有最小计数器的数据项将被移除。

比较LRU和LFU算法

LRU和LFU算法都是常用的缓存淘汰算法,但它们在适用场景上存在差异。LRU算法更适合于数据访问模式具有时间相关性的场景,例如Web缓存。LFU算法更适合于数据访问模式与时间相关性较弱的场景,例如数据库缓存。

代码实现

以下是使用Swift语言实现的LRU和LFU算法的代码示例:

class LRUCache {
    private var cache: [Int: Node] = [:]
    private var head: Node?
    private var tail: Node?
    private let capacity: Int

    init(capacity: Int) {
        self.capacity = capacity
    }

    func get(_ key: Int) -> Int? {
        guard let node = cache[key] else {
            return nil
        }
        removeNode(node)
        addNode(node)
        return node.value
    }

    func put(_ key: Int, _ value: Int) {
        if let node = cache[key] {
            node.value = value
            removeNode(node)
            addNode(node)
        } else {
            let newNode = Node(key: key, value: value)
            cache[key] = newNode
            addNode(newNode)
            if cache.count > capacity {
                cache.removeValue(forKey: tail!.key)
                removeNode(tail!)
            }
        }
    }

    private func addNode(_ node: Node) {
        if head == nil {
            head = node
            tail = node
        } else {
            node.next = head
            head?.prev = node
            head = node
        }
    }

    private func removeNode(_ node: Node) {
        if node.prev != nil {
            node.prev?.next = node.next
        } else {
            head = node.next
        }

        if node.next != nil {
            node.next?.prev = node.prev
        } else {
            tail = node.prev
        }
    }

    private class Node {
        var key: Int
        var value: Int
        var next: Node?
        var prev: Node?

        init(key: Int, value: Int) {
            self.key = key
            self.value = value
        }
    }
}

class LFUCache {
    private var cache: [Int: Node] = [:]
    private var freqMap: [Int: DoublyLinkedList] = [:]
    private var minFreq: Int = 0

    init(capacity: Int) {
        self.capacity = capacity
    }

    func get(_ key: Int) -> Int? {
        guard let node = cache[key] else {
            return nil
        }

        increaseFreq(node)
        return node.value
    }

    func put(_ key: Int, _ value: Int) {
        if capacity <= 0 {
            return
        }

        if let node = cache[key] {
            node.value = value
            increaseFreq(node)
        } else {
            let newNode = Node(key: key, value: value)
            cache[key] = newNode

            let freq = 1
            if !freqMap.keys.contains(freq) {
                freqMap[freq] = DoublyLinkedList()
            }

            freqMap[freq]?.addTail(newNode)
            minFreq = 1

            if cache.count > capacity {
                let nodeToRemove = freqMap[minFreq]?.removeHead()
                cache.removeValue(forKey: nodeToRemove!.key)
            }
        }
    }

    private func increaseFreq(_ node: Node) {
        guard let freq = freqMap[node.freq] else {
            return
        }

        freq.removeNode(node)

        node.freq += 1
        if !freqMap.keys.contains(node.freq) {
            freqMap[node.freq] = DoublyLinkedList()
        }

        freqMap[node.freq]?.addTail(node)

        if freq.isEmpty() {
            minFreq += 1
        }
    }

    private class Node {
        var key: Int
        var value: Int
        var freq: Int
        var prev: Node?
        var next: Node?

        init(key: Int, value: Int) {
            self.key = key
            self.value = value
            self.freq = 1
        }
    }

    private class DoublyLinkedList {
        var head: Node?
        var tail: Node?

        func isEmpty() -> Bool {
            return head == nil
        }

        func addTail(_ node: Node) {
            if head == nil {
                head = node
                tail = node
            } else {
                tail?.next = node
                node.prev = tail
                tail = node
            }
        }

        func removeHead() -> Node? {
            if head == nil {
                return nil
            }

            let nodeToRemove = head
            head = head?.next
            head?.prev = nil

            return nodeToRemove
        }

        func removeNode(_ node: Node) {
            if node.prev != nil {
                node.prev?.next = node.next
            } else {
                head = node.next
            }

            if node.next != nil {
                node.next?.prev = node.prev
            } else {
                tail = node.prev
            }
        }
    }
}

总结

LRU和LFU算法都是常用的缓存淘汰算法,它们在适用场景上存在差异。LRU算法更适合于数据访问模式具有时间相关性的场景,例如Web缓存。LFU算法更适合于数据访问模式与时间相关性较弱的场景,例如数据库缓存。

如果您正在寻找一种缓存淘汰算法,那么您需要考虑以下因素:

  • 数据访问模式
  • 缓存大小
  • 淘汰策略