你們好,這裏是《齊姐聊算法》系列之 LRU 問題。node
在講這道題以前,我想先聊聊「技術面試到底是在考什麼」這個問題。git
在人人都知道刷題的今天,面試官也都知道你們會刷題準備面試,代碼你們都會寫,那面試爲何還在考這些題?那爲何有些人代碼寫出來了還掛了?github
你們知道美國的大廠面試 80%是在考算法,這實際上是最近 5-10 年以谷歌、雅虎爲首才興起的;國內大廠對於算法的考察雖然沒有這麼狂熱,但也愈來愈重視了。web
那麼算法面試真的只是在考算法嗎?顯然不是。本質上考的是思考問題的方式,分析、解決問題的能力,以及和同事溝通交流的能力,看你可否主動推動去解決問題。面試
套路就是:算法
雖然說是套路,但未嘗不是一個高效的工做方式?數據庫
那拿到一個問題,首先應該是去 clarify 這個問題,由於工做就是如此,不像在刷題網站作題什麼都給你定義好了,面試官一般都不會一次性給你全部條件,而是須要你思考以後去問他。那經過這個環節,面試官就知道了你遇到問題是怎麼去思考的,你考慮的是否全面,怎麼去和別人溝通的,從此和你一塊兒工做的狀態是怎樣的。緩存
就像咱們平時工做時,須要和 product manager 不斷的 clarify 需求,特別是沒定義清楚的部分,反反覆覆的討論,也是磨刀不誤砍柴工。那這個過程,在我司可能就要 1-2 周,不會很着急的就開始,不然努力錯了方向就是南轅北轍,得不償失。那麼面試時也是同樣,代碼都寫完了面試官說這不是我想問的,那時候已經沒時間了,買單的是咱們本身。服務器
第二點分析思路就是重中之重了,也是本文的核心,會以 LRU Cache 這到經典題爲例,展現我是如何思考、分析的。數據結構
第三點寫代碼,沒什麼好說的,終究是須要落到實處的。
第四點跑測試,不少同窗可能會忘,因此若是你能主動提出 run test cases,過幾個例子檢驗一下,是很好的。
有些人說每道題我都作出來了,爲何仍是掛了?那照着這四點對比一下,看看是哪一個環節出了問題。
另外這道題爲何各大公司都喜歡考呢?
一是由於它可以多方面、多維度的考察 candidate:這道題考察的是基本功,考對數據結構理解使用,考能不能寫出 readable 的代碼。一場 45 分鐘-60 分鐘的面試,如何摸清楚 candidate 的真實水平,也是不容易的啊。
二是由於這道題可難可易,能夠簡單到像 Leetcode 上那樣把 API 什麼的都已經定義好了,也能夠難到把 System Design 的內容都包含進來,聊一下 Redis 中的近似 LRU 算法。
因此 follow up 就能夠無限的深刻下去,若是面試官想問的你都能回答的頭頭是道,那 strong hire 天然跑不掉。那有些同窗只會到第一層或者第二層,面試是優中選優的過程,其餘同窗會的比你多,溝通交流能力又好,天然就是別人拿 offer 了。
那今天就以這道題爲例,在這裏淺談一下個人思考過程,爲你們拋磚引玉,歡迎在留言區分享你的想法。
LRU = Least Recently Used 最近最少使用
它是一種緩存逐出策略 cache eviction policies[1]
LRU 算法是假設最近最少使用的那些信息,未來被使用的機率也不大,因此在容量有限的狀況下,就能夠把這些不經常使用的信息踢出去,騰地方。
好比有熱點新聞時,全部人都在搜索這個信息,那剛被一我的搜過的信息接下來被其餘人搜索的機率也大,就比前兩天的一個過期的新聞被搜索的機率大,因此咱們把好久沒有用過的信息踢出去,也就是 Least Recently Used 的信息被踢出去。
舉個例子:咱們的內存容量爲 5,如今有 1-5 五個數。
咱們如今想加入一個新的數:6
但是容量已經滿了,因此須要踢出去一個。
那按照什麼規則踢出去,就有了這個緩存逐出策略。好比:
FIFO (First In First Out)
這個就是普通的先進先出。
LFU (Least Frequently Used)
這個是計算每一個信息的訪問次數,踢走訪問次數最少的那個;若是訪問次數同樣,就踢走很久沒用過的那個。這個算法其實很高效,可是耗資源,因此通常不用。
LRU (Least Recently Used)
這是目前最經常使用了。
LRU 的規則是把很長時間沒有用過的踢出去,那它的隱含假設就是,認爲最近用到的信息之後用到的機率會更大。
那咱們這個例子中就是把最老的 1 踢出去,變成:
不斷迭代...
簡單理解就是:把一些能夠重複使用的信息存起來,以便以後須要時能夠快速拿到。
那至於它存在哪裏就不必定了,最多見的是存在內存裏,也就是 memory cache,但也能夠不存在內存裏。
使用場景就更多了,好比 Spring 中有 @Cacheable 等支持 Cache 的一系列註解。上個月我在工做中就用到了這個 annotation,固然是我司包裝過的,大大減小了 call 某服務器的次數,解決了一個性能上的問題。
再好比,在進行數據庫查詢的時候,不想每次請求都去 call 數據庫,那咱們就在內存裏存一些經常使用的數據,來提升訪問性能。
這種設計思想實際上是遵循了著名的「二八定律」。在讀寫數據庫時,每次的 I/O 過程消耗很大,但其實 80% 的 request 都是在用那 20% 的數據,因此把這 20% 的數據放在內存裏,就可以極大的提升總體的效率。
總之,Cache 的目的是存一些能夠複用的信息,方便未來的請求快速得到。
那咱們知道了 LRU,瞭解了 Cache,合起來就是 LRU Cache 了:
當 Cache 儲存滿了的時候,使用 LRU 算法把老傢伙清理出去。
說了這麼多,Let's get to the meat of the problem!
這道經典題你們都知道是要用 HashMap + Doubly Linked List,或者說用 Java 中現成的 LinkedHashMap,可是,爲何?你是怎麼想到用這兩個數據結構的?面試的時候不講清楚這個,不說清楚思考過程,代碼寫對了也沒用。
和在工做中的設計思路相似,沒有人會告訴咱們要用什麼數據結構,通常的思路是先想有哪些 operations,而後根據這些操做,再去看哪些數據結構合適。
那咱們來分析一下對於這個 LRU Cache 須要有哪些操做:
那第一個操做很明顯,咱們須要一個可以快速查找的數據結構,非 HashMap
莫屬,還不瞭解 HashMap 原理和設計規則的在公衆號內發消息「HashMap」,送你一篇爆款文章;
但是後面的操做 HashMap 就不頂用了呀。。。
來來來,咱們來數一遍基本的數據結構:
Array, LinkedList, Stack, Queue, Tree, BST, Heap, HashMap
在作這種數據結構的題目時,就這樣把全部的數據結構列出來,一個個來分析,有時候不是由於這個數據結構不行,而是由於其餘的數據結構更好。
怎麼叫更好?忘了咱們的衡量標準嘛!時空複雜度,趕忙複習遞歸那篇文章,公衆號內回覆「遞歸」便可得到。
那咱們的分析以下:
Array, Stack, Queue 這三種本質上都是 Array 實現的(固然 Stack, Queue 也能夠用 LinkedList 來實現。。),一會插入新的,一會刪除老的,一會調整下順序,array 不是不能作,就是得 O(n) 啊,用不起。
BST 同理,時間複雜度是 O(logn).
Heap 即使能夠,也是 O(logn).
LinkedList,有點能夠哦,按照從老到新的順序,排排站,刪除、插入、移動,均可以是 O(1) 的誒!可是刪除時我還須要一個 previous pointer 才能刪掉,因此我須要一個 Doubly LinkedList.
那麼咱們的數據結構敲定爲:
HashMap + Doubly LinkedList
選好了數據結構以後,還須要定義清楚每一個數據結構具體存儲的是是什麼,這兩個數據結構是如何聯繫的,這纔是核心問題。
咱們先想個場景,在搜索引擎裏,你輸入問題 Questions,谷歌給你返回答案 Answer。
那咱們就先假設這兩個數據結構存的都是 <Q, A>,而後來看這些操做,若是都很順利,那沒問題,若是有問題,咱們再調整。
那如今咱們的 HashMap 和 LinkedList 長這樣:
而後咱們回頭來看這四種操做:
操做 1,沒問題,直接從 HashMap 裏讀取 Answer 便可,O(1);
操做 2,新加入一組 Q&A,兩個數據結構都得加,那先要判斷一下當前的緩存裏有沒有這個 Q,那咱們用 HashMap 判斷,
但是,怎麼找 LinkedList 的這個 node 呢?一個個 traverse 去找並非咱們想要的,由於要 O(n) 的時間嘛,咱們想用 O(1) 的時間操做。
那也就是說這樣記錄是不行的,還須要記錄 LinkedList 中每一個 ListNode 的位置,這就是本題關鍵所在。
那天然是在 HashMap 裏記錄 ListNode 的位置這個信息了,也就是存一下每一個 ListNode 的 reference。
想一想其實也是,HashMap 裏沒有必要記錄 Answer,Answer 只須要在 LinkedList 裏記錄就能夠了。
以後咱們更新、移動每一個 node 時,它的 reference 也不須要變,因此 HashMap 也不用改動,動的只是 previous, next pointer.
那再一想,其實 LinkedList 裏也不必記錄 Question,反正 HashMap 裏有。
這兩個數據結構是相互配合來用的,不須要記錄同樣的信息。
更新後的數據結構以下:
這樣,咱們才分析出來用什麼數據結構,每一個數據結構裏存的是什麼,物理意義是什麼。
那其實,Java 中的 LinkedHashMap 已經作了很好的實現。可是,即使面試時可使用它,也是這麼一步步推導出來的,而不是一看到題目就知道用它,那一看就是背答案啊。
有同窗問我,若是面試官問我這題作沒作過,該怎麼回答?
答:實話實說。
真誠在面試、工做中都是很重要的,因此實話實說就行了。但若是面試官沒問,就沒必要說。。。
其實面試官是不 care 你作沒作過這道題的,由於你們都刷題,基本都作過,問這個問題沒有意義。只要你能把問題分析清楚,講清楚邏輯,作過了又怎樣?不少作過了題的人是講不清楚的。。。
那咱們再總結一下那四點操做:
第一個操做,也就是 get()
API,沒啥好說的;
二三四,是 put()
API,有點小麻煩:
畫圖的時候邊講邊寫,每一步都從 high level 到 detail 再到代碼,把代碼模塊化。
當年我把這圖畫出來,面試官就沒讓我寫代碼了,直接下一題了...
那若是面試官還讓你寫,就寫唄。。。
class LRUCache {
// HashMap: <key = Question, value = ListNode>
// LinkedList: <Answer>
public static class Node {
int key;
int val;
Node next;
Node prev;
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
Map<Integer, Node> map = new HashMap<>();
private Node head;
private Node tail;
private int cap;
public LRUCache(int capacity) {
cap = capacity;
}
public int get(int key) {
Node node = map.get(key);
if(node == null) {
return -1;
} else {
int res = node.val;
remove(node);
appendHead(node);
return res;
}
}
public void put(int key, int value) {
// 先 check 有沒有這個 key
Node node = map.get(key);
if(node != null) {
node.val = value;
// 把這個node放在最前面去
remove(node);
appendHead(node);
} else {
node = new Node(key, value);
if(map.size() < cap) {
appendHead(node);
map.put(key, node);
} else {
// 踢走老的
map.remove(tail.key);
remove(tail);
appendHead(node);
map.put(key, node);
}
}
}
private void appendHead(Node node) {
if(head == null) {
head = tail = node;
} else {
node.next = head;
head.prev = node;
head = node;
}
}
private void remove(Node node) {
// 這裏我寫的可能不是最 elegant 的,可是是很 readable 的
if(head == tail) {
head = tail = null;
} else {
if(head == node) {
head = head.next;
node.next = null;
} else if (tail == node) {
tail = tail.prev;
tail.next = null;
node.prev = null;
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
node.prev = null;
node.next = null;
}
}
}
}
/**
* 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);
*/
那再回到面試上來,爲何不少面試是以算法考察爲主的?這樣的面試道理何在?算法題面試真的能衡量一我的的工做能力嗎?(固然了,對於有些工做經驗的人還會考察系統設計方面的內容。)
這是我一直在思考的問題,工做以後愈發以爲,這樣的面試真的是有效的。
由於咱們須要的是可以去解決未知的問題的能力,知識可能會被遺忘,可是思考問題的方式方法是一直跟隨着咱們的,也是咱們須要不斷提升的。那麼在基本功紮實的前提下,有正確的方法和思路作指引,nothing is impossible.
若是你喜歡這篇文章,記得給我點贊留言哦~大家的支持和承認,就是我創做的最大動力,咱們下篇文章見!
我是小齊,紐約程序媛,終生學習者,天天晚上 9 點,雲自習室裏不見不散!
更多幹貨文章見個人 Github: https://github.com/xiaoqi6666/NYCSDE
Cache replacement policies: https://en.wikipedia.org/wiki/Cache_replacement_policies