LeetCode 146 LRUCache Python题解

本文详细介绍了如何使用Python实现LRUCache,从最初的线性时间复杂度到最终的常数时间复杂度,涉及了Python list、heap、双向链表和OrderedDict等数据结构的应用。通过不断优化,最终实现Get和Put操作均为Θ(1),并成功通过了LeetCode的测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LRUCache全名为Least Recently Used,即最近最少使用算法,是操作系统中发生缺页中断时常用的一种页面置换算法。

根据局部性原理,最近使用的数据块很有可能继续被频繁使用,因此当Cache已满的时候,LRUCache算法会把最久未使用的数据块替换出去。

对于LRU算法主要实现两个操作:

  • 访问数据块
    • 将访问的数据块更新为最近访问,并返回访问的数据块。
  • 添加数据块
    • 如果Cache还有容量,将添加的数据块添加到Cache之后标记为最近访问。
    • 如果Cache的容量已满,替换最久未访问的数据块为添加的数据块。

关于数据块的最近访问顺序可以表示为一个list,list中的元素按照访问顺序从最久到最近排序,当需要替换数据块的时候弹出list的首个元素,并将添加的数据块放到队尾;当需要访问数据块的时候,将访问的数据块放到队尾。

第一版代码

class LRUCache(object):

    def __init__(self, capacity):
        self.capacity = capacity
        self._cache = []   
        self._cache_look_up = {}

    def get(self, key):
        if key not in self._cache_look_up:
            return -1

        self._cache.remove(key)
        self._cache.append(key)

        return self._cache_look_up[key]

    def put(self, key, value):
        if key in self._cache_look_up:
            self._cache_look_up[key] = value
            self._cache.remove(key)
            self._cache.append(key)
            return
        else:
            if len(self._cache) == self.capacity:
                del_key = self._cache[0]
                self._cache = self._cache[1:]
                del self._cache_look_up[del_key]

            self._cache.append(key)
            self._cache_look_up[key] = value

这里使用了Python中内置的list数据结构作为保存访问顺序的队列,Python list实现为一块连续分配的内存(即C中的数组),因此如果list中删除元素或者插入元素的时间复杂度都为 Θ(n) ,但是根据索引访问元素或者在队尾插入元素都非常快,只需要常数时间。

除了保存访问顺序的队列以外,还需要保存key和value之间的对应关系,在这里直接使用Python dict来实现,相比C++中使用红黑树来实现的map,Python dict是通过hash table来实现的,因此搜索元素的时间复杂度能达到 Θ(1) 。在解决hash冲突的时候,Python dict使用了开放寻址法,通过二次探测函数计算下一个内存地址,当散列表中的装载因子达到2/3时,通过realloc函数重新分配内存空间。

