算法学习笔记——数据结构:LRU和LFU(有序字典LinkedHashMap和有序集LinkedHashSet的Python实现)

算法不能脱离数据结构,各种算法本质上都是将基本的数据结构进行组合,把握好链表、二叉树等基本的数据结构的特性,进而构建上层算法

基础:Python中的内置数据结构

列表list

理解为数组
访问复杂度O(1),在尾部append复杂度O(1)
判断元素x in list需要逐项对比,复杂度O(n)
缺点:在中间部分插入和删除的复杂度O(n)

字典dict

理解为Python中的哈希表HashMap实现
访问、删除复杂度O(1)
判断元素x in dict复杂度O(1)
缺点:元素无序

注意,从Python 3.6开始,普通字典dict也保证插入顺序不变
dict可直接实现有序字典OrderedDict的功能;
若只关注key而忽略value,dict可直接实现有序集的功能

集合set

理解为Python中的哈希集合HashSet实现,底层实现基于哈希表dict
访问、删除复杂度O(1)
判断元素x in dict复杂度O(1)
缺点:元素无序

参考:【python】list,dict,set的时间复杂度

HashMap:基于哈希表的 Map 接口的实现

HashMap=数组+链表,数组是主体,链表用于解决哈希冲突
不看底层实现,HashMap就是一个(用链表解决哈希冲突的)散列表,在Python中就是字典

有序字典LinkedHashMap与LRU算法

LRU缓存淘汰算法,全称LeastRecentlyUsed,即认为最近使用的数据是最有用的,在生活中的例子就是手机后台任务:各后台任务按照使用的顺序排列,若缓存满了后再打开新任务,就会优先删除最久没使用的任务。

LeetCode 146 “LRU 缓存”

要求编写能够实现LRU的数据结构,初始化时接受参数capacity,对外提供put和get两个接口

  • 要使插入、删除复杂度为O(1)
  • 要使cache中的数据有序,从而删除最久未使用的数据
  • 要能快速查询任意元素key的value

链表满足第一、二个要求,哈希表(字典)满足第三个要求,因此将两者结合,就得到有序字典LinkedHashMap,它由哈希表和双向链表组成

哈希链表LinkedHashMap可以理解为能保存(和修改)插入顺序的字典,在Python中提供内置实现collections.OrderedDict

LinkedHashMap的实现细节

  • 队尾保存最近使用的元素,队首保存最久没使用的元素
  • 必须使用双向链表,因为删除一个节点,需要获得其前驱和后继节点的指针
  • 哈希表中的key指向链表中的整个节点对象
  • 双向链表中每个节点的属性有四个:key,val,next,prev,靠近表尾的元素是最近使用的
  • 必须在双向链表中同时保存key和val,因为每当删除链表中头部最不常用的节点时,需要获取对应的key,从而在哈希表中同步删除key
    在这里插入图片描述

用Python实现LinkedHashMap

LinkedHashMap对应于Python中的collections.OrderedDict
提供的方法:

  • 基本操作方法同dict
  • OrderedDict.popitem(last=True):删除队列两端的元素并返回其值,last为True时,删除队尾元素
  • OrderedDict.move_to_end(key, last=True)将某个key对应的节点移动到队列两端,last为True时,移动到队尾

实现代码:用OrderedDict实现LRU

from collections import OrderedDict


class LRUCache:
    # 队尾保存最近使用的元素,队首保存最久没使用的元素

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.linkedHashMap = OrderedDict()

    def get(self, key: int) -> int:
        """如果关键字key存在于缓存中,则返回关键字的值并将节点提至队尾,否则返回-1"""
        if key not in self.linkedHashMap:
            return -1
        self.linkedHashMap.move_to_end(key, last=True)
        return self.linkedHashMap[key]

    def put(self, key: int, value: int) -> None:
        """如果key已存在,则更新value并将节点提至队尾;
        如果不存在,则向缓存中插入该键值对,
        如果插入时元素数量超过capacity,则逐出最久未使用的元素"""
        if key in self.linkedHashMap:
            self.linkedHashMap[key] = value  # 更新value
            self.linkedHashMap.move_to_end(key, last=True)
            return
        if len(self.linkedHashMap) >= self.capacity:
            self.linkedHashMap.popitem(last=False)  # 删除最久未使用元素
        self.linkedHashMap[key] = value
        return

# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

注意,从Python 3.6开始,普通字典dict也保证插入顺序不变,“dict就是OrderedDict”
要将一个元素移动到字典的末尾,pop删除后重新插入即可
要将一个元素移动到字典的开头,pop后,单独创建一个仅含一个键值对的字典,然后用dict1.update(dict2)拼接两个字典

用dict实现同样的功能:

