LeetCode.146-LRU缓存机制(Swift)

921 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 。如果哪里写的不对,请大家评论批评。

希望往后的日子,可以每天坚持一个算法,最近发现一个有意思的事情,LeetCode中等难度的题,也不简单,暴力算法固然能解决问题,但是从时间复杂度和空间复杂度上肯定达不到要求。

LRU缓存机制

题目

请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

LRUCache(int capacity) 以正整数作为容量capacity 初始化LRU 缓存

int get(int key) 如果关键字key存在于缓存中,则返回关键字的值,否则返回 -1

void put(int key, int value)  如果关键字key已经存在,则变更其数据值value;如果不存在,则向缓存中插入该组key-value,如果插入操作导致关键字数量超过capacity,则应该逐出最久未使用的关键字.

函数 get 和 put 必须以 O(1) 的平均时间复杂度运行

示例

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
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

提示

提示:

1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 105
最多调用 2 * 105 次 get 和 put

分析

一般常用的方式就是Hash+双链表的形式,哈希可以利用Key-Value的原理直线O(1)的时间复杂度,可以满足getput的要求的条件。Value存储的是双链表node节点

为什么用双链表

我们需要一个有序的存储空间来存储先后的循序,数组、栈都可以做到这个一点,但是操作比较麻烦,这个时候双链表是最好的选择,无论插入还是删除,都可以在O(1)解决(哈希的Value存储的是双链表node节点)

图解

分析示例

  1. Cache初始化的时候入参是2,这里我们需要一个定义一个Hash来存储key-value,然后存储最大缓存数据2

  2. put(1,1)也就是说key1 ,value1LRU 缓存机制.drawio (1).png

  3. put(2,2)也就是说key2 ,value2,因为链表上有数据,新的数据要保持在最顶部。相当于活跃度的排序LRU 缓存机制.drawio (3).png

  4. get(1)从Hash中获取链表的node地址,直接获取到node的值,这时候要注意了。要注意了,因为get(1)相当于对与1有了操作,这时候就需要中心排序,把当前位置的节点,删除,重新插入到头节点上去LRU 缓存机制.drawio (4).png

  5. put(3, 3)也就是说key3 ,value3,这时候要注意,缓存长度是2,这时候就必须删除掉最后一位不怎么使用的节点,把3插入到头结点上去。LRU 缓存机制.drawio (6).png

  6. get(2)去Hash中已经找不到缓存了,因为空间有限,在上一步被删除了,所以得到的是-1

  7. 后面就不一步步的解析了直接上图吧~~

  8. 后面三个都是get(1)得到-1已经被删除了,get(3)得到3以及get(4)得到4在正在缓存中。 LRU 缓存机制.drawio (7).png

代码

class LRUCache {
    // 哈希
    var mapNode : [Int:MyNode]!
    // 最大数量
    var capacity = 0
    // 头节点
    var first:MyNode!
    // 尾节点
    var last:MyNode!

    // 初始化,存储最大缓存
    init(_ capacity: Int) {
      self.capacity = capacity
        mapNode = [Int:MyNode]()
      
      first = MyNode.init()
      last = MyNode.init()
      
      first.next = last
      last.prev = first
    }
    
    // 获取数据
    func get(_ key: Int) -> Int {
     // 如果map中不存在,或者node不存在直接返回-1
      guard let node = mapNode[key] else {
        return -1
      }
      
      // 删除节点重新插入到开始,这里可以简单优化一下,如果已经是头节点,可以不用操作
      removeNode(node)
        addFirst(node)
      return node.val
    }
    
    // 插入数据
    func put(_ key: Int, _ value: Int) {
      let node = mapNode[key]
      // 插入数据也是对原有数据操作,需要放在头节点
      if node != nil {
        //! 更新 key
        node!.val = value
        removeNode(node!)
        addFirst(node!)
        
      } else {
         // 如果大于存储长度,需要淘汰末尾的
        if mapNode.keys.count == capacity {
          let prevNode = mapNode.removeValue(forKey: last.prev!.key)!
          removeNode(prevNode)
        }
        let newNode = MyNode.init(key, value)
        mapNode[key] = newNode
        addFirst(newNode)
      }
      
    }
  
  
    // 双向链表,保持链表不要断开
    private func removeNode(_ node:MyNode) {
      // 下一个链表的上一个节点 指向当前节点的上一个
      node.next!.prev = node.prev
      // 上一个链表的下一个节点(原来指向自己) ,现在要指向自己的下一个节点
      node.prev!.next = node.next
    }
  
    //  双向链表 -> 插入节点到头节点后面
    private func addFirst(_ node:MyNode) {
      node.next = first.next
      first.next!.prev = node
    
      first.next = node
      node.prev = first
    }
}

class MyNode {
  var key:Int = 0
  var val: Int = 0
  var prev: MyNode?
  var next: MyNode?
  init(_ key:Int = 0, _ value:Int = 0) {
    self.key = key
    self.val = value
  }
}