0146. LRU 缓存机制

题目地址(146. LRU 缓存机制)

https://leetcode-cn.com/problems/lru-cache/

题目描述

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。

实现 LRUCache 类:

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

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

  • void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

进阶:你是否可以在 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. 确定需要使用的数据结构

    1. 根据题目要求,存储的数据需要保证顺序关系(逻辑层面) ===> 使用数组,链表等保证顺序关系

    2. 同时需要对数据进行频繁的增删, 时间复杂度 O(1) ==> 使用链表等

    3. 对数据进行读取时, 时间复杂度 O(1) ===> 使用哈希表 最终采取双向链表 + 哈希表

      1. 双向链表按最后一次访问的时间的顺序进行排列, 链表头部为最近访问的节点

      2. 哈希表,以关键字为键,以链表节点的地址为值

  2. put 操作 通过哈希表, 查看传入的关键字对应的链表节点, 是否存在

    1. 如果存在,

      1. 将该链表节点的值更新

      2. 将该该链表节点调整至链表头部

    2. 如果不存在

      1. 如果链表容量未满,

        1. 新生成节点,

        2. 将该节点位置调整至链表头部

      2. 如果链表容量已满

        1. 删除尾部节点

        2. 新生成节点

        3. 将该节点位置调整至链表头部

      3. 将新生成的节点,按关键字为键,节点地址为值插入哈希表

  3. get 操作 通过哈希表, 查看传入的关键字对应的链表节点, 是否存在

    1. 节点存在

      1. 将该节点位置调整至链表头部

      2. 返回该节点的值

    2. 节点不存在, 返回 null

伪代码如下:

var LRUCache = function(capacity) {
	保存一个该数据结构的最大容量
	生成一个双向链表,同时保存该链表的头结点与尾节点
	生成一个哈希表
};

function get (key) {
	if 哈希表中存在该关键字 {
		根据哈希表获取该链表节点
		将该节点放置于链表头部
		return 链表节点的值
	} else {
		  return -1
	}
};

function put (key, value) {
    if 哈希表中存在该关键字 {
		根据哈希表获取该链表节点
		将该链表节点的值更新
		将该节点放置于链表头部
	} else {
		if 容量已满 {
			删除链表尾部的节点
			新生成一个节点
			将该节点放置于链表头部
		} else {
			新生成一个节点
			将该节点放置于链表头部
		}
	}
};

代码

语言支持: JS, Go, PHP, Python3

JS Code:

class ListNode{
    constructor(key, val){
        this.key = key;
        this.val = val;
        this.pre = null;
        this.next = null;
    }
};

class LRUCache{
    constructor(capacity){
        this.capacity = capacity;
        this.size = 0;
        this.data = {};
        this.head = new ListNode();
        this.tail = new ListNode();
        this.head.next = this.tail;
        this.tail.pre = this.head;
    }

    get(key){
        if(!this.data[key]) return -1;
        else{
            let node = this.data[key];
            this.removeNode(node);
            this.appendHead(node);
            
            return node.val;
        }
    }

    put(key, value){
        if(!this.data[key]){
            let node = new ListNode(key, value);

            this.data[key] = node;
            this.appendHead(node);
            this.size++;

            if(this.size > this.capacity){
                const lastKey = this.removeTail();
                delete this.data[lastKey];
                this.size--;
            }

        }else{
            let node = this.data[key];
            this.removeNode(node);
            node.val = value;
            this.appendHead(node);
        }   
    }

    removeNode(node){
        let preNode = node.pre;
        let nextNode = node.next;

        preNode.next = nextNode;
        nextNode.pre = preNode;
    }

    appendHead(node){
        let firstNode = this.head.next;

        this.head.next = node;
        node.pre = this.head;
        node.next = firstNode;
        firstNode.pre = node;
    }

    removeTail(){
        let key = this.tail.pre.key;

        this.removeNode(this.tail.pre);
        
        return key;
    }
}

Go Code:

type LRUCache struct {
	Cap   int
	Hash  map[int]*DoubleListNode
	Cache *DoubleList
	Size  int
}

func Constructor(capacity int) LRUCache {
	return LRUCache{
		Cap:   capacity,
		Hash:  make(map[int]*DoubleListNode),
		Cache: NewDoubleList(),
	}
}

func (this *LRUCache) Get(key int) int {
	n, ok := this.Hash[key]
	if !ok {
		return -1
	}

	// 设为最近使用过
	this.Cache.remove(n)
	this.Cache.append(n)

	return n.Val
}