class LRUCache:
    # 队尾保存最近使用的元素,队首保存最久没使用的元素

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.linkedHashMap = dict()

    def get(self, key: int) -> int:
        """如果关键字key存在于缓存中,则返回关键字的值并将节点提至队尾,否则返回-1"""
        if key not in self.linkedHashMap:
            return -1
        val = self.linkedHashMap.pop(key)  # 元素移动到队尾
        self.linkedHashMap[key] = val
        return val

    def put(self, key: int, value: int) -> None:
        """如果key已存在,则更新value并将节点提至队尾;
        如果不存在,则向缓存中插入该键值对,
        如果插入时元素数量超过capacity,则逐出最久未使用的元素"""
        if key in self.linkedHashMap:
            self.linkedHashMap.pop(key)  # 更新value,元素移动到队尾
            self.linkedHashMap[key] = value
            return
        if len(self.linkedHashMap) >= self.capacity:
            firstKey = list(self.linkedHashMap.keys())[0]
            self.linkedHashMap.pop(firstKey)  # 删除队首的最久未使用元素
        self.linkedHashMap[key] = value
        return

# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

LFU算法

LFU缓存淘汰算法,全称LeastFrequentlyUsed,把数据按照使用频次排序,淘汰访问频次最低的数据,而且如果多个数据访问频次相同,应删除最早插入的(最旧的)数据。

LeetCode 460 “LFU 缓存”

编写能够实现LFU的数据结构,初始化时接受参数capacity,对外提供put和get两个接口:

  • 应当维护每个数据被访问的频次freq,每当get或put访问某个key,其对应的freq就要加一
  • put插入时,若容量已满,应将freq最小的key删除,若同时有多个freq最小的key,应删除最旧的

根据需求构思

  1. get需要快速查询,用一个哈希表dict实现key-val的映射,下面简称KV表
  2. 需要维护元素key对应的freq
    一方面,需要freq到key的映射,从而随时快速删除freq最小的元素
    另一方面,需要key到freq的映射,从而能快速获取每个元素对应的freq
  3. 用一个哈希表dict实现key到freq的映射,下面简称KF表
  4. 用哈希表+有序集LinkedHashSet实现freq到key的映射,下面简称FK表
    有序集是为了在多个freq最小的key删除最旧的
  5. 一个细节:要删除freq最小的key,可以维护一个minFreq变量,避免每次删除时都要遍历所有freq值

第4点中使用有序集LinkedHashSet的理由:

  • 同一个freq可能对应多个key,因此freq应该映射到一个key的列表
  • freq对应的key列表应该保持时序,从而才能删除最旧的key

哈希表+普通链表似乎能满足上述要求(一对多映射、保持时序、快速删除),然而这里还包含一个隐含的需求:

  • 希望能快速(查找并)删除某freq对应的key列表中的任何一个key,因为每个key的freq在实际操作过程中都在不断变化,一个key被多访问一次,就应该从[freq对应的key列表]中删除,加到[freq+1对应的key列表]中

显然普通链表不能快速访问任意节点
因此我们需要哈希表+有序集LinkedHashSet实现freq到key的映射:外层为哈希表,其key为freq,value为[ key的有序集 ]

有序集LinkedHashSet

文章开头介绍了Python中的HashSet实现为set,但是它无法保持插入顺序

LinkedHashSet简单理解为能保持插入顺序的set集合,能快速访问和删除任意节点,且克服了set无序的缺点
LinkedHashSet结合了哈希集合和链表的优点,既能快速访问删除元素,又能保持元素的时序

有序集的Python实现

Python没有直接提供OrderedSet,但是可以间接用有序字典collections.OrderedDict实现有序集。

从功能上看,有序集就是有序字典OrderedDict的特例:
有序字典能保持key-val对的插入顺序,而字典的key是唯一的
因此,忽略有序字典中的值(将所有val设置为None),就实现了有序集
在这里插入图片描述
从图中可见,上半部分的哈希表实现“快速查找和访问”,下半部分的链表实现“快速删除和保持插入顺序”

不要忘记之前提过的,Python 3.6后,dict就是有序字典OrderedDict,因此,直接用字典dict代替OrderedDict创建有序集

>>> keywords = ['1', '2', '3', '2', '1', '3']
>>> d=dict.fromkeys(keywords,None)
>>> d
{'1': None, '2': None, '3': None}
>>> list(d.keys())[0]# 求首部元素
'1'

LFU的代码实现

要同时维护三个映射表的复杂情况,注意技巧才能避免出错:

  1. 不要试图上来直接实现所有细节,应该自顶向下,先用(未定义的)顶层函数写清楚主要逻辑框架,再实现具体函数细节
  2. 明确映射关系:一旦更新key的freq,要同步修改KF表和FK表
  3. 画程序流程图