Get( Θ(n)

首先判断key是否在Cache之中存在,一种很常见的写法是:

if key not in self.cache_look_up.keys():
     return -1

dict的keys方法返回了一个包含所有key的list,因此in操作的时间复杂度就变为 Θ(n) ,但是Python dict是通过hash table实现的,key的搜索可以通过 Θ(1) 来实现。即:

if key not self.cache_look_up:
    return -1

参考链接:Check if a given key already exists in a dictionary(其中第二个回答)

接下来就是将最近访问的数据块放到队尾,这里使用了list数据结构,所以时间复杂度为 Θ(n)

Put( Θ(n)

在设置新的数据块的时候,主要的耗时操作还是在list中删除或移动元素,时间复杂度也是为 Θ(n)

Result(TLE 17/18)

结果妥妥地TLE,毕竟题目要求的是两个操作都必须实现为 Θ(1)

第二版代码

优先队列除了使用传统的list来实现,还可以使用heap来实现,在heap中操作的时间复杂度一般为 log2N

在list中,直接可以通过key的位置的顺序表示访问顺序,但在堆中做不到,因此需要在堆的节点中存储一个访问时间,由于heapq库中没有提供针对节点接口的比较,因此节点自身需要重载比较运算符:

import time

class HeapNode(object):

    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.access_time = time.time()

    def update_time(self):
        self.access_time = time.time()

    def __lt__(self, other):
        return self.access_time < other.access_time

    def __ge__(self, other):
        return self.access_time >= other.access_time

    def __le__(self, other):
        return self.access_time <= other.access_time

    def __cmp__(self, other):
        return self.access_time == other.access_time

实现堆的一些常用ADT:

class Heap(object):

    def __init__(self):
        self.heap = []
        self.heap_size = 0

    def insert(self, node):
        self.heap_size += 1
        heapq.heappush(self.heap, node)

    def heapify(self):
        self.heap.sort()

    def pushpop(self, node):
        return heapq.heappushpop(self.heap, node)

最后实现LRUCache:

class LRUCache(object):

    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = Heap()
        self.cache_look_up = {}

    def get(self, key):
        if key not in self.cache_look_up:
            return -1

        heap_node = self.cache_look_up[key]
        heap_node.update_time()

        self.cache.heapify()
        return heap_node.value

    def put(self, key, value):
        if key in self.cache_look_up:
            heap_node = self.cache_look_up[key]
            heap_node.value = value
            heap_node.update_time()

            self.cache.heapify()
        else:
            new_node = HeapNode(key, value)
            self.cache_look_up[key] = new_node

            if self.cache.heap_size == self.capacity:
                min_node = self.cache.pushpop(new_node)
                self.cache_look_up.pop(min_node.key)
                return

            self.cache.insert(new_node)

Get( Θ(log2N) )

获取key的时候直接查询dict即可,但是还需要在堆的优先队列中更新key对应的节点访问时间,在堆中维护一次堆性质的时间复杂度为 log2N

Put( Θ(log2N) )

在堆中弹出最小值,并将最小值替换成新插入的节点,维护一次堆性质,时间复杂度为 log2N

Result(TLE 16/18)

虽然堆实现的优先队列的时间复杂度比线性时间更少,但是在通过leetcode测试的时候明显比第一版代码更慢,因此有可能是堆的常数操作花费的时间更多。

第三版代码

在Python list中,即一段连续分配的内存中,移动和删除元素需要的时间复杂度都为 Θ(n) ,那么有没有办法实现为 Θ(1) 呢?与数组相对应的另一种线性结构就是链表,在移动和删除元素之间都只需要常数级的时间复杂度。

但是在链表中搜寻元素需要遍历链表,即 Θ(n) ,很明显如果不改进搜寻的方法,肯定也是吃一发TLE。因此,可以通过哈希表来记录key的节点地址,当需要访问指定节点的时候直接访问哈希表即可。

从哈希表中获取相对应的节点之后,还要对节点之间的移动,单向链表不能很好的满足需求,因此需要实现为双向链表。

class LinkNode(object):

    def __init__(self, key, val, prev_node=None, next_node=None):
        self.key = key
        self.val = val
        self.prev = prev_node
        self.next = next_node


class DoubleLinkList(object):

    def __init__(self, capacity):
        self.capacity = capacity
        self.size = 0

        self.head = None
        self.tail = None

    def append(self, node):
        """

        :param node:
        :return:

        append a node to the double link list last
        """
        if self.size == self.capacity:
            raise ValueError("The double link list has been full.")

        self.size += 1

        if self.head is None:
            self.head = self.tail = node
            return

        self.tail.next = node
        node.prev = self.tail
        self.tail = node

    def delete(self, node):
        """

        :param node:
        :return node:

        delete a node in double link list. switch(node):
           1.node == self.head
           2.node == self.tail
           3.node in the double link list middle
        """
        if self.size == 0:
            raise ValueError("can not delete empty double link list")

        self.size -= 1

        if node == self.head:
            if node.next:
                node.next.prev = None

            self.head = node.next
        elif node == self.tail:
            if node.prev:
                node.prev.next = None

            self.tail = node.prev
        else:
            node.prev.next = node.next
            node.next.prev = node.prev

        return node


class LRUCache(object):

    def __init__(self, capacity):
        self.capacity = capacity
        self.cache_look_up = {}
        self.cache_list = DoubleLinkList(capacity)

    def get(self, key):
        if key not in self.cache_look_up:
            return -1

        node = self.cache_look_up[key]
        self.cache_list.delete(node)
        self.cache_list.append(node)

        return node.val

    def put(self, key, value):
        if key in self.cache_look_up:
            node = self.cache_look_up[key]
            node.val = value
            self.cache_list.delete(node)
            self.cache_list.append(node)
        else:
            if self.capacity == self.cache_list.size:
                head_node = self.cache_list.delete(self.cache_list.head)
                del self.cache_look_up[head_node.key]

            new_node = LinkNode(key, value)
            self.cache_look_up[key] = new_node
            self.cache_list.append(new_node)

Get( Θ(1) )

在Python dict中搜寻元素的时间复杂度为 Θ(1) ,而在双向链表中移动元素的时间复杂度也为 Θ(1)

Put( Θ(1) )

与Get的分析基本相同,操作基本可以在 Θ(1) 中实现。

Result(Accpeted)

这里写图片描述

第四版代码

也可以使用Collections中的OrderedDict来实现:

from collections import OrderedDict


class LRUCache(object):

    def __init__(self, capacity):
        self.capacity = capacity
        self.cache = OrderedDict()

    def get(self, key):
        if key not in self.cache:
            return -1

        value = self.cache[key]
        self.cache.pop(key)
        self.cache[key] = value
        return value

    def put(self, key, value):
        if key in self.cache:
            self.cache.pop(key)
            self.cache[key] = value
        else:
            if len(self.cache.keys()) == self.capacity:
                self.cache.popitem(last=False)

            self.cache[key] = value

Result

这里写图片描述

如果还有更好的方法,欢迎跟我一起交流。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值