面試官:來了,老弟,LRU快取實現一下?

我:直接LinkedHashMap就好了。

面試官:不要用現有的實現,自己實現一個。

我:.....

面試官:回去等訊息吧....


大家好,我是程式設計師學長,今天我們來聊一聊LRU快取的問題。

Tips: LRU在計算機軟體中無處不在,希望大家一定要了解透徹。

問題描述

  1. 設計LRU(最近最少使用)快取結構,該結構在構造時確定大小,假設大小為K,並有如下兩個功能
  2. 1. set(key, value):將記錄(key, value)插入該結構
  3. 2. get(key):返回key對應的value

分析問題

根據問題描述,我們可以知道LRU快取中包含兩種操作,即Set和Get操作。

對於Set操作來說,分為兩種情況。

  1. 如果快取中已經存在。把快取中對應的該元素移動到快取頭部。
  2. 如果快取中不存在。把該元素新增到快取頭部。此時如果快取的大小超過限制的大小,需要刪除快取中末尾的元素。

對於Get操作來說,也分為兩種情況。

  1. 如果快取中存在。把快取中的該元素移動到快取頭部,並返回對應的value值。
  2. 如果快取中不存在。直接返回-1。

綜上所述:對於一個LRU快取來說,主要包含以下三種操作。

  1. 查詢一個元素。
  2. 在快取末尾刪除一個元素。
  3. 在快取頭部新增一個元素。

所以,我們最容易想到的就是使用一個連結串列來實現LRU快取。我們可以維護一個有序的單鏈表,越靠近連結串列尾部的結點是越早訪問的。當我們進行Set操作時,我們從連結串列頭開始順序遍歷。遍歷的結果有兩種情況。

  1. 如果此資料已經在連結串列中,我們遍歷得到這個資料對應的結點,然後將其從這個位置移動到連結串列的頭部。
  2. 如果此資料不在連結串列中,又會分為兩種情況。如果此時連結串列沒有滿,我們直接將該結點插入到連結串列頭部。如果此時連結串列已經滿了,我們從連結串列尾部刪除一個結點,然後將新的資料結點插入到連結串列頭部。

當我們進行Get操作時,我們從連結串列頭開始順序遍歷。遍歷的結果有兩種情況。

  1. 如果此資料在連結串列中。我們遍歷得到這個資料對應的結點,然後將其從這個位置移動到連結串列的頭部,並返回這個結點對應的value。
  2. 如果此資料不在連結串列中。我們直接返回-1。

下面我們來看一下程式碼如何實現。

  1. class LinkedNode:
  2. def __init__(self, key=0, value=0):
  3. self.key = key
  4. self.value = value
  5. self.next = None
  6. class LRUCache():
  7. def __init__(self, capacity: int):
  8. # 使用偽頭部節點
  9. self.capacity=capacity
  10. self.head = LinkedNode()
  11. self.head.next=None
  12. self.size = 0
  13. def get(self, key: int) -> int:
  14. cur=self.head.next
  15. pre=self.head
  16. while cur!=None:
  17. if cur.key==key:
  18. pre.next = cur.next
  19. cur.next = self.head.next
  20. self.head.next = cur
  21. break
  22. pre=pre.next
  23. cur=cur.next
  24. if cur!=None:
  25. return cur.value
  26. else:
  27. return -1
  28. def put(self, key: int, value: int) -> None:
  29. cur = self.head.next
  30. pre = self.head
  31. #快取沒有元素,直接新增
  32. if cur==None:
  33. node = LinkedNode()
  34. node.key = key
  35. node.value = value
  36. self.head.next = node
  37. self.size = self.size + 1
  38. return
  39. #快取有元素,判斷是否存在於快取中
  40. while cur!=None:
  41. #表示已經存在
  42. if cur.key == key:
  43. #把該元素反正連結串列頭部
  44. cur.value=value
  45. pre.next = cur.next
  46. cur.next = self.head.next
  47. self.head.next = cur
  48. break
  49. #代表當前元素時最後一個元素
  50. if cur.next==None:
  51. #如果此時快取已經滿了,淘汰最後一個元素
  52. if self.size==self.capacity:
  53. pre.next=None
  54. self.size=self.size-1
  55. node=LinkedNode()
  56. node.key=key
  57. node.value=value
  58. node.next=self.head.next
  59. self.head.next=node
  60. self.size=self.size+1
  61. break
  62. pre = pre.next
  63. cur=cur.next

這樣我們就用單鏈表實現了一個LRU快取,我們接下來分析一下快取訪問的時間複雜度。對於Set來說,不管快取有沒有滿,我們都需要遍歷一遍連結串列,所以時間複雜度是O(n)。對於Get操作來說,也是需要遍歷一遍連結串列,所以時間複雜度也是O(n)。

優化

從上面的分析,我們可以看到。如果用單鏈表來實現LRU,不論是Set還是Get操作,都需要遍歷一遍連結串列,來查詢當前元素是否存在於快取中,時間複雜度為O(n),那我們可以優化嗎?我們知道,使用hash表,我們查詢元素的時間複雜度可以減低到O(1),如果我們可以用hash表,來替代上述的查詢操作,那不就可以減低時間複雜度嗎?根據這個邏輯,所以我們採用hash表和連結串列的組合方式來實現一個高效的LRU快取。

  1. class LinkedNode:
  2. def __init__(self, key=0, value=0):
  3. self.key = key
  4. self.value = value
  5. self.prev = None
  6. self.next = None
  7. class LRUCache:
  8. def __init__(self, capacity: int):
  9. self.cache = dict()
  10. self.head = LinkedNode()
  11. self.tail = LinkedNode()
  12. self.head.next = self.tail
  13. self.tail.prev = self.head
  14. self.capacity = capacity
  15. self.size = 0
  16. def get(self, key: int):
  17. #如果key不存在,直接返回-1
  18. if key not in self.cache:
  19. return -1
  20. #通過hash表定位位置,然後刪除,省去遍歷查詢過程
  21. node = self.cache[key]
  22. self.moveHead(node)
  23. return node.value
  24. def put(self, key: int, value: int) -> None:
  25. if key not in self.cache:
  26. # 如果key不存在,建立一個新的節點
  27. node = LinkedNode(key, value)
  28. # 新增進雜湊表
  29. self.cache[key] = node
  30. self.addHead(node)
  31. self.size += 1
  32. if self.size > self.capacity:
  33. # 如果超出容量,刪除雙向連結串列的尾部節點
  34. removed = self.removeTail()
  35. # 刪除雜湊表中對應的項
  36. self.cache.pop(removed.key)
  37. self.size -= 1
  38. else:
  39. node = self.cache[key]
  40. node.value = value
  41. self.moveHead(node)
  42. def addHead(self, node):
  43. node.prev = self.head
  44. node.next = self.head.next
  45. self.head.next.prev = node
  46. self.head.next = node
  47. def removeNode(self, node):
  48. node.prev.next = node.next
  49. node.next.prev = node.prev
  50. def moveHead(self, node):
  51. self.removeNode(node)
  52. self.addHead(node)
  53. def removeTail(self):
  54. node = self.tail.prev
  55. self.removeNode(node)
  56. return node

總結

LRU快取不論在工作中還是面試中,我們都會經常碰到。希望這篇文章能對你有所幫助。

你知道的越多,你的思維也就越開闊,我們下期再見。