手撕LRU演算法

2021-10-18 23:17:19 字數 4373 閱讀 1255

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,...