LeetCode 146 Hard 實現LRU演算法 2種解法 Python
阿新 • • 發佈:2019-01-13
""" My Method 演算法:雙向連結串列+字典 思路: 首先一定要回顧一下LRU演算法,其實就是一個優先佇列,最近使用過的在隊尾,時間久的在隊頭,該佇列 是有長度限制的,超出長度後最久未訪問的元素出佇列,類似於一種先進先出。然後訪問元素的時候,要將 該元素設為最近訪問過的元素 我這裡就設隊頭代表最近最久未訪問,佇列尾代表最近訪問的元素 很容易想到用線性表去做,但是也很容易發現,用線性表的話在get調整元素位置的時候會挪動大量的元素, 導致時間效率很低,這個時候可以很容易想到用連結串列結構去做,用連結串列的話將一個元素拆下來放到佇列尾是O1的 時間複雜度,所以只要設定一個佇列頭指標head和一個尾指標tail就OK了,因為拆卸連結串列需要知道節點的前驅, 所以用雙向連結串列來記錄節點的情況,同時需要設立一個字典dict記錄節點的記憶體地址,達到get時快速訪問的目的。 因此可以將LRU描述為用含head和tail指標的佇列進行put,get操作 get時: key在dict中時,將節點remove掉,再append回佇列,就完成了node的位置調整 put時: 要注意同key的node更新,也是先將節點remove掉,然後再append進來,只不過append的時候要改一下node.val 不同key的話: 先要考慮佇列是否滿,佇列長度滿則彈出隊首,否則的話計數加一,然後都要append新的node 所以要建立啊remove和append的輔助函式進行操作 remove拿到node的前驅就可以正常刪了,但是要注意node可能是tail,所以要將self.tail更新為前一個位置! append的話就比較直觀了,在self.tail後面追加這個node即可 再一個就是不要忘記刪除節點和新增節點的時候要更新dict 複雜度分析: 時間:O1 空間:ON """ class ListNode: def __init__(self, key, value): self.key = key self.val = value self.next = None self.prev = None class LRUCache: def __init__(self, capacity): """ :type capacity: int """ self.head = ListNode(0, 0) self.tail = self.head self.node_dict = {} self.capacity = capacity def get(self, key): """ :type key: int :rtype: int """ if key in self.node_dict: node = self.node_dict[key] node = self.del_node(node) self.append(node) return node.val else: return -1 def put(self, key, value): """ :type key: int :type value: int :rtype: void """ if key in self.node_dict: node = self.node_dict[key] node = self.del_node(node) node.val = value self.append(node) return if len(self.node_dict)== self.capacity: self.node_dict.pop(self.head.next.key) self.del_node(self.head.next) node = ListNode(key, value) self.append(node) self.node_dict[key] = node def del_node(self, node): if node == self.tail: self.tail = node.pre pre = node.pre next = node.next pre.next = node.next node.pre = None node.next = None if next != None: next.pre = pre return node def append(self, node): self.tail.next = node node.next = None node.pre = self.tail self.tail = self.tail.next """ ---------------------------------------------------------------- Disscussion Method 1 其實我也是看了Disscution後棄用了單鏈表,不過之前我的做法中用單鏈表+dict儲存pre和雙向連結串列也差不多,還不如 直接雙向連結串列更加直接。 下面的這個方法是設立了兩個啞節點headdummy和taildummy,而我的方法是head是dummy,tail是指向 實在的元素的,他這種寫法也是比較簡潔的 dummyhead-->1-->2-->3-->4-->taildummy 而我的是: dummyhead-->1-->2-->3-->4 <--tail 其實他這種兩個dummy的可能看起來會更方便一些,我這種的話tail每次要棟,而dummytail的話就像dummyhead一樣 dummyhead.next是真實的head dummytail.prev是真實的tail """ class Node(object): def __init__(self, key, val): self.key = key self.val = val self.next = None self.prev = None class LRUCache1(object): def __init__(self, capacity): self.dic = {} self.capacity = capacity self.dummy_head = Node(0, 0) self.dummy_tail = Node(0, 0) self.dummy_head.next = self.dummy_tail self.dummy_tail.prev = self.dummy_head def get(self, key): if key not in self.dic: return -1 node = self.dic[key] self.remove(node) self.append(node) return node.val def put(self, key, value): if key in self.dic: self.remove(self.dic[key]) node = Node(key, value) self.append(node) self.dic[key] = node if len(self.dic) > self.capacity: head = self.dummy_head.next self.remove(head) del self.dic[head.key] def append(self, node): tail = self.dummy_tail.prev tail.next = node node.prev = tail self.dummy_tail.prev = node node.next = self.dummy_tail def remove(self, node): prev = node.prev next = node.next prev.next = next next.prev = prev """ ---------------------------------------------------------------------------- Disscussion Method 2 Do as they say 演算法:小頂堆 思路: 用優先佇列,or 堆來保持LRU的狀態,就像do as they say一樣,用time來儲存時間戳,利用最小堆通過時間戳 的大小來儲存當前應當替換的頁面key 這樣思路就比較直觀了 用dic去儲存時間戳和key,value,dict[key] = (time,value) 所以當get的時候,就判斷key in dict or not,在的話就返回value並更新dict中儲存的key的時間戳 put的時候,也是先判斷key如果在dic中,就更新dict[key]中的時間戳為最新的 不在put中的話同樣先判斷len(dic) > capacity or not, 要做一些【清理工作】 具體來說就是檢查有沒有節點的時間戳在後面get 或者put的時候被更新了,因為最小堆不像連結串列一樣 可以直接找到節點的位置並且拆卸下來,所以這種思路就是曲線去找,如果當前應該彈出的堆頂元素髮現 其實後面被更新過了,堆頂的時間戳time和dict[key]中儲存的時間戳不一樣,那麼久更新堆頂元素, 一直更新直到找到一個元素確實是最小堆堆頂該彈出,就把他彈出就好了,heapq.pop,並且記得要del dict[key] 然後push新的時間戳的key,value就好了 複雜度: 時間:OK,K是佇列長度,肯定不是O1,get是O1,put不是O1,因為要做清理工作,但是至多對佇列內所有的K個元素每個都更新 一次,所以OK,效率也還是挺高的 """ from time import time import heapq class LRUCache2: def __init__(self, capacity): """ :type capacity: int """ self.cap = capacity self.hp = [] # (pre,vale) self.dic = {} # (now, value) def get(self, key): """ :type key: int :rtype: int """ if key not in self.dic: # never exits before return -1 pre, val = self.dic[key] new = time() self.dic[key] = (new, val) return val def put(self, key, value): """ :type key: int :type value: int :rtype: void """ now = time() if key in self.dic: self.dic[key] = (now, value) return if len(self.hp) >= self.cap: self.clean() self.dic[key] = (now, value) heapq.heappush(self.hp, (now, key)) def clean(self): while (True): (pre, key) = self.hp[0] (new, value) = self.dic[key] if new > pre: heapq.heapreplace(self.hp, (new, key)) else: heapq.heappop(self.hp) del self.dic[key] return