lru是least recently used的縮寫,即最近最少使用,是一種常用的頁面置換演算法,選擇最久未使用的頁面予以淘汰。lru是一種快取淘汰策略,它認為最近使用的資料就是有用的,最久沒使用的資料就是沒用的,所以在當容量滿了之後,會先淘汰掉最久沒使用的資料,騰出空間來放新資料。
leetcode的146題就要求我們設計這樣乙個類,要求我們在o(1)
時間複雜度內完成。
根據lru的定義,我們總結出lru的操作規則:
在使用資料(get/put)後,那麼該資料就是最近使用的。
當容量滿了之後,要刪除掉最久沒使用的資料。
根據上面的規則,我們設計的cache應該滿足以下要求:
保證新增的資料有時序
,即能夠體現他們加入的時間順序,來區分最近使用和最久未使用的
可以在cache中快速查詢某個key是否存在,並獲取對應的value
在需要刪除時,可以在cache快速找到某個key,對其進行快速刪除
每次訪問某個key時,要將其提公升為最近使用的,那麼就需要改變其在cache裡的存放位置,也就是說cache需要能在任意位置快速插入和刪除
那麼什麼資料結構符合上述條件?雜湊表支援快速查詢,但其資料存放無順序;鍊錶存放資料有順序,支援快速插入和刪除,但其查詢效率低。因此把他兩結合起來,就有了linkedhashmap
(雜湊鍊錶)。
lru快取的核心資料結構就是雜湊鍊錶,雙向鍊錶和雜湊表的結合體。
我們使用雙向鍊錶來存放資料,以保持其加入順序;通過雜湊表來儲存key到鍊錶節點的對映,以支援快速查詢和刪除。
那麼如何區分資料是最近使用還是最久未使用的呢?我們每次在新增資料時可以把資料新增到鍊錶尾部,每次在訪問資料(get/put已有key)時把該資料移動到鍊錶尾部。這樣的話鍊錶尾部節點就是最近使用的資料,鍊錶頭部就是最久沒使用的,當容量滿了之後,就刪除鍊錶的頭部節點即可。
接下來我們就要實現自己的資料結構了,手寫乙個雙向鍊錶,使用內建的hashmap。
其實我們的雙向鍊錶只要滿足上述操作要求即可,不需要太多的功能。那麼需要哪幾個操作呢?
新增資料到鍊錶尾部(新增新資料時、提公升資料為最近使用時)
對給定節點進行刪除(提公升資料為最近使用時需要移動節點)
移除頭部節點(容量滿了之後,要刪除最久未使用的節點)
我們可以在煉表裡新增兩個節點:head/tail節點,分別用來做鍊錶頭部和尾部,這兩個是啞結點,這樣在運算元據時會更加方便,在鍊錶頭部和尾部操作時不需要做特殊處理。
/**
* 雙向鍊錶節點
*/class
dlnode
public
dlnode
(int key,
int val)
}/**
* 雙向鍊錶
*/class
doublelink
// 在鍊錶尾部插入節點
dlnode addlast
(dlnode x)
// 移除煉表裡的節點,可以看到**很簡介,這也是為什麼使用雙向鍊錶的原因
public dlnode remove
(dlnode node)
// 移除第乙個節點
public dlnode removefirst()
}
雙向鍊錶就設計完成了。接下來我們來看如何結合hashmap完成cache的操作。
get是對節點進行訪問的過程,故需要將其變為最近使用的。
若該key不存在,返回-1
該key存在,將其變成最近使用的,即移動到鍊錶尾部
返回該key對應的value
我們先把邏輯結構寫出來,再慢慢去填充。
public
intget
(int key)
可以看到,get操作並不複雜,主要是如何實現upgraderecently()
,即把該節點移動到鍊錶尾部。操作也很簡單:
在map中快速找到該節點
在鍊錶中對該節點進行刪除
將該節點新增到鍊錶尾部
void
upgraderecently
(int key)
put就稍微複雜點。有以下操作:
put時若該key已存在,則表示該節點被訪問了,需要將其變成最近使用的,同時要更新key對應的value;
若key不存在,則先判斷當前鍊錶長度是否等於容量大小,等於的話需要先淘汰節點,騰出空間後才能新增新資料。我們要淘汰的是鍊錶的第乙個節點。
把新新增的key和value,作為最近使用的資料,即新增到鍊錶尾部。
接下來我們同樣先把基本邏輯結構寫出來:
public
void
put(
int key,
int value)
//如果超過容量則需要先刪除最久未使用的節點
if(map.
size()
>=capacity)
addrecently
(key,value)
;}
upgraderecently()
我們上面已經說過了。接下來我們看移除最久未使用的節點:
很簡單,移除第乙個節點
在map裡移除該節點的對映
void
removeleastused()
再來看看如何新增新節點addrecently
:
因為新節點就是我們最近使用的,所以把它新增到鍊錶尾部
注意要在map裡新增key對節點的對映
void
addrecently
(int key,
int val)
完整**:
class
lrucache
public
dlnode
(int key,
int val)
}class
doublelink
dlnode addlast
(dlnode x)
public dlnode remove
(dlnode node)
public dlnode removefirst()
} hashmap
map =
newhashmap
<
>()
; doublelink doublelink;
int capacity =1;
public
lrucache
(int capacity)
//提公升節點到鍊錶尾部,表示是最近使用的
public
intget
(int key)
public
void
put(
int key,
int value)
//如果超過容量則需要先刪除最久未使用的節點
if(map.
size()
>=capacity)
addrecently
(key,value);}
void
upgraderecently
(int key)
void
addrecently
(int key,
int val)
void
removeleastused()
}/**
* your lrucache object will be instantiated and called as such:
* lrucache obj = new lrucache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
至此,lru演算法已經完成了,且操作都是常數級時間複雜度o(1)
。其實lru的邏輯並不難理解,其核心資料結構是雜湊鍊錶,一種能進行快速查詢、刪除,保持key新增順序的雜湊表。注意在get和put的邏輯處理即可。 手撕演算法 adaboost
adaboost是典型的boosting演算法。boosting演算法的核心思想是 上乙個模型對單個樣本 的結果越差,下個模型越重視這個樣本 增大該樣本的權重,加大模型 錯的成本 提公升樹就是每個模型都是決策樹,提公升樹種效果比較好的是gbdt和xgboost,入門是adaboost。adaboos...
手撕演算法 PCA
pca,principle component analysis。lda,linear discriminant analysis 首先說一下pca和lda的區別,二者都是降維的方法,pca的主要思想是降維後各個樣本點的方差之和最大,也就是各個樣本點要盡量的區分開。而lda的思想是降維後同類的樣本要...
手撕演算法 排序
時間複雜度o n 2 o n 2 o n2 空間複雜度 o 1 穩定 從第乙個元素開始,認為左邊的序列是有序的,從有序部分的最後乙個向前比較,如果當前元素小於有序部分就交換,否則比較下乙個元素。function insertmerge arr else return arr let arr 1 5,...