LRU緩存淘汰算法: 緩存是一種提升數據讀取性能的技術,在硬件設計、軟件發開發中都有着很是普遍的應用,好比常見的CPU緩存、數據庫緩存、瀏覽器緩存等等。 緩存的大小有限,當緩存被用滿時,那些數據應該被清理出去,哪些數據應該被保留?這就須要緩存淘汰策略來決定,常見的緩存淘汰策略有三種:先進先出策略FIFO(First In First Out),最少使用策略LFU(Least Frequently Used),最近最少使用策略LRU(Least Recently Used)。 java
那麼,如何使用鏈表來實現LRU緩存淘汰策略呢?算法
數組須要一塊連續的內存空間來存儲,相對於數組,鏈表並不須要連續的內存塊,它經過「指針」將一組零散的內存塊串聯起使用,鏈表的結構有不少,最多見的三種鏈表結構有:單鏈表、雙向鏈表、循環鏈表。 數據庫
鏈表經過指針將一組零散的內存塊串聯在一塊兒,其中,咱們把內存塊稱爲鏈表的「結點」。爲了將鏈表的全部結點串起來,每一個結點除了存儲數據以外,還須要記錄鏈表上邊下一個結點的地址。咱們將這個記錄下一個結點的指針叫作後繼指針next。 在單鏈表中,有兩個結點比較特殊,分別是第一個結點和最後一個結點,咱們習慣性地將第一個結點叫作頭結點,最後一個結點叫作尾結點。其中,頭結點用來記錄鏈表的基地址,咱們能夠經過它來遍歷整個鏈表。而尾結點的特殊之處在於:指針不是指向下一個結點,而是指向一個空地址NULL,表示這是鏈表的最後一個結點。 數組
鏈表也同數組同樣支持查找、插入、刪除操做。數組在進行插入、刪除的操做的時候,爲了保持內存空間的連續性,須要作大量的數據搬移,因此時間複雜度是O(n)。而在鏈表中插入或者刪除一個數據,並不須要爲了保持內存的連續性而搬移結點,由於鏈表的存儲空間自己就是不連續的。針對鏈表的插入和刪除操做,咱們只須要考慮相鄰結點的指針改變,因此,鏈表進行插入和刪除的時間複雜度爲O(1)。瀏覽器
有利就有弊,鏈表想要隨機訪問第K個元素,就沒有數組那麼高效了,因爲鏈表中的數據存儲並不是是連續的,因此沒法像數組那樣根據首地址和下標,經過尋址公式直接得到對應元素的內存地址,而是需要根據指針一個結點一個結點地依次遍歷,直到找到相應的節點。緩存
循環鏈表是一種特殊的單鏈表。它與單鏈表的惟一區別就在尾結點。單鏈表的尾結點指針指向空地址,表示這是最後的結點了。而循環鏈表的尾結點指針指向鏈表的頭結點,像一個環同樣首尾相連,因此叫作"循環"鏈表。數據結構
與單鏈表相比,循環鏈表的優勢是從鏈尾到鏈頭比較方便,當要處理的數據具備環型結構特色時,就特別適合採用循環鏈表,好比著名的約瑟夫問題。數據結構和算法
單鏈表只有一個方向,結點只有一個後繼指針next指向後面的節點。而雙向鏈表,顧名思義,它支持兩個方向,每一個結點不止有一個後繼指針next指向後面的結點,還有一個前驅指針prev指向前面的結點。因爲雙向鏈表須要額外的兩個空間來存儲後繼結點個前驅結點,因此,存儲一樣多的數據,雙向鏈表比單向鏈表佔用更多的內存空間。雖然浪費空間,可是雙向鏈表支持雙向遍歷,這帶來了雙向鏈表操做的靈活性。post
雙向鏈表適合解決哪一種問題?性能
雙向鏈表能夠支持O(1)時間複雜度的狀況下找到前驅結點,正是這樣一個緣由,使得雙向鏈表在某些狀況下的插入、刪除等操做比單鏈表簡單、高效。
在實際的軟件開發中,從鏈表中刪除一個數據無外乎這兩種狀況:
a)刪除結點中「值等於某個給定值」的結點;
b)刪除給定指針指向的結點
對於第一種狀況,不論是單鏈表仍是雙向鏈表,爲了查找到值等於給定值的結點,都須要從頭結點開始一個一個依次遍歷對比,直到找到值等於給定值的結點,而後再經過以前說的指針操做將其刪除。
儘管單純的刪除時間複雜度是O(1),可是遍歷查找的時間是主要的耗時點,對應的時間複雜度爲O(n)。根據時間複雜度分析中的加法法則,刪除值等於給定值的結點對應的鏈表操做的總時間複雜度爲O(n)。
對於第二種狀況,咱們已經找到了要刪除的結點,可是刪除某個結點q須要知道其前驅結點,而單鏈表並不支持直接獲取前驅結點,爲了找到前驅結點,咱們仍是要從頭結點開始遍歷鏈表,直到p->next=q,說明p是q的前驅結點,
可是對於雙向鏈表來講,這種狀況就比較有優點了,由於雙向鏈表中的結點已經保存了前驅結點的指針,不須要像單鏈表那樣遍歷。因此,針對第二種狀況,單鏈表刪除操做須要O(n)的時間複雜度,而雙向鏈表只須要在O(1)的時間複雜度內就搞定了。
同理,咱們在鏈表的某個指定結點前面插入一個結點,雙向鏈表能夠再O(1)的時間複雜度內搞定,而單向鏈表須要O(n)的時間複雜度。
java LinkedHashMap容器的實現原理使用了雙向鏈表這種數據結構。
這裏有一個很重要的思想:用空間換時間的設計思想。當內存空間充足的時候,若是咱們更加追求代碼的執行速度,咱們就能夠選擇空間複雜度相對較高,但時間複雜度相對較低的算法或者數據結構。
開篇緩存的例子就是應用了空間換時間的設計思想。
對於執行較慢的程序,能夠經過消耗更多的內存(空間換時間)來進行優化,而消耗過多內存的程序,能夠經過消耗更多的時間(時間換空間)來下降內存的消耗。
循環鏈表和雙向鏈表能夠整合成新的版本:雙向循環鏈表。
數組:插入刪除->O(n),隨機訪問->O(1)
鏈表:插入刪除->O(1),隨機訪問->O(n)
數組和鏈表的比較,並不能侷限於時間複雜度,並且在實際的軟件開發中,不能僅僅利用複雜度就決定使用哪一個數據結構來存儲數據。
數組簡單易用,在實現上使用的是連續的內存空間,能夠藉助CPU的緩存機制,預讀數組中的數據,因此訪問效率更高。而鏈表在內存中並非連續存儲,因此對CPU緩存不友好,沒辦法預讀。
數組的缺點是大小固定,一經聲明就要佔用整個塊連續內存空間。若是聲明的數組過大,系統沒有足夠的連續內存空間分配給它,致使「內存不足(out of memory)」 。若是聲明的數組太小,則可能出現不夠用的狀況,這是隻能再申請一個更大的內存空間,把原數組拷貝進去,很是費時。鏈表自己沒有大小的限制,自然地支持動態擴容。
維護一個單鏈表,越靠近鏈表尾部的結點是越早以前訪問的,當有一個新的數據被訪問時,從鏈表頭開始順序遍歷鏈表。
a)若是此數據以前已經被緩存在鏈表中,咱們遍歷獲得這個數據對應的結點,並將其從原來的位置刪除,而後再插入到鏈表的頭部。
b)若是此數據不在緩存鏈表中,又能夠分爲兩種狀況:
b1)若是此時緩存沒滿,直接將此結點插入到鏈表的頭部,
b2)若是此時緩存已滿,則將鏈表尾結點刪除,將新的數據結點插入鏈表的頭部。
在不用其餘數據結構優化的狀況下,這種方案的緩存訪問的時間複雜度是O(n)。
鏈表和數據同樣,也是很是基礎、很是經常使用的數據結構。不過鏈表要比數組稍微複雜,從普通的單鏈表衍生出來好幾種鏈表結構,好比雙向鏈表、循環鏈表、雙向循環鏈表。
和數組相比,鏈表更適合插入、刪除操做頻繁的場景,查詢的時間複雜度較高。在具體的軟件開發中,要對數組和鏈表的各類性能進行對比,綜合來選擇使用二者中的哪個。