func (this *LRUCache) Put(key int, value int) {
	n, ok := this.Hash[key]
	if !ok { // cache 不存在
		// 增加节点
		newNode := &DoubleListNode{Key: key, Val: value}
		this.Hash[key] = newNode
		this.Cache.append(newNode)

		if this.Cap == this.Size { // 已满
			// 删除尾节点
			tailNode := this.Cache.tail.Pre
			delete(this.Hash, tailNode.Key)
			this.Cache.remove(tailNode)
		} else {
			this.Size++
		}
	} else {
		// cache 存在
		// 设为最近使用过
		this.Cache.remove(n)
        this.Cache.append(n)

        n.Val = value // 更新值
	}
}

type DoubleListNode struct {
	Key, Val  int
	Pre, Next *DoubleListNode
}

type DoubleList struct {
	Head, tail *DoubleListNode
}

func NewDoubleList() *DoubleList {
	dl := &DoubleList{
		Head: &DoubleListNode{},
		tail: &DoubleListNode{},
	}
	dl.Head.Next = dl.tail
	dl.tail.Pre = dl.Head
	return dl
}

func (dl *DoubleList) append(n *DoubleListNode) {
	n.Next = dl.Head.Next
	n.Pre = dl.Head
	dl.Head.Next.Pre = n
	dl.Head.Next = n
}

func (dl *DoubleList) remove(n *DoubleListNode) {
	n.Pre.Next = n.Next
	n.Next.Pre = n.Pre
}

PHP Code:

class LRUCache
{
    public $hash = []; // key => DoubleListNode
    public $cache;
    public $size = 0;
    public $cap; // 容量

    /**
     * @param Integer $capacity
     */
    function __construct($capacity)
    {
        $this->cap = $capacity;
        $this->cache = new DoubleList();
    }

    /**
     * @param Integer $key
     * @return Integer
     */
    function get($key)
    {
        if (!isset($this->hash[$key])) return -1;

        // 更新节点
        /** @var DoubleListNode $node */
        $node = $this->hash[$key];
        $this->cache->remove($node);
        $this->cache->append($node);

        return $node->val;
    }

    /**
     * @param Integer $key
     * @param Integer $value
     * @return NULL
     */
    function put($key, $value)
    {
        if (isset($this->hash[$key])) { // key 存在
            // 更新节点
            /** @var DoubleListNode $node */
            $node = $this->hash[$key];
            $this->cache->remove($node);
            $this->cache->append($node);
            $node->val = $value;
        } else { // key 不存在, 新增节点
            $node = new DoubleListNode($key, $value);
            $this->cache->append($node);
            $this->hash[$key] = $node;

            if ($this->size == $this->cap) {
                // 删除原节点
                $oldNode = $this->cache->tail->pre;
                $this->cache->remove($oldNode);
                unset($this->hash[$oldNode->key]);
            } else {
                $this->size++;
            }
        }
    }
}

class DoubleListNode
{
    public $key;
    public $val;
    public $pre;
    public $next;

    public function __construct($key = null, $val = null)
    {
        if ($key) $this->key = $key;
        if ($val) $this->val = $val;
    }
}

class DoubleList
{
    public $head;
    public $tail;

    public function __construct()
    {
        $this->head = new DoubleListNode();
        $this->tail = new DoubleListNode();
        $this->head->next = $this->tail;
        $this->tail->pre = $this->head;
    }

    public function append(DoubleListNode $node)
    {
        $node->pre = $this->head;
        $node->next = $this->head->next;
        $this->head->next->pre = $node;
        $this->head->next = $node;
    }

    public function remove(DoubleListNode $node)
    {
        $node->pre->next = $node->next;
        $node->next->pre = $node->pre;
    }
}

Python Code:

来自 leetcode

class DLinkedNode:
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache:
    def __init__(self, capacity: int):
        self.cache = dict()
        # 使用伪头部和伪尾部节点
        self.head = DLinkedNode()
        self.tail = DLinkedNode()
        self.head.next = self.tail
        self.tail.prev = self.head
        self.capacity = capacity
        self.size = 0

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        # 如果 key 存在,先通过哈希表定位,再移到头部
        node = self.cache[key]
        self.moveToHead(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        if key not in self.cache:
            # 如果 key 不存在,创建一个新的节点
            node = DLinkedNode(key, value)
            # 添加进哈希表
            self.cache[key] = node
            # 添加至双向链表的头部
            self.addToHead(node)
            self.size += 1
            if self.size > self.capacity:
                # 如果超出容量,删除双向链表的尾部节点
                removed = self.removeTail()
                # 删除哈希表中对应的项
                self.cache.pop(removed.key)
                self.size -= 1
        else:
            # 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node = self.cache[key]
            node.value = value
            self.moveToHead(node)

    def addToHead(self, node):
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def removeNode(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def moveToHead(self, node):
        self.removeNode(node)
        self.addToHead(node)

    def removeTail(self):
        node = self.tail.prev
        self.removeNode(node)
        return node

复杂度分析

  • 时间复杂度:$O(1)$

  • 空间复杂度:$O(n)$ n 为容量的大小

最后更新于