class LFUCache:

    def __init__(self, capacity: int):
        self.capacity = capacity  # 缓存最大容量
        self.minFreq = 0  # 记录当前的最小频次
        self.keyToVal = dict()  # KV表,key到val的映射,哈希表
        self.keyToFreq = dict()  # KF表,key到freq的映射,哈希表
        self.freqToKeys = dict()  # FK表,freq到key的映射,哈希表但其value为有序集(也用dict实现)

    def get(self, key: int) -> int:
        """如果关键字key存在于缓存中,则返回关键字的值并将freq加一,否则返回-1"""

    def put(self, key: int, value: int) -> None:
        """如果key已存在,则更新value并将freq加一;
        如果不存在,则向缓存中插入该键值对,
        如果插入时元素数量超过capacity,则逐出freq最小的key(若有多个优先删除最旧的)"""

# Your LRUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

最终实现代码

class LFUCache:

    def __init__(self, capacity: int):
        self.capacity = capacity  # 缓存最大容量
        self.minFreq = 0  # 记录当前的最小频次
        self.keyToVal = dict()  # KV表,key到val的映射,哈希表
        self.keyToFreq = dict()  # KF表,key到freq的映射,哈希表
        self.freqToKeys = dict()  # FK表,freq到key的映射,哈希表但其value为有序集(也用dict实现)

    def get(self, key: int) -> int:
        """如果关键字key存在于缓存中,则返回关键字的值并将freq加一,否则返回-1"""
        if key not in self.keyToVal.keys():
            return -1
        self.increaseFreq(key)  # 增加key对应的freq
        return self.keyToVal[key]

    def put(self, key: int, value: int) -> None:
        """如果key已存在,则更新value并将freq加一;
        如果不存在,则向缓存中插入该键值对,
        如果插入时元素数量超过capacity,则逐出freq最小的key(若有多个优先删除最旧的)"""
        if self.capacity <= 0:
            return
        if key in self.keyToVal.keys():
            self.keyToVal[key] = value
            self.increaseFreq(key)  # 增加key对应的freq
            return
        if self.capacity <= len(self.keyToVal):
            self.removeMinFreqKey()
        # 插入键值对,注意要同时更新三个表
        self.keyToVal[key] = value
        self.keyToFreq[key] = 1
        self.freqToKeys.setdefault(1, dict())  # 如果没有,则初始化映射freq到有序集(用dict实现)
        orderedSet = self.freqToKeys[1]
        orderedSet[key] = None  # 向有序集插入新key

        self.minFreq = 1  # 插入新key由,最小的freq必为1

    def increaseFreq(self, key):
        """key对应的freq加一,同时更新KF表和FK表"""
        freqNow = self.keyToFreq[key]
        # 更新KF表
        self.keyToFreq[key] = freqNow + 1

        # 更新FK表:将key从旧的freq对应的列表中删除
        self.freqToKeys[freqNow].pop(key)
        # 若删除后列表空了,移除旧的freq
        if len(self.freqToKeys[freqNow]) == 0:
            self.freqToKeys.pop(freqNow)
            # 若freqNow恰好是minFreq,应更新minFreq
            if freqNow == self.minFreq:
                self.minFreq += 1

        # 更新FK表:将key加入到freqNow+1对应的列表中
        self.freqToKeys.setdefault(freqNow + 1, dict())  # 如果没有,则初始化映射则初始化映射freq到有序集到有序集(用dict实现)
        orderedSet = self.freqToKeys[freqNow + 1]
        orderedSet[key] = None  # 向有序集插入新key
        return

    def removeMinFreqKey(self):
        """移除freq最小的key(若有多个优先删除最旧的)"""
        # freq最小的key列表
        keyList = self.freqToKeys[self.minFreq]  # 有序集,本质是一个有序字典
        # 其中应该删除最早插入的key
        deletedKey = list(keyList.keys())[0]

        # 更新KV表
        self.keyToVal.pop(deletedKey)

        # 更新KF表
        self.keyToFreq.pop(deletedKey)

        # 更新FK表
        keyList.pop(deletedKey)
        if len(keyList) == 0:
            self.freqToKeys.pop(self.minFreq)
            # 这里无需更新minFreq,因为removeMinFreqKey仅在put插入新元素时调用,put末尾必会将minFreq置为1

        return

# Your LRUCache object will be instantiated and called as such:
# obj = LFUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

类似题目

LeetCode 432. 全 O(1) 的数据结构
设计一个数据结构,能对字符串的计数增/减,能获得cnt最大/最小的字符串

这一类型的题目一般都是使用 哈希表 + 双向链表 的方式来实现。
其中,哈希表存储 key->Node 的映射关系,双向链表按频率排序,对于这道题key是字符串,val是出现次数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值