1. 程式人生 > 其它 >面試官:如何實現LRU?你學會了嗎?

面試官:如何實現LRU?你學會了嗎?

面試官:來了,老弟,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. 如果此資料之前就已經被快取在連結串列中,我們遍歷得到這個資料對應的結點,然後將其從這個位置移動到連結串列的頭部。
  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快取不論在工作中還是面試中,我們都會經常碰到。希望這篇文章能對你有所幫助。

今天,我們就聊到這裡。更多有趣知識,請關注公眾號【程式設計師學長】。

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