技術面試題是許多頂尖科技公司面試的主要內容,其中一些難題會令許多面試者望而卻步,但其實這些題是有合理的解決方法的。git
多數求職者只是通讀一遍問題和解法,囫圇吞棗。這比如試圖單憑看問題和解法就想學會微積分。你得動手練習如何解題,單靠死記硬背效果不彰。程序員
就本書的面試題以及你可能遇到的其餘題目,請參照如下幾個步驟。面試
(1) 儘可能獨立解題。本書後面有一些提示可供參考,但請儘可能不要依賴提示解決問題。許多題目確實難乎其難,可是不要緊,不要怕!此外,解題時還要考慮空間和時間效率。算法
(2) 在紙上寫代碼。在電腦上編程能夠享受到語法高亮、代碼完整、調試快速等種種好處,在紙上寫代碼則否則。經過在紙上多多實踐來適應這種狀況,並對在紙上編寫、編輯代碼之緩慢習覺得常。編程
(3) 在紙上測試代碼。就是要在紙上寫下通常用例、基本用例和錯誤用例等。面試中就得這麼作,所以最好提早作好準備。數組
(4) 將代碼照原樣輸入計算機。你也許會犯一大堆錯誤。請整理一份清單,羅列本身犯過的全部錯誤,這樣在真正面試時才能牢記在心。緩存
此外,儘可能多作模擬面試。你和朋友能夠輪流給對方作模擬面試。雖然你的朋友不見得受過什麼專業訓練,但至少能帶你過一遍代碼或者算法面試題。你也會在當面試官的體驗中,受益良多。服務器
許多公司關注數據結構和算法面試題,並非要測試面試者的基礎知識。然而,這些公司卻默認面試者已具有相關的基礎知識。數據結構
大多數面試官都不會問你二叉樹平衡的具體算法或其餘複雜算法。老實說,離開學校這麼多年,恐怕他們本身也記不清這些算法了。app
通常來講,你只要掌握基本知識便可。下面這份清單列出了必須掌握的知識。
數據結構 |
算法 |
概念 |
---|---|---|
鏈表 |
廣度優先搜索 |
位操做 |
樹、單詞查找樹、圖 |
深度優先搜索 |
內存(堆和棧) |
棧和隊列 |
二分查找 |
遞歸 |
堆 |
歸併排序 |
動態規劃 |
向量/數組列表 |
快排 |
大時間及空間 |
散列表 |
|
|
對於上述各項題目,務必掌握它們的具體用法、實現方法、應用場景以及空間和時間複雜度。
一種不錯的方法就是練習如何實現數據結構和算法(先在紙上,而後在電腦上)。你會在這個過程當中學到數據結構內部是如何工做的,這對不少面試而言都是不可或缺的。
你錯過上面那段了嗎?千萬不要錯過,這很是重要。若是對上面列出的某個數據結構和算法感受不能運用自如,就從頭開始練習吧。
其中,散列表是必不可少的一個題目。對這個數據結構,務必要成竹在胸。
下面這張表會在不少涉及可擴展性或者內存排序限制等問題上助你一臂之力。儘管不強求你記下來,但是記住總會有用。你至少應該輕車熟路。
2的冪 |
準確值(X) |
近似值 |
X字節轉換成MB、GB等 |
---|---|---|---|
7 |
128 |
|
|
8 |
256 |
|
|
10 |
1024 |
一千 |
1 K |
16 |
65 536 |
|
64 K |
20 |
1 048 576 |
一百萬 |
1 MB |
30 |
1 073 741 824 |
十億 |
1 GB |
32 |
4 294 967 296 |
|
4 GB |
40 |
1 099 511 627 776 |
一萬億 |
1 TB |
這張表能夠拿來作速算。例如,一個將每一個32位整數映射成布爾值的向量表能夠在一臺普通計算機內存中放下。那樣的整數有個。由於每一個整數只佔位向量表中的一位,共須要位(或者字節)來存儲該映射表,大約是千兆字節的一半,普通機器很容易知足。
在接受互聯網公司的電話面試時,不妨把表放在眼前,也許能派上用場。
下面的流程圖將教你如何逐步解決一個問題。要學以至用。你能夠從CrackingTheCodingInterview.com下載這個提綱及更多內容。
接下來我會詳述該流程圖。
面試本就困難。若是你沒法馬上得出答案,那也沒有關係,這很正常,並不表明什麼。
注意聽面試官的提示。面試官有時熱情洋溢,有時卻意興闌珊。面試官參與程度取決於你的表現、問題的難度以及該面試官的期待和個性。
當你被問到一個問題或者當你在練習時,按下面的步驟完成解題。
也許你之前聽過這個常規性建議:確保聽清楚題。但我給你的建議不止這一點。
固然了,你首先要保證聽清題,其次弄清楚模棱兩可的地方。
可是我要說的不止如此。
舉個例子,假設一個問題如下列其中一個話題做爲開頭,那麼能夠合理地認爲它給出的全部信息都並不是無緣無故的。
「有兩個排序的數組,找到……」
你極可能須要注意到數據是有序的。數據是否有序會致使最優算法截然不同。
「設計一個在服務器上常常運行的算法……」
在服務器上/重複運行不一樣於只運行一次的算法。也許這意味你能夠緩存數據,或者意味着你能夠瓜熟蒂落地對數據集進行預處理。
若是信息對算法沒影響,那麼面試官不大可能(儘管也不無可能)把它給你。
不少求職者都能準確聽清問題。可是開發算法的時間只有短短的十來分鐘,以致於解決問題的一些關鍵細節被忽略了。這樣一來不管怎樣都沒法優化問題了。
你的初版算法確實不須要這些信息。可是若是你陷入瓶頸或者想尋找更優方案,就回頭看看有沒有錯過什麼。
即便把相關信息寫在白板上也會對你大有裨益。
畫個例圖能顯著提升你的解題能力,儘管如此,還有如此多的求職者只是試圖在腦海中解決問題。
當你聽到一道題時,離開椅子去白板上畫個例圖。
不過畫例圖是有技巧的。首先你須要一個好例子。
一般狀況下,以一棵二叉搜索樹爲例,求職者可能會畫以下例圖。
這是個很糟糕的例子。第一,過小,不容易尋找模式。第二,不夠具體,二叉搜索樹有值。若是那些數字能夠幫助你處理這個問題怎麼辦?第三,這其實是個特殊狀況。它不只是個平衡樹,也是個漂亮、完美的樹,其每一個非葉節點都有兩個子節點。特殊狀況極具欺騙性,對解題無益。
實際上,你須要設計一個這樣的例子。
盡力作出最好的例子。若是後面發現你的例子不那麼正確,你應該修復它。
一旦完成了例子(其實,你也能夠在某些問題中調換7.3.1.2步和7.3.1.3步的順序),就給出一個蠻力法。你的初始算法不怎麼好也沒有關係,這很正常。
一些求職者不想給出蠻力法,是由於他們認爲此方法不只顯而易見並且糟糕透頂。可是事實是:即便對你來講垂手可得,也未必對全部求職者來講都這樣。你不會想讓面試官認爲,即便解出這一簡單算法對你來講也得絞盡腦汁。
初始解法很糟糕,這很正常,沒必要介懷。先說明該解法的空間和時間複雜度,再開始優化。
你一旦有了蠻力法,就應該努力優化該方法。如下技巧就有了用武之地。
(1) 尋找未使用的信息。你的面試官告訴過你數組是有序的嗎?你如何利用這些信息?
(2) 換個新例子。不少時候,換個不一樣的例子會讓你思路暢通,看到問題模式所在。
(3) 嘗試錯誤解法。低效的例子能幫你看清優化的方法,一個錯誤的解法可能會幫助你找到正確的方法。比方說,若是讓你從一個全部值可能都相等的集合中生成一個隨機值。一個錯誤的方法多是直接返回半隨機值。能夠返回任何值,可是可能某些值機率更大,進而思考爲何解決方案不是完美隨機值。你能調整機率嗎?
(4) 權衡時間、空間。有時存儲額外的問題相關數據可能對優化運行時間有益。
(5) 預處理信息。有辦法從新組織數據(排序等)或者預先計算一些有助於節省時間的值嗎?
(6) 使用散列表。散列表在面試題中用途普遍,你應該第一個想到它。
(7) 考慮可想象的極限運行時間(詳見7.9節)。
在蠻力法基礎上試試這些技巧,尋找BUD的優化點。
明確了最佳算法後,不要急於寫代碼。花點時間鞏固對該算法的理解。
白板編程很慢,慢得超乎想象。測試、修復亦如此。所以,要儘量地在一開始就確保思路近乎完美。
梳理你的算法,以瞭解它須要什麼樣的結構,有什麼變量,什麼時候發生改變。
僞代碼是什麼?若是你更願意寫僞代碼,沒有問題。可是寫的時候要小心。基本的步驟((1) 訪問數組。(2) 找最大值。(3) 堆插入。)或者簡明的邏輯(if
p < q
, movep
. else moveq
.)值得一試。可是若是你用簡單的詞語表明for
循環,基本上這段代碼就爛透了,除了代碼寫得快以外一無可取。
你若是沒有完全理解要寫什麼,就會在編程時舉步維艱,這會致使你用更長的時間才能完成,而且更容易犯大錯。
這下你已經有了一個最優算法而且對全部細節都瞭如指掌,接下來就是實現算法了。
寫代碼時要從白板的左上角(要省着點空間)開始。代碼儘可能沿水平方向寫(不要寫成一條斜線),不然會亂做一團,而且像Python那樣對空格敏感的語言來講,讀起來會雲裏霧裏,使人困惑。
切記:你只能寫一小段代碼來證實本身是個優秀的開發人員。所以,每行代碼都相當重要,必定要寫得漂亮。
寫出漂亮代碼意味着你要作到如下幾點。
{{1, 2, 3}, {4, 5, 6}, ...}
,不要浪費時間去寫初始化的代碼。能夠僞裝本身有個函數initIncrementalMatrix(int size)
,稍後須要時再回頭寫完它。todo
,這樣只需解釋清楚你想測試什麼就能夠了。StartEndPair
(或者Range
)對象看成list
返回。你不須要去把這個類寫完,大可假設有這樣一個類,後面若是有富裕時間再補充細節便可。for
循環)使用i
和j
就不對。可是,使用i
和j
時要多加當心。若是寫了相似於int i = startOfChild(array)
的變量名稱,可能還能夠使用更好的名稱,好比startChild
。然而,長的變量名寫起來也會比較慢。你能夠除第一次之外都用縮寫,多數面試官都能贊成。比方說你第一次能夠使用startChild
,而後告訴面試官後面你會將其縮寫爲sc
。
評價代碼好壞的標準因面試官、求職者、題目的不一樣而有所變化。因此只要專心寫出一手漂亮的代碼便可,盡人事、知天命。
若是發現某些地方須要稍後重構,就和麪試官商量一下,看是否值得花時間重構。一般都會獲得確定答覆,偶爾不是。
若是以爲一頭霧水(這很常見),就再回頭過一遍。
在現實中,不通過測試就不會簽入代碼;在面試中,未通過測試一樣不要「提交」。
測試代碼有兩種辦法:一種聰明的,一種不那麼聰明的。
許多求職者會用最開始的例子來測試代碼。那樣作可能會發現一些bug,但一樣會花很長時間。手動測試很慢。若是設計算法時真的使用了一個大而好的例子,那麼測試時間就會很長,但最後可能只在代碼末尾發現一些小問題。
你應該嘗試如下方法。
(1) 從概念測試着手。概念測試就是閱讀和分析代碼的每一行。像代碼評審那樣思考,在心中解釋每一行代碼的含義。
(2) 跳着看代碼。重點檢查相似x = length-2
的行。對於for
循環,要尤其注意初始化的地方,好比i = 1
。當你真的去檢查時,就很容易發現小錯誤。
(3) 熱點代碼。若是你編程經驗足夠豐富的話,就會知道哪些地方可能出錯。遞歸中的基線條件、整數除法、二叉樹中的空節點、鏈表迭代中的開始和結束,這些要反覆檢查才行。
(4) 短小精悍的用例。接下來開始嘗試測試代碼,使用真實、具體的用例。不要使用大而全的例子,好比前面用來開發算法的8元素數組,只須要使用3到4個元素的數組就夠了。這樣也能夠發現相同的bug,但比大的快多了。
(5) 特殊用例。用空值、單個元素、極端狀況和其餘特殊狀況檢測代碼。
發現了bug(極可能會)就要修復。但注意不要貿然修改。仔細斟酌,找出問題所在,找到最佳的修改方案,只有這樣才能動手。
這也許是我找到的優化問題最有效的方法了。BUD是如下詞語的首字母縮寫:
以上是最多見的3個問題,而面試者在優化算法時每每會浪費時間於此。你能夠在蠻力法中找找它們的影子。發現一個後,就能夠集中精力來解決。
若是這樣仍沒有獲得最佳算法,也能夠在當前最好的算法中找找這3類優化點。
瓶頸就是算法中拖慢總體運行時間的某部分。一般會以兩種方式出現。
一次性的工做會拖累整個算法。例如,假設你的算法分爲兩步,第一步是排序整個數組,第二步是根據屬性找到特定元素。第一步是,第二步是。儘管能夠把第二步時間優化到甚至,但那又有什麼用呢?聊勝於無而已。它不是當務之急,由於纔是瓶頸。除非優化第一步,不然你的算法總體上一直是。
你有一塊工做不斷重複,好比搜索。也許你能夠把它從降到甚至。這樣就大大加快了總體運行時間。
優化瓶頸,對總體運行時間的影響是立竿見影的。
舉個例子:有一個值都不相同的整數數組,計算兩個數差值爲的對數。例如,數組
{1, 7, 5, 9, 2, 12, 3}
,差值爲2,差值爲2的一共有4對:(1, 3)、(3, 5)、(5, 7)、(7, 9)。
用蠻力法就是遍歷數組,從第一個元素開始搜索剩下的元素(即一對中的另外一個)。對於每一對,計算差值。若是差值等於,計數加一。
該算法的瓶頸在於重複搜索對數中的另外一個。所以,這是接下來優化的重點。
怎麼才能更快地找到正確的另外一個?已知的另外一個,即 或。若是把數組排序,就能夠用二分查找來找到另外一個,個元素的話查找的時間就是。
如今,將算法分爲兩步,每一步都用時。接下來,排序構成新的瓶頸。優化第二步於事無補,由於第一步已經拖慢了總體運行時間。
必須徹底丟棄第一步排序數組,只使用未排序的數組。那如何在未排序的數組中快速查找呢?藉助散列表吧。
把數組中全部元素都放到散列表中。而後判斷或者是否存在。只是過一遍散列表,用時爲。
舉個例子:打印知足 的全部正整數解,其中、、、是1至1000間的整數。
用蠻力法來解會有四重for
循環,以下:
1. n = 1000 2. for a from 1 to n 3. for b from 1 to n 4. for c from 1 to n 5. for d from 1 to n 6. if a3 + b3 == c3 + d3 7. print a, b, c, d
用上面算法迭代、、、全部可能,而後檢測是否知足上述表達式。
在找到一個可行解後,就不用繼續檢查的其餘值了。由於的一次循環中只有一個值能知足。因此一旦找到可行解至少應該跳出循環。
1. n = 1000 2. for a from 1 to n 3. for b from 1 to n 4. for c from 1 to n 5. for d from 1 to n 6. if a3 + b3 = c3 + d3 7. print a, b, c, d 8. break // 跳出d循環
雖然該優化對運行時間並沒有改變,運行時間還是,但仍值得一試。
還有其餘無用功嗎?答案是確定的,對於每一個 ,均可以經過這個簡單公式獲得。
1. n = 1000 2. for a from 1 to n 3. for b from 1 to n 4. for c from 1 to n 5. d = pow(a3 + b3 - c3, 1/3) // 取整成int 6. if a3 + b3 == c3 + d3 && 0 <= d && d <= n // 驗證結果 7. print a, b, c, d
第6行的if
語句相當重要,由於第5行每次都會找到一個的值,可是須要檢查是不是正確的整數值。
這樣一來,運行時間就從降到了。
沿用上述問題及蠻力法,此次來找一找有哪些重複性工做。
這個算法本質上遍歷全部對的可能性,而後尋找全部對的可能性,找到和對匹配的對。
爲何對於每一對都要計算全部對的可能性?只需一次性建立一個對列表,而後對於每一個對,都去列表中尋找匹配。想要快速定位對,對列表中每一個元素,均可以把對的和看成鍵,看成值(或者知足那個和的對列表)插入到散列表。
1. n = 1000 2. for c from 1 to n 3. for d from 1 to n 4. result = c3 + d3 5. append (c, d) to list at value map[result] 6. for a from 1 to n 7. for b from 1 to n 8. result = a3 + b3 9. list = map.get(result) 10. for each pair in list 11. print a, b, pair
實際上,已經有了全部對的散列表,大可直接使用。不須要再去生成對。每一個都已在散列表中。
1. n = 1000
2. for c from 1 to n
3. for d from 1 to n
4. result = c<sup>3</sup> + d<sup>3</sup>
5. append (c, d) to list at value map[result]
6.
7. for each result, list in map
8. for each pair1 in list
9. for each pair2 in list
10. print pair1, pair2複製代碼
它的運行時間是。
第一次遇到如何在排序的數組中尋找某個元素(習得二分查找以前),你可能不會一會兒想到:「啊哈!咱們能夠比較中間值和目標值,而後在剩下的一半中遞歸這個過程。」
然而,若是讓一些沒有計算機科學背景的人在一堆按字母表排序的論文中尋找指定論文,他們可能會用到相似於二分查找的方式。他們估計會說:「天哪,Peter Smith?可能在這堆論文的下面。」而後隨機選擇一箇中間的(例如i,s,h開頭的)論文,與Peter Smith作比較,接着在剩餘的論文中繼續用這個方法查找。儘管他們不知道二分查找,但能夠憑直覺「作出來」。
咱們的大腦頗有趣。乾巴巴地拋出像「設計一個算法」這樣的題目,人們常常會搞得亂七八糟。可是若是給出一個實例,不管是數據(例如數組)仍是現實生活中其餘的相似物(例如一堆論文),他們就會憑直覺開發出一個很好的算法。
我已經無數次地看到這樣的事發生在求職者身上。他們在計算機上完成的算法奇慢無比,但一旦被要求人工解決一樣問題,立馬乾淨利落地完成。
所以,當你遇到一個問題時,一個好辦法是嘗試在直觀的真實例子上憑直覺解決它。一般越大的例子越容易。
舉個例子:給定較小字符串
s
和較大字符串b
,設計一個算法,尋找在較大字符串中較小字符串的全部排列,打印每一個排列的位置。
考慮一下你要怎麼解決這道題。注意排列是字符串的重組,所以s
中的字符能以任何順序出如今b
中,可是它們必須是連續的(不被其餘字符隔開)。
像大多數求職者同樣,你可能會這麼想:先生成s
的全排列,而後看它們是否在b
中。全排列有種,所以運行時間是,其中是s
的長度,是b
的長度。
這樣是可行的,但實在慢得太離譜了。實際上該算法比指數級的算法還要糟糕透頂。若是s
有14個字符,那麼會有超過870億個全排列。s
每增長一個字符,全排列就會增長15倍。天哪!
換種不一樣的方式,就能夠垂手可得地開發出一個還不錯的算法。參考以下例子:
s:abbc
b:cbabadcbbabbcbabaabccbabc複製代碼
b
中s
的全排列在哪兒?不要管如何作,找到它們就行。很簡單的,12歲的小孩子都能作到!
(真的,趕忙去找,我等你。)
我已經在每一個全排列下面畫了線。
s: abbc
b: cbabadcbbabbcbabaabccbabc
———— ———— ————
———— ———— ————
————複製代碼
你找到了嗎?怎麼作的?
不多有人——即便以前提出算法的人——真的去生成abbc
的全排列,再去b
中逐個尋找。幾乎全部人都採用了以下兩種方式(很是類似)之一。
(1) 遍歷b
,查看4個字符(由於s
中只有4個字符)的滑動窗口。逐一檢查窗口是不是s
的一個全排列。
(2) 遍歷b
。每次發現一個字符在s
中時,就去檢查它日後的4個(包括它)字符是否屬於s
的全排列。
取決於「是不是一個全排列」的具體實現方式,你獲得的運行時多是、或者。儘管這些都不是最優算法(包含算法),但已經比咱們以前的好太多。
解題時,試試這個方法。使用一個大而好的例子,直觀地手動解決這個特定例子。而後覆盤,思考你是如何解決它的。反向設計算法。
重點留意你憑直覺或不經意間作的任何「優化」。例如,解題時你可能會跳過以d
開頭的窗口,由於d
不在abbc
中。這是你靠大腦作出的一個優化,在設計算法時也應該留意到。
咱們經過簡化來實現一個由多步驟構成的方法。首先,能夠簡化或者調整約束,好比數據類型。這樣一來,就能夠解決簡化後的問題了。最後,調整這個算法,讓它適應更爲複雜的狀況。
舉個例子:能夠經過從雜誌上剪下詞語拼湊成句來完成一封邀請函。如何分辨一封邀請函(以字符串表示)是否能夠從給定雜誌(字符串)中獲取呢?
爲了簡化問題,能夠把從雜誌上剪下詞語改成剪下字符。
經過建立一個數組並計數字符串,能夠解決邀請函的字符串簡化版問題,其中數組中的每一位對應一個字母。首先計算每一個字符在邀請函中出現的次數,而後遍歷雜誌查看是否能知足。
推導出這個算法,意味着咱們作了相似的工做。不一樣的是,此次不是建立一個字符數組來計數,而是建立一個單詞映射頻率的散列表。
咱們能夠由淺入深,首先解決一個基本狀況(例如,),而後嘗試從這裏開始構建。遇到更復雜或者有趣的狀況(一般是 或者)時,嘗試使用以前的方法解決。
舉個例子:設計一個算法打印出字符串的全部排列組合。簡單起見,假設全部字符均不相同。
思考一個測試字符串abcdefg
。
用例 "a" --> {"a"}
用例 "ab" --> {"ab", "ba"}
用例 "abc" --> ?複製代碼
這是第一個「有趣」的狀況。若是已經有了P("ab")
的答案,如何獲得P("abc")
的答案呢?已知可選的字母是c
,所以能夠在每種可能中插入c
,即以下模式。
P("abc") = 把"c"插入到 P("ab")中的全部字符串的全部位置
P("abc") = 把"c"插入到{"ab","ba"}中的全部字符串的全部位置
P("abc") = 合併({"cab", "acb", "abc"}, {"cba", "bca", bac"})
P("abc") = {"cab", "acb", "abc", "cba", "bca", bac"}複製代碼
理解了這個模式後,就能夠寫個差很少的遞歸算法了。經過「截斷末尾字符」的方式,能夠生成s1...sn
字符串的全部組合。作法很簡單,首先生成字符串s1...sn-1
的全部組合,而後遍歷全部組合,每一個字符串的每一個位置都插入sn
獲得新的字符串。
這種由基礎例子逐漸推導的方法一般會獲得一個遞歸算法。
這種方法很取巧但奏效。咱們能夠簡單過一遍全部的數據結構,一個個地試。這種方法之因此有效在於,一旦數據結構(比方說樹)選對了,解題可能就簡單了,手到擒來。
舉個例子:隨機產生數字並放入(動態)數組。你怎麼記錄它每一步的中間值?
應用數據結構頭腦風暴法的過程可能以下所示。
總的來講,你解決過的問題越多,就越擅於選擇出合適的數據結構。不只如此,你的直覺還會變得更加敏銳,能判斷出哪一種方法最爲行之有效。
考慮到可想象的極限運行時間(BCR),可能對解決某些問題大有裨益。
可想象的極限運行時間,按字面意思理解就是,關於某個問題的解決,你能夠想象出的運行時間的極限。你能夠垂手可得地證實,BCR是沒法超越的。
比方說,假設你想計算兩個數組(長度分別爲、)共有元素的個數,會立馬想到用時不可能超過,由於必需要訪問每一個數組中的全部元素,因此就是可想象的極限運行時間。
或者,假設你想打印數組中全部成對值。你固然明白用時不可能超過,由於有對須要打印。
不過還要注意。假設面試官要求你在一個數組中(假定全部元素均不一樣)找到全部和爲的對。一些對可想象的極限運行時間概念只知其一;不知其二的求職者可能會說BCR是,理由是不得不訪問對。
這種說法大錯特錯。僅僅由於你想要全部和爲特定值的對,並不意味着必須訪問全部對。事實上根本不須要。
可想象的極限運行時間與最佳運行時間(best case runtime)有什麼關係呢?絕不相干!可想象的極限運行時間是針對一個問題而言,在很大程度上是一個輸入輸出的函數,和特定的算法並沒有關係。事實上,若是計算可想象的極限運行時間時還要考慮具體用到哪一個算法,那就極可能作錯了。最佳運行時間是針對具體算法(一般是一個毫無心義的值)的。
注意,可想象的極限運行時間不必定能夠實現。它的意義在於告訴你用時不會超過該時間。
問題:找到兩個排序數組中相同元素的個數,這兩個數組長度相同,且每一個數組中元素都不一樣。
從以下這個經典例子着手,在共同元素下標註下劃線。
A: 13 27 35 40 49 55 59 B: 17 35 39 40 55 58 60
解出這道題使用的是蠻力法,即對於A
中的每一個元素都去B
中搜索。這須要花費的時間,由於對於A
中的每一個元素(共個)都須要在B
中作的搜索。
BCR爲,由於咱們知道每一個元素至少訪問一次,一共個元素。若是跳過一個元素,那麼這個元素是否有相同的值會影響最後的結果。例如,若是從沒有訪問過B
中的最後一個元素,那麼把60改爲59,結果就不對了。
回到正題。如今有一個的算法,咱們想要更好地優化該算法,但不必定要像那樣快。
Brute Force: O(N2) Optimal Algorithm: ? BCR: O(N)
與之間的最優算法是什麼?有許多,準確地講,有無窮無盡。理論上能夠有個算法是。然而,不管是在面試仍是現實中,運行時間都不太多是這樣。
請記住這個問題,由於它在面試中淘汰了不少人。運行時間不是一個多選題。雖然常見的運行時間有、、、 或者,但你不應直接假設某個問題的運行時間是多少而不考慮推導的過程。事實上,當你對運行時間是多少百思不解時,不妨猜一猜。這時你最有可能遇到一個不太明顯、不太常見的運行時間。也許是,是數組的大小,是數值對的個數。合理推導,不要只靠猜。
最有可能的是,咱們正努力推導出或者算法。這說明什麼呢?
若是當前算法的運行時間是,那麼想獲得或者可能意味着要把第二個優化成或者。
這是BCR的一大益處,咱們能夠經過運行時間獲得關於優化方向的啓示。
第二個來自於搜索。已知數組是排序的,能夠用快於的時間在排序的數組中搜索嗎?
固然能夠了,用二分查找在一個排序的數組中尋找一個元素的運行時間是。
如今咱們把算法優化爲。
Brute Force: O(N2) Improved Algorithm: O(N log N) Optimal Algorithm: ? BCR: O(N)
還能繼續優化嗎?繼續優化意味着把 縮短爲。
一般狀況下,二分查找在排序數組中的最快運行時間是。但此次不是正常狀況,咱們一直在重複搜索。
BCR告訴咱們,解出這個算法的最快運行時間爲。所以,咱們所作的任何的工做都是「免費的」,不會影響到運行時間。
重讀7.3.1節關於優化的技巧,是否有一些能夠派上用場呢?
一個技巧是預計算或者預處理。任何時間內的預處理都是「免費的」。這不會影響運行時間。
這又是BCR的一大益處。任何你所作的不超過或者等於BCR的工做都是「免費的」,從這個意義上來講,對運行時間並沒有影響。你可能最終會將此剔除,可是目前不是當務之急。
重中之重仍在於將搜索由減小爲。任何或者不超過時間內的預計算都是「免費的」。
所以,能夠把B
中全部數據都放入散列表,它的運行時間是,而後只須要遍歷A
,查看每一個元素是否在散列表中。查找(搜索)時間是,因此總的運行時間是。
假設面試官問了一個讓咱們坐立不安的問題:還能繼續優化嗎?
答案是不能夠,這裏指運行時間。咱們已經實現了最快的運行時間,所以沒辦法繼續優化大時間,倒能夠嘗試優化空間複雜度。
這是BCR的另外一大益處。它告訴咱們運行時間優化的極限,咱們到這兒就該調轉槍頭,開始優化空間複雜度了。
事實上,就算面試官不主動要求,咱們也應該對算法抱有疑問。就算不存儲數據,也能夠精確地得到相同的運行時間。那麼爲何面試官給出了排序的數組?並不是不尋常,只是有些奇怪罷了。
回到咱們的例子:
A: 13 27 35 40 49 55 59 B: 17 35 39 40 55 58 60
要找有以下特徵的算法。
不使用其餘空間的最佳算法是二分查找。想想怎麼優化它。試着過一遍整個算法。
(1) 用二分查找在B
中找 A[0] = 13
。沒找到。
(2) 用二分查找在B
中找 A[1] = 27
。沒找到。
(3) 用二分查找在B
中找 A[2] = 35
。在B[1]
中找到。
(4) 用二分查找在B
中找 A[3] = 40
。在B[5]
中找到。
(5) 用二分查找在B
中找 A[4] = 49
。沒找到。
(6) ……
想一想BUD。搜索是瓶頸。整個過程有多餘或者重複性工做嗎?
搜索A[3] = 40
不須要搜索整個B
。在B[1]
中已找到35,因此40不可能在35前面。
每次二分查找都應該從上次終止點的左邊開始。
實際上,根本不須要二分查找,大可直接藉助線性搜索。只要在B
中的線性搜索每次都從上次終止的左邊出發,就知道將要用線性時間進行搜索。
(1) 在B
中線性搜索 A[0] = 13
,開始於 B[0] = 17
,結束於 B[0] = 17
。未找到。
(2) 在B
中線性搜索 A[1] = 27
,開始於 B[0] = 17
,結束於 B[1] = 35
。未找到。
(3) 在B
中線性搜索 A[2] = 35
,開始於 B[1] = 35
,結束於 B[1] = 35
。找到。
(4) 在B
中線性搜索 A[3] = 40
,開始於 B[2] = 39
,結束於 B[3] = 40
。找到。
(5) 在B
中線性搜索 A[4] = 49
,開始於 B[3] = 40
,結束於 B[4] = 55
。找到。
(6) ……
以上算法與合併排序數組一模一樣。該算法的運行時間爲,空間爲。
如今同時達到了BCR和最小的空間佔用,這已是極限了。
這是另外一個使用BCR的方式。若是達到了BCR而且其餘空間爲,那麼不管是大時間仍是空間都已經沒法優化。
BCR不是一個真正的算法概念,也沒法在算法教材中找到其身影。但我我的以爲其大有用處,無論是在我本身解題時,仍是在指導別人解題時。
若是很難掌握它,先確保你已經理解了大時間的概念。你要作到運用自如。一旦你掌握了,弄懂BCR不過是小菜一碟。
流傳最廣、危害最大的謠言就是,求職者必須答對每一個問題。這種說法並不全對。
首先,面試的回答不該該簡單分爲「對」或「不對」。當我評價一我的在面試中的表現時,從不會想:「他答對了多少題?」評價不是非黑即白。相反地,評價應該基於最終解法有多理想,解題花了多長時間,須要多少提示,代碼有多幹淨。這些纔是關鍵。
其次,評價面試表現時,要和其餘的候選人作對比。例如,若是你優化一個問題須要15分鐘,別人解決一個更容易的問題只須要5分鐘,那麼他就比你表現好嗎?也許是,也許不是。若是給你一個顯而易見的問題,面試官可能會但願你乾淨利落地給出最優解法。可是若是是難題,那麼犯些錯也是在乎料之中的。
最後,許多或者絕大多數的問題都不簡單,就算一個出類拔萃的求職者也很難馬上給出最優算法。一般來講,對於我提出的一些問題,厲害的求職者也要20到30分鐘才能解出。
我在谷歌評估過成千上萬份求職者的信息,也只看到過一個求職者天衣無縫地經過了面試。其餘人,包括收到錄用通知的人,都或多或少犯過錯。
若是你曾見過某個面試題,要提早說明。面試官問你這些問題是爲了評估你解決問題的能力。若是你已經知道某個題的答案了,他們就沒法準確無誤地評估你的水平了。
此外,若是你對本身見過這道題諱莫如深,面試官還可能會發現你爲人不誠實。反過來講,若是你坦白了這一點,就會給面試官留下誠實的好印象。
在不少頂級公司,面試官並不在意你用什麼語言。相比之下,他們更在意你解決問題的能力。
不過,也有些公司比較關注某種語言,樂於看到你是如何駕輕就熟地使用該語言編寫代碼的。
若是你能夠任意選擇語言的話,就選最爲駕輕就熟的。
話雖如此,若是你擅長几種語言,就將如下幾點牢記於心。
這一點不強求。可是若面試官知道你所使用的語言,多是最爲理想的。從這點上講,更流行的語言可能更爲合適。
即便面試官不知道你所用的語言,他們也但願能對該語言有個大體瞭解。一些語言的可讀性天生就優於其餘語言,由於它們與其餘語言有類似之處。
舉個例子,Java很容易理解,即便沒有用過它的人也能看懂。絕大多數人都用過與Java語法相似的語言,好比C和C++。
然而,像Scala和Objective C這樣的語言,其語法就大不相同了。
使用某些語言會帶來潛在的問題。例如,使用C++就意味着除了代碼中常見的bug,還存在內存管理和指針的問題。
有些語言更爲冗長煩瑣。Java就是一個例子,與Python相比,該語言極爲煩瑣。經過比較如下代碼就一目瞭然了。
Python:
1. dict = {"left": 1, "right": 2, "top": 3, "bottom": 4};複製代碼
Java:
1. HashMap<String, Integer> dict = new HashMap<String, Integer>().
2. dict.put("left", 1);
3. dict.put("right", 2);
4. dict.put("top", 3);
5. dict.put("bottom", 4);複製代碼
能夠經過縮寫使Java更爲簡潔。好比一個求職者能夠在白板上這樣寫:
1. HM<S, I> dict = new HM<S, I>().
2. dict.put("left", 1);
3. ... "right", 2
4. ... "top", 3
5. ... "bottom", 4複製代碼
你須要解釋這些縮寫,但絕大多數面試官並不在乎。
有些語言使用起來更爲容易。例如,使用Python能夠垂手可得地讓一個函數返回多個值。可是若是使用Java,就還須要一個新的類。語言的易用性可能對解決某些問題大有裨益。
與上述相似,能夠經過縮寫或者實際上不存在的假設方法讓語言更易使用。例如,若是一種語言提供了矩陣轉置的方法而另外一種語言未提供,也並不必定要選第一種語言(若是面試題須要那個函數的話),能夠假設另外一種語言也有相似的方法。
到目前爲止,你可能知道僱主想看到你寫出一手「漂亮的、乾淨的」代碼。但具體的標準是什麼呢?在面試中又如何體現呢?
通常來說,好代碼應符合如下標準。
追求這些須要掌握好平衡。好比,有時犧牲必定的效率來提升可維護性就是明智之舉,反之亦然。
在面試中寫代碼時應該考慮到這些。如下內容更爲具體地闡述了好代碼的標準。
假設讓你寫一個函數,把兩個單獨的數學表達式相加,形如(其中係數和指數能夠爲任意正實數或負實數),即該表達式是由一系列項組成,每一個項都是一個常數乘以一個指數。面試官還補充說,不但願你解析字符串,但你能夠使用任何數據結構。
這有幾種不一樣的實現方式。
一個糟糕透頂的實現方式是把表達式放在一個double
的數組中,第個元素對應表達式中項的係數。這個數據結構的問題在於,不支持指數爲負數或非整數的表達式,還要求1000個元素大小的數組來存儲表達式。
1. int[] sum(double[] expr1, double[] expr2) {
2. ...
3. }複製代碼
稍差的方案是用兩個數組分別保存係數和指數。用這種方法,表達式的每一項都有序保存,但能「匹配」。第項就表示爲oefficients[i]*xexponents[i]
。
對於這種實現方式,若是coefficients[p] = k
而且exponents[p] = m
,那麼第項就是。雖然這樣沒有了上一種方式的限制,但仍然顯得雜亂無章。一個表達式卻須要使用兩個數組。若是兩個數組長度不一樣,表達式可能有「未定義」的值。不只如此,返回也讓人不勝其煩,由於要返回兩個數組。
1. ??? sum(double[] coeffs1, double[] expon1, double[] coeffs2, double[] expon2) {
2. ...
3. }複製代碼
一個好的實現方式就是爲這個問題中的表達式設計數據結構。
1. class ExprTerm {
2. double coefficient;
3. double exponent;
4. }
5.
6. ExprTerm[] sum(ExprTerm[] expr1, ExprTerm[] expr2) {
7. ...
8. }複製代碼
有些人可能認爲甚至聲稱,這是「過分優化」。無論是否是,也無論你有沒有以爲這是過分優化,關鍵在於上面的代碼體現了你在思考如何設計代碼,而不是以最快速度將一些數據東拼西湊。
假設讓你寫一個函數來檢查是否一個二進制的值(以字符串表示)等於用字符串表示的一個十六進制數。
解決該問題的一種簡單方法就是複用代碼。
1. boolean compareBinToHex(String binary, String hex) {
2. int n1 = convertFromBase(binary, 2);
3. int n2 = convertFromBase(hex, 16);
4. if (n1 < 0 || n2 < 0) {
5. return false;
6. }
7. return n1 == n2;
8. }
9.
10. int convertFromBase(String number, int base) {
11. if (base < 2 || (base > 10 && base != 16)) return -1;
12. int value = 0;
13. for (int i = number.length() - 1; i >= 0; i--) {
14. int digit = digitToValue(number.charAt(i));
15. if (digit < 0 || digit >= base) {
16. return -1;
17. }
18. int exp = number.length() - 1 - i;
19. value += digit * Math.pow(base, exp);
20. }
21. return value;
22. }
23.
24. int digitToValue(char c) { ... }複製代碼
能夠單獨實現二進制轉換和十六進制轉換的代碼,但這隻會讓代碼難寫且難以維護。不如寫一個convertFromBase
方法和 digitToValue
方法,而後複用代碼。
編寫模塊化的代碼時要把獨立代碼塊放到各自的方法中。這有助於提升代碼的可維護性、可讀性和可測試性。
想象你正在寫一個交換數組中最小數和最大數的代碼,能夠用以下方法完成。
1. void swapMinMax(int[] array) {
2. int minIndex = 0;
3. for (int i = 1; i < array.length; i++) {
4. if (array[i] < array[minIndex]) {
5. minIndex = i;
6. }
7. }
8.
9. int maxIndex = 0;
10. for (int i = 1; i < array.length; i++) {
11. if (array[i] > array[maxIndex]) {
12. maxIndex = i;
13. }
14. }
15.
16. int temp = array[minIndex];
17. array[minIndex] = array[maxIndex];
18. array[maxIndex] = temp;
19. }複製代碼
或者你也能夠把相對獨立的代碼塊封裝成方法,這樣寫出的代碼更爲模塊化。
1. void swapMinMaxBetter(int[] array) {
2. int minIndex = getMinIndex(array);
3. int maxIndex = getMaxIndex(array);
4. swap(array, minIndex, maxIndex);
5. }
6.
7. int getMinIndex(int[] array) { ... }
8. int getMaxIndex(int[] array) { ... }
9. void swap(int[] array, int m, int n) { ... }複製代碼
雖然非模塊化的代碼也不算糟糕透頂,可是模塊化的好處是易於測試,由於每一個組件均可以單獨測試。隨着代碼愈來愈複雜,代碼的模塊化也越發重要,這將使代碼更易維護和閱讀。面試官想在面試中看到你能展現這些技能。
你的面試官要求你寫代碼來檢查一個典型的井字棋是否有個贏家,並不意味着你必須要假定是一個3×3的棋盤。爲何不把代碼寫得更爲通用一些,實現成的棋盤呢?
把代碼寫得靈活、通用,也許意味着能夠經過用變量替換硬編碼值或者使用模板、泛型來解決問題。若是能夠的話,應該把代碼寫得更爲通用。
固然,凡事無絕對。若是一個解決方案對於通常狀況而言顯得太過複雜,而且不合時宜,那麼實現簡單預期的狀況可能更好。
一個謹慎的程序員是不會對輸入作任何假設的,而是會經過ASSERT
和if
語句驗證輸入。
一個例子就是以前把數字從進制(好比二進制或十六進制)表示轉換成一個整數。
1. int convertFromBase(String number, int base) {
2. if (base < 2 || (base > 10 && base != 16)) return -1;
3. int value = 0;
4. for (int i = number.length() - 1; i >= 0; i--) {
5. int digit = digitToValue(number.charAt(i));
6. if (digit < 0 || digit >= base) {
7. return -1;
8. }
9. int exp = number.length() - 1 - i;
10. value += digit * Math.pow(base, exp);
11. }
12. return value;
13. }複製代碼
在第2行,檢查進制數是否有效(假設進制大於10時,除了16之外,沒有標準的字符串表示)。在第6行,又作了另外一個錯誤檢查以確保每一個數字都在容許範圍內。
像這樣的檢查在生產代碼中相當重要,也就是說,面試中一樣重要。
不過,寫這樣的錯誤檢查會很枯燥無味,還會浪費寶貴的面試時間。關鍵是,要向面試官指出你會寫錯誤檢查。若是錯誤檢查不是一個簡單的if
語句能解決的,最好給錯誤檢查留有空間,告訴面試官等完成其他代碼後還會返回來寫錯誤檢查。
面試題有時會讓人不得要領,但這只是面試官的測試手段。直面挑戰仍是知難而退?不畏艱險,奮勇向前,這一點相當重要。總而言之,切記面試不是一蹴而就的。遇到攔路虎本就在乎料之中。
還有一個加分項:表現出解決難題的滿腔熱情。
本文摘自《程序員面試金典(第6版)》