LeetCode 146 LRUCache Python题解

来源:互联网 发布:服装店记账软件 编辑:程序博客网 时间:2024/06/07 19:26

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 timeclass 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_nodeclass 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 nodeclass 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 OrderedDictclass 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

这里写图片描述

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

0 0
原创粉丝点击