面試官:來了,老弟,LRU快取實現一下?
我:直接LinkedHashMap就好了。
面試官:不要用現有的實現,自己實現一個。
我:.....
面試官:回去等訊息吧....
大家好,我是程式設計師學長,今天我們來聊一聊LRU快取問題。
Tips: LRU在計算機軟體中無處不在,希望大家一定要了解透徹。
問題描述
設計LRU(最近最少使用)快取結構,該結構在構造時確定大小,假設大小為K,並有如下兩個功能
1. set(key, value):將記錄(key, value)插入該結構
2. get(key):返回key對應的value值
分析問題
根據問題描述,我們可以知道LRU包含兩種操作,即Set和Get操作。
對於Set操作來說,分為兩種情況。
1. 快取中已經存在。把快取中的該元素移動到快取頭部。
2. 如果快取中不存在。把該元素新增到快取頭部。如果此時快取的大小超過限制的大小,需要刪除快取中末尾的元素。
對於Get操作來著,也分為兩種情況。
- 快取中存在。把快取中的該元素移動到快取頭部。並返回對應的value值。
- 快取中不存在。直接返回-1。
綜上所述:對於一個LRU快取結構來說,主要需要支援以下三種操作。
- 查詢一個元素。
- 在快取末尾刪除一個元素。
- 在快取頭部新增一個元素。
所以,我們最容易想到的就是使用一個連結串列來實現LRU快取。
我們可以維護一個有序的單鏈表,越靠近連結串列尾部的結點是越早訪問的。
當我們進行Set操作時,我們從連結串列頭開始順序遍歷。遍歷的結果有兩種情況。
- 如果此資料之前就已經被快取在連結串列中,我們遍歷得到這個資料對應的結點,然後將其從這個位置移動到連結串列的頭部。
- 如果此資料不在連結串列中,又會分為兩種情況。如果此時快取連結串列沒有滿,我們直接將該結點插入連結串列頭部。如果此時快取連結串列已經滿了,我們從連結串列尾部刪除一個結點,然後將新的資料結點插入到連結串列頭部。
當我們進行Get操作時,我們從連結串列頭開始順序遍歷。遍歷的結果有兩種情況。
- 如果此資料之前就已經被快取在連結串列中,我們遍歷得到這個資料對應的結點,然後將其從這個位置移動到連結串列的頭部。
- 如果此資料之前不在快取中,我們直接返回-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快取不論在工作中還是面試中,我們都會經常碰到。希望這篇文章能對你有所幫助。
今天,我們就聊到這裡。更多有趣知識,請關注公眾號【程式設計師學長】。我給你準備了上百本學習資料,包括python、java、資料結構和演算法等。如果需要,請關注公眾號【程式設計師學長】,回覆【資料】,即可得。
你知道的越多,你的思維也就越開闊,我們下期再見。