LeetCode 146. LRU 缓存

74 阅读3分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

146. 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

思路:

  • 参考:力扣

  • 更多lru相关知识可学习大佬文章:其实吧,LRU也就那么回事。

  • 借助hash链表(hashmap + 双向链表)这一组合的数据结构

  • Put() 操作中:

    • 若key已存在:更新值并将该节点移动到最前
    • 若key不存在:判断链表容量是否已满,若满则移除链表尾元素后再加入新节点到链表,未满则直接加入
  • Get() 操作中:

    • 若已存在:直接返回元素的val值,并把该元素移除并重新加入到首位置上去(链表头部表示近期访问过的节点元素)
    • 若不存在:返回-1
  • 注意:若元素被get()或put()操作都算被访问过,需要把该元素移除并重新加入到首位置上去

  • 其中 AddNode() 方法将某节点元素重新加入到首位置,其中双向链表的指针操作较多,可参照图示:​编辑

时间复杂度: get()、put()方法,由于是hash链表的数据结构,通过空间换时间的方式,均为O(1)

空间复杂度: get()、put()方法,由于是hash链表的数据结构,通过空间换时间的方式,均为O(1)

type LinkNode struct {
	key, val  int
	pre, next *LinkNode
}

// 其中head和tail都为虚拟节点,不存储实际数据,仅用于标记双向链表首尾位置
type LRUCache struct {
	m          map[int]*LinkNode
	cap        int
	head, tail *LinkNode
}

func Constructor(capacity int) LRUCache {
	head := &LinkNode{0, 0, nil, nil}
	tail := &LinkNode{0, 0, nil, nil}
	head.next = tail
	tail.pre = head
	return LRUCache{make(map[int]*LinkNode), capacity, head, tail}
}

func (this *LRUCache) Get(key int) int {
	cache := this.m
	if v, exist := cache[key]; exist {
		this.MoveToHead(v)
		return v.val
	} else {
		return -1
	}
}

func (this *LRUCache) Put(key int, value int) {
    tail := this.tail
	cache := this.m
	if v, exist := cache[key]; exist {
		// 若存在:更新值并将该节点移动到最前
        v.val = value
		this.MoveToHead(v)
	} else {
        // 若不存在:判断链表容量是否已满,若满则移除链表尾元素后再加入新节点到链表,未满则直接加入
		v := &LinkNode{key, value, nil, nil}

        // 超出容量cap,删除最后元素
		if len(cache) == this.cap {
			delete(cache, tail.pre.key)
			
            // tail是虚拟的尾节点,移除 tail.pre 等同于下面两句
            this.RemoveNode(tail.pre) 
            // tail.pre.pre.next = tail
            // tail.pre = tail.pre.pre
		}
		this.AddNode(v)
		cache[key] = v
	}
}

// 以下为内部公共函数
// 访问过某节点元素,则将该节点元素从当前位置移除并重新加入到首位置
func (this *LRUCache) MoveToHead(node *LinkNode) {
	this.RemoveNode(node)
	this.AddNode(node)
}

// 移除某节点元素
func (this *LRUCache) RemoveNode(node *LinkNode) {
	node.pre.next = node.next
	node.next.pre = node.pre
}

// 将某节点元素重新加入到首位置
func (this *LRUCache) AddNode(node *LinkNode) {
	head := this.head
	node.next = head.next
	head.next.pre = node
	node.pre = head
	head.next = node
}