ShannonChenCHN / algorithm-and-data-structure

Algorithms + Data Structures = Programs
2 stars 1 forks source link

算法练习: 设计题 #26

Open ShannonChenCHN opened 3 years ago

ShannonChenCHN commented 3 years ago

LeetCode 设计类型的题库:https://leetcode-cn.com/tag/design/

题目列表

总结

ShannonChenCHN commented 3 years ago

146. LRU 缓存机制

2021.05.07 09:15 pm ~ 11: 15 pm

image image

参考题解:

期望效果:

LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1);   // 缓存更新成 {1: 1}
lRUCache.put(2, 2);  // 缓存更新成 {2: 2, 1: 1}
lRUCache.get(1);       // 返回 1,缓存更新成 {1: 1, 2: 2}
lRUCache.put(3, 3);  // 超出限制,关键字 2 作废,缓存更新成 {3: 3, 1: 1}
lRUCache.get(2);      // 返回 -1 (未找到)
lRUCache.put(4, 4); // 超出限制,关键字 1 作废,缓存更新成 {4: 4, 3: 3}
lRUCache.get(1);      // 返回 -1 (未找到)
lRUCache.get(3);     // 返回 3
lRUCache.get(4);     // 返回 4

题目最重要的两点要求:

综上,我们可以总结出 cache 用到的数据结构需要满足的条件:增删改查快,有顺存储。

(1)什么数据结构同时符合上述条件呢? 哈希表查找快,但是数据存储是无序的;链表是有序的,而且插入删除快,但是查找慢。所以我们可以将两者结合起来:哈希表+链表。

(2)为什么要是双向链表,单链表行不行? 删除节点时需要获取到前一个节点,而题目要求删除的时间复杂度是 O(1),所以必须是双向链表。

(3)哈希表和双向链表中需要存储的内容是什么? 哈希表中存储的 value 应该是双向链表的节点,而不是外面输入的 value,因为我们需要在读取哈希表中的缓存的同时,还要能够快速访问链表中对应的节点。

双向链表中需要存储的内容有以下四个:

伪代码:

class LRUCache {

    init(_ capacity: Int) {

    }

    /// 时间复杂度:O(1)
    func get(_ key: Int) -> Int {
        从 Hash Map 中读取缓存
        if 缓存不存在 {
            return -1
        }

        将 Linked List 中的节点前置到 head

        将缓存结果返回
    }

    /// 时间复杂度:O(1)
    func put(_ key: Int, _ value: Int) {
        创建新的链表节点

        if Hash Map 中已经存在该缓存 {
            更新 Hash Map 中的缓存
            删除 Linked List 中的旧缓存
            在 Linked List 中插入新的缓存
        } else {
            if 超出容量 {
                删除 Linked List 中的尾端的缓存
                删除 Hash Map 中对应的缓存
            }

            在 Hash Map 中添加新缓存
            在 Linked List 中插入新的缓存
        }
    }
}

代码实现:

class LRUCache {
    var capacity: Int
    var hashMap: [Int: ListNode] = [:]
    var list: LinkedList = LinkedList()

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

    /// 时间复杂度:O(1)
    func get(_ key: Int) -> Int {
        if let node = hashMap[key] {
            list.moveToHead(node)
            return node.value
        }
        return -1
    }

    /// 时间复杂度:O(1)
    func put(_ key: Int, _ value: Int) {
        if let node = hashMap[key] {
            node.value = value
            list.moveToHead(node)
        } else {
            if list.count == capacity {
                if let tail = list.removeLast() {
                    hashMap[tail.key] = nil
                }
            }
            let node = ListNode(key, value)
            hashMap[key] = node
            list.insert(node)
        }
    }

}

class LinkedList {
    var count: Int = 0
    var dummyHead: ListNode = ListNode(0, 0)
    var dummyTail: ListNode = ListNode(0, 0) // 为了保证 removeLast 的时间复杂度为 O(1)

    init() {
        dummyHead.next = dummyTail
        dummyTail.prev = dummyHead
    }

    /// 时间复杂度: O(1)
    func insert(_ node: ListNode) {
        let prev = dummyHead
        let next = dummyHead.next!
        prev.next = node
        node.prev = prev
        node.next = next
        next.prev = node

        count += 1
    }

    /// 时间复杂度: O(1)
    func remove(_ node: ListNode) {
        let prev = node.prev!
        let next = node.next!
        prev.next = node.next
        next.prev = prev

        count -= 1
    }

    /// 时间复杂度: O(1)
    func moveToHead(_ node: ListNode) {
        remove(node)
        insert(node)
    }

    /// 时间复杂度: O(1)
    func removeLast() -> ListNode? {
        let tail = dummyTail.prev!
        if tail === dummyHead { // 没有节点了
            return nil
        }
        remove(tail)
        return tail
    }
}

class ListNode {
    var key: Int
    var value: Int
    var next: ListNode?
    var prev: ListNode?

    init(_ key: Int, _ value: Int) {
        self.key = key
        self.value = value
        self.next = nil
        self.prev = nil
    }
}

进阶:如果考虑到线程安全问题,该怎么设计呢?

延伸阅读