算法不能脱离数据结构,各种算法本质上都是将基本的数据结构进行组合,把握好链表、二叉树等基本的数据结构的特性,进而构建上层算法
基础: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)
缺点:元素无序
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,应删除最旧的
根据需求构思
- get需要快速查询,用一个哈希表dict实现key-val的映射,下面简称KV表
- 需要维护元素key对应的freq
一方面,需要freq到key的映射,从而随时快速删除freq最小的元素
另一方面,需要key到freq的映射,从而能快速获取每个元素对应的freq - 用一个哈希表dict实现key到freq的映射,下面简称KF表
- 用哈希表+有序集LinkedHashSet实现freq到key的映射,下面简称FK表
有序集是为了在多个freq最小的key删除最旧的 - 一个细节:要删除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的代码实现
要同时维护三个映射表的复杂情况,注意技巧才能避免出错:
- 不要试图上来直接实现所有细节,应该自顶向下,先用(未定义的)顶层函数写清楚主要逻辑框架,再实现具体函数细节
- 明确映射关系:一旦更新key的freq,要同步修改KF表和FK表
- 画程序流程图
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是出现次数