天天看點

面試官:手撕LRU緩存了解一下#2021年底大盤點#

大家好,我是程式員學長~

關注公衆号【程式員學長】,回複【電子書】,免費獲得上百本計算機經典書籍

面試官:來了,老弟,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操作來說,也分為兩種情況。

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

綜上所述:對于一個LRU緩存來說,主要包含以下三種操作。

  1. 查找一個元素。
  2. 在緩存末尾删除一個元素。
  3. 在緩存頭部添加一個元素。

是以,我們最容易想到的就是使用一個連結清單來實作LRU緩存。我們可以維護一個有序的單連結清單,越靠近連結清單尾部的結點是越早通路的。當我們進行Set操作時,我們從連結清單頭開始順序周遊。周遊的結果有兩種情況。

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

當我們進行Get操作時,我們從連結清單頭開始順序周遊。周遊的結果有兩種情況。

  1. 如果此資料在連結清單中。我們周遊得到這個資料對應的結點,然後将其從這個位置移動到連結清單的頭部,并傳回這個結點對應的value。
  2. 如果此資料不在連結清單中。我們直接傳回-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緩存不論在工作中還是面試中,我們都會經常碰到。希望這篇文章能對你有所幫助。