大家好,我是程式員學長~
關注公衆号【程式員學長】,回複【電子書】,免費獲得上百本計算機經典書籍
面試官:來了,老弟,LRU緩存實作一下?
我:直接LinkedHashMap就好了。
面試官:不要用現有的實作,自己實作一個。
我:.....
面試官:回去等消息吧....
大家好,我是程式員學長,今天我們來聊一聊LRU緩存的問題。
Tips: LRU在計算機軟體中無處不在,希望大家一定要了解透徹。
問題描述
設計LRU(最近最少使用)緩存結構,該結構在構造時确定大小,假設大小為K,并有如下兩個功能
1. set(key, value):将記錄(key, value)插入該結構
2. get(key):傳回key對應的value值
分析問題
根據問題描述,我們可以知道LRU緩存中包含兩種操作,即Set和Get操作。
對于Set操作來說,分為兩種情況。
- 如果緩存中已經存在。把緩存中對應的該元素移動到緩存頭部。
- 如果緩存中不存在。把該元素添加到緩存頭部。此時如果緩存的大小超過限制的大小,需要删除緩存中末尾的元素。
對于Get操作來說,也分為兩種情況。
- 如果緩存中存在。把緩存中的該元素移動到緩存頭部,并傳回對應的value值。
- 如果緩存中不存在。直接傳回-1。
綜上所述:對于一個LRU緩存來說,主要包含以下三種操作。
- 查找一個元素。
- 在緩存末尾删除一個元素。
- 在緩存頭部添加一個元素。
是以,我們最容易想到的就是使用一個連結清單來實作LRU緩存。我們可以維護一個有序的單連結清單,越靠近連結清單尾部的結點是越早通路的。當我們進行Set操作時,我們從連結清單頭開始順序周遊。周遊的結果有兩種情況。
- 如果此資料已經在連結清單中,我們周遊得到這個資料對應的結點,然後将其從這個位置移動到連結清單的頭部。
- 如果此資料不在連結清單中,又會分為兩種情況。如果此時連結清單沒有滿,我們直接将該結點插入到連結清單頭部。如果此時連結清單已經滿了,我們從連結清單尾部删除一個結點,然後将新的資料結點插入到連結清單頭部。
當我們進行Get操作時,我們從連結清單頭開始順序周遊。周遊的結果有兩種情況。
- 如果此資料在連結清單中。我們周遊得到這個資料對應的結點,然後将其從這個位置移動到連結清單的頭部,并傳回這個結點對應的value。
- 如果此資料不在連結清單中。我們直接傳回-1。
下面我們來看一下代碼如何實作。
class LinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.next = None
class LRUCache():
def __init__(self, capacity: int):
# 使用僞頭部節點
self.capacity=capacity
self.head = LinkedNode()
self.head.next=None
self.size = 0
def get(self, key: int) -> int:
cur=self.head.next
pre=self.head
while cur!=None:
if cur.key==key:
pre.next = cur.next
cur.next = self.head.next
self.head.next = cur
break
pre=pre.next
cur=cur.next
if cur!=None:
return cur.value
else:
return -1
def put(self, key: int, value: int) -> None:
cur = self.head.next
pre = self.head
#緩存沒有元素,直接添加
if cur==None:
node = LinkedNode()
node.key = key
node.value = value
self.head.next = node
self.size = self.size + 1
return
#緩存有元素,判斷是否存在于緩存中
while cur!=None:
#表示已經存在
if cur.key == key:
#把該元素反正連結清單頭部
cur.value=value
pre.next = cur.next
cur.next = self.head.next
self.head.next = cur
break
#代表目前元素時最後一個元素
if cur.next==None:
#如果此時緩存已經滿了,淘汰最後一個元素
if self.size==self.capacity:
pre.next=None
self.size=self.size-1
node=LinkedNode()
node.key=key
node.value=value
node.next=self.head.next
self.head.next=node
self.size=self.size+1
break
pre = pre.next
cur=cur.next
這樣我們就用單連結清單實作了一個LRU緩存,我們接下來分析一下緩存通路的時間複雜度。對于Set來說,不管緩存有沒有滿,我們都需要周遊一遍連結清單,是以時間複雜度是O(n)。對于Get操作來說,也是需要周遊一遍連結清單,是以時間複雜度也是O(n)。
優化
從上面的分析,我們可以看到。如果用單連結清單來實作LRU,不論是Set還是Get操作,都需要周遊一遍連結清單,來查找目前元素是否存在于緩存中,時間複雜度為O(n),那我們可以優化嗎?我們知道,使用hash表,我們查找元素的時間複雜度可以減低到O(1),如果我們可以用hash表,來替代上述的查找操作,那不就可以減低時間複雜度嗎?根據這個邏輯,是以我們采用hash表和連結清單的組合方式來實作一個高效的LRU緩存。
class LinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = dict()
self.head = LinkedNode()
self.tail = LinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
self.capacity = capacity
self.size = 0
def get(self, key: int):
#如果key不存在,直接傳回-1
if key not in self.cache:
return -1
#通過hash表定位位置,然後删除,省去周遊查找過程
node = self.cache[key]
self.moveHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
# 如果key不存在,建立一個新的節點
node = LinkedNode(key, value)
# 添加進哈希表
self.cache[key] = node
self.addHead(node)
self.size += 1
if self.size > self.capacity:
# 如果超出容量,删除雙向連結清單的尾部節點
removed = self.removeTail()
# 删除哈希表中對應的項
self.cache.pop(removed.key)
self.size -= 1
else:
node = self.cache[key]
node.value = value
self.moveHead(node)
def addHead(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def removeNode(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def moveHead(self, node):
self.removeNode(node)
self.addHead(node)
def removeTail(self):
node = self.tail.prev
self.removeNode(node)
return node
總結
LRU緩存不論在工作中還是面試中,我們都會經常碰到。希望這篇文章能對你有所幫助。