一、首先实现一个双向链表
双向链表原理
1–>2–>3–>4–>5
上面是一个单向链表,每一个节点都有下一个节点的地址或引用,最后一个节点5下一个节点是null
1<–>2<–>3<–>4<–>5
而双向链表的每一个节点都有它上一个节点和下一个节点的地址或引用,头部有下一个节点引用,同理尾部节点只有上一个节点引用。
通过上面的指向可以发现,相对于单链表,双向链表除了可以快速找到一个节点的下一个节点,也可以快速的找到它的上一个节点,并且可以快速去掉链表中的某一个节点。
使用Python实现DoubleLinkedList类
# !-*- encoding=utf-8 -*-
class Node:
def __init__(self, key, value):
# 链表里存放键值对
self.key = key
self.value = value
# 需要具备上下两个节点的引用
self.prev = Node
self.next = Node
def __str__(self):
val = '{%d,%d}' % (self.key, self.value)
return val
def __repr__(self):
val = '{%d,%d}' % (self.key, self.value)
return val
class DoubleLinkedList:
def __init__(self, capacity=0xffff):
"""
需要头部指针head和尾部指针tail和容积
size用来存放已存节点
:param capacity: 默认int最大值65535
:return:
"""
self.capacity = capacity
self.head = None
self.tail = None
self.size = 0
# 从头部添加节点
def __add_head(self, node):
# 链表无节点,添加的节点就是链表头节点和尾节点
if not self.head:
self.head = node
self.tail = node
self.head.prev = None
self.head.next = None
# 不为空时,node的next指向头节点,下一节点的prev指向node,并维护node为头节点让它的prev指向null
else:
node.next = self.head
self.head.prev = node
self.head = node
self.head.prev = None
# 维护size++
self.size += 1
return node
# 往尾部添加节点
def __add_tail(self, node):
# 链表为空,添加的节点尾节点
if not self.tail:
self.head = node
self.tail = node
self.tail.prev = None
self.tail.next = None
# 不为空时,node的prev指向尾节点,上一节点的next指向node,并维护node为尾节点让它的next指向null
else:
self.tail.next = node
node.prev = self.tail
self.tail = node
self.tail.next = None
# 维护size++
self.size += 1
return node
# 从head端删除节点
def __del_head(self):
if not self.head:
return
node = self.head
# 如果节点有下一个节点
if self.head.next:
self.head = node.next
self.head.prev = None
else:
self.head = self.tail = None
self.size -= 1
return node
# 从尾部删除节点
def __del_tail(self):
if not self.tail:
return
node = self.tail
# 如果节点有前一个节点
if node.prev:
self.tail = node.prev
self.tail.next = None
else:
self.head = self.tail = None
self.size -= 1
return node
# 删除任意节点
def __remove(self, node):
# 如果node为空,默认删除尾部节点
if not node:
node = self.tail
if node == self.tail:
self.__del_tail()
elif node == self.head:
self.__del_head()
else:
node.prev.next = node.next
node.next.prev = node.prev
self.size -= 1
return node
"""
提供给外部使用的api
"""
# 默认头部删除
def pop(self):
return self.__del_head()
# 默认尾部添加
def append(self, node):
return self.__add_tail(node)
# 头部添加节点
def append_front(self, node):
return self.__add_head(node)
# 删除节点
def remove(self, node=None):
return self.__remove(node)
# 打印当前链表
def print(self):
p = self.head
line = ""
# p不为空
while p:
line += "%s" % p
p = p.next
if p:
line += "=>"
return line
if __name__ == '__main__':
link = DoubleLinkedList(10)
nodes = []
# 生成0-9的键值对数组用来测试
for i in range(10):
node = Node(i, i)
nodes.append(node)
link.append(nodes[0])
link.append(nodes[1])
link.append(nodes[2])
link.print()
link.pop()
link.print()
link.append(nodes[5])
link.append_front(nodes[7])
link.print()
link.remove(nodes[5])
link.print()
main函数中的测试结果
{0,0}=>{1,1}=>{2,2}
{1,1}=>{2,2}
{7,7}=>{1,1}=>{2,2}=>{5,5}
{7,7}=>{1,1}=>{2,2}
二、实现置换算法
内存置换算法
1.先进先出算法(FIFO)
他会把高速缓存看成是一个先进先出的队列,并且在发生置换的时候优先替换最先进入队列的字块
2.最近最少使用算法(LRU)
会优先淘汰掉一段时间内没有使用的字块
LRU有多种实现方法,一般使用双向链表实现,实现的时候会把当前访问的节点也就是字块置换到列表的最前面,保证链表的头节点是最近使用的。当需要淘汰的时候,把链表尾部的节点淘汰就可以了。
3.最不经常使用算法(LFU)
会优先淘汰掉最不经常使用的字块,需要额外的空间记录每个字块的使用频率。
1.FIFO缓存置换算法
当需要淘汰缓存的时候,把最先进入链表的节点进行淘汰。 即从尾部添加,头部淘汰
"""
实现FIFO缓存置换算法
"""
from DoubleLinkedList import DoubleLinkedList, Node
class FIFOCache:
def __init__(self, capacity):
"""
size记录已缓存大小
map保存key和node的映射关系
:param capacity: 当前缓存可容纳最大缓存数量
"""
self.capacity = capacity
self.size = 0
self.map = {}
self.list = DoubleLinkedList(self.capacity)
def get(self, key):
"""
判断key的节点是否在缓存里面,在返回value,不再返回-1
:param key:
:return:value或-1
"""
if key not in self.map:
return -1
else:
# 从map里拿出key所对应的node
node = self.map.get(key)
return node.value
def put(self, key, value):
"""
首先判断缓存容量 0直接返回说明缓存无法保存数据
判断key是否已经存在缓存里
存在把旧的node拿出来更新value 删除旧节点,添加新的节点
不存在先判断缓存是否满 (满先pop头部节点,在添加尾部,不满直接添加尾部)
"""
if self.capacity == 0:
return
if key in self.map:
node = self.map.get(key)
# 删除旧节点
self.list.remove(node)
# 新节点默认添加尾部
node.value = value
self.list.append(node)
else:
if self.size == self.capacity:
node = self.list.pop()
# 在本地map映射中删除节点
del self.map[node.key]
self.size -= 1
node = Node(key, value)
self.list.append(node)
# 同时本地映射缓存保存key和node
self.map[key] = node
self.size += 1
def print(self):
# 打印当前链表内容
print(self.list.print())
if __name__ == '__main__':
cache = FIFOCache(2)
cache.put(1, 1)
cache.print()
cache.put(2, 2)
cache.print()
print(cache.get(1))
cache.put(3, 3)
cache.print()
print(cache.get(4)) # 没有得到-1
cache.put(4, 4)
cache.print()
测试用例结果如下: 从左边淘汰,链表始终是2个节点。
{1,1}
{1,1}=>{2,2}
1
{2,2}=>{3,3}
-1
{3,3}=>{4,4}
2.LRU缓存置换算法
使用双向链表实现,如果每次使用,则把使用的节点放在链表头部,淘汰缓存时候,移除链表尾部节点。
from DoubleLinkedList import DoubleLinkedList, Node
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.map = {} # 保存key与节点的映射关系
self.list = DoubleLinkedList(self.capacity)
def get(self, key):
"""
如果节点在缓存里,把节点重新保存添加到头部,并删除原来节点
"""
if key in self.map:
node = self.map[key]
self.list.remove(node)
self.list.append_front(node)
return node.value
else:
return -1
def put(self, key, value):
# key在缓存里 把节点拿出来更新
if key in self.map:
node = self.map.get(key)
self.list.remove(node)
node.value = value
self.list.append_front(node)
else:
# 不在缓存创建新的节点
node = Node(key, value)
# 缓存已经满了且不再缓存里,删除掉尾部节点,变成了缓存未满
if self.list.size >= self.list.capacity:
old_node = self.list.remove()
self.map.pop(old_node.key)
self.list.append_front(node)
self.map[key] = node
def print(self):
print(self.list.print())
if __name__ == '__main__':
cache = LRUCache(2)
cache.put(1, 1)
cache.print()
cache.put(2, 2)
cache.print()
cache.put(3, 3)
cache.print()
print(cache.get(1)) # 此时1由于最少使用已经移除返回-1
print(cache.get(2)) # 此时获取2已经是再次使用2,2放在头部
cache.print()
测试结果如下
{1,1}
{2,2}=>{1,1}
{3,3}=>{2,2}
-1
2
{2,2}=>{3,3}
3.LFU缓存置换算法
淘汰的是最不经常使用的缓存,就需要使用额外的空间来记录节点的使用频次
举个例子:下表是一个capacity为6的缓存,已经存满6
node | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
freq | 5 | 2 | 1 | 5 | 4 | 1 |
这时,需要新加入一个缓存,按照使用频次,就应该淘汰节点3或6
由于节点3和6的使用频次相同都是1,不知道应该淘汰哪一个节点,所以记录频率时还需要额外的操作,将相同频率的节点归纳为一个新的双向链表,如下所示:
freq = 5 :1<=>4
freq = 4 :5
freq = 2 :2
freq = 1 :3<=>6
如此一来,淘汰的时候就可以先找到频率最低的链表,然后利用FIFO算法进行淘汰。
可以直观的看出来,LRU算法所有缓存都存放在一个双向链表里,而LFU则是把相同频率的节点连在一起,可以有多个链表。
from DoubleLinkedList import DoubleLinkedList, Node
class LFUNode(Node):
"""
满足最不经常使用算法的节点,继承自Node
"""
def __init__(self, key, value):
"""
重写构造方法
新增freq记录频次
"""
self.freq = 0
super(LFUNode, self).__init__(key, value)
class LFUCache:
def __init__(self, capacity):
"""
freq_map 频率的map 保存每一个频率以及它对应的双向链表
"""
self.capacity = capacity
self.size = 0
self.map = {}
# key: 频率, value: 频率对应的双向链表
self.freq_map = {}
def __update_freq(self, node):
"""
更新频次
"""
# 先获取频次
freq = node.freq
# 把节点从原来的双向链表中删除
node = self.freq_map[freq].remove(node)
if self.freq_map[freq].size == 0:
# 如果链表为0 删除链表
del self.freq_map[freq]
# 更新频次并添加节点
freq += 1
node.freq = freq
# 新频次不在freq_map中,需要新建链表
if freq not in self.freq_map:
self.freq_map[freq] = DoubleLinkedList()
self.freq_map[freq].append(node)
def get(self, key):
# 如果key不在映射
if key not in self.map:
return -1
node = self.map.get(key)
# 更新节点频率 返回新的节点值
self.__update_freq(node)
return node.value
def put(self, key, value):
# 容量为0 返回
if self.capacity == 0:
return
# 缓存命中 节点从映射中拿出来更新value(更新频次,在新的链表操作)
if key in self.map:
node = self.map.get(key)
node.value = value
self.__update_freq(node)
# 缓存没有命中
else:
# 缓存满了淘汰节点
if self.capacity == self.size:
# 取出频率最低
min_freq = min(self.freq_map)
# 摘掉频率最低链表的头部节点
node = self.freq_map[min_freq].pop()
# 同步本地映射
del self.map[node.key]
self.size -= 1
# 未满新建LFUNode 频次默认为1
node = LFUNode(key, value)
node.freq = 1
# 缓存中保存node
self.map[key] = node
# 不在频次链表中,需要新建链表
if node.freq not in self.freq_map:
self.freq_map[node.freq] = DoubleLinkedList()
# 在频次链表中,新增一个节点
node = self.freq_map[node.freq].append(node)
self.size += 1
def print(self):
print("##########")
for k, v in self.freq_map.items():
print("freq = {} : {}".format(k, self.freq_map[k].print()))
print("##########")
print()
if __name__ == '__main__':
cache = LFUCache(2)
cache.put(1, 1)
cache.put(2, 2)
cache.print()
print(cache.get(1)) # 获取1时使用1 1更新频次为2
cache.print()
cache.put(3, 3) # 添加3时,移除频次为1的2,3频次为1
cache.print()
print(cache.get(2)) # 2以及移除,返回-1
cache.print()
print(cache.get(3)) # 获取3,3和1的频次都是2
cache.print()
cache.put(4, 4) # 此时添加4,这时,3,1频次相同 FIFO删除1 并更新4频次为1
cache.print()
测试结果如下:
##########
freq = 1 : {1,1}=>{2,2}
##########
1
##########
freq = 1 : {2,2}
freq = 2 : {1,1}
##########
##########
freq = 1 : {3,3}
freq = 2 : {1,1}
##########
-1
##########
freq = 1 : {3,3}
freq = 2 : {1,1}
##########
3
##########
freq = 2 : {1,1}=>{3,3}
##########
##########
freq = 2 : {3,3}
freq = 1 : {4,4}
##########