Technical Questions#
技術問題是許多頂尖科技公司面試的核心。許多應試者會被這些問題的難度嚇到,但其實有邏輯性的方法可以應對。
How to Prepare(如何準備)#
許多應試者只是閱讀題目和解答,這就像讀一道微積分題和它的答案來學微積分——你需要的是實際練習解題。
對書中(以及任何你遇到的)每道題目,請遵循以下步驟:
- 自己嘗試解題:盡量不用提示。許多題目設計本來就很難,卡住是正常的。思考時也要考慮時間與空間效率。
- 在紙上寫程式碼:電腦提供語法高亮、自動補全和快速除錯——紙上沒有。提前適應這種緩慢的節奏。
- 在紙上測試程式碼:包含一般情況、邊界情況、錯誤情況等。面試時需要這樣做,提前練習。
- 將紙上的程式碼原封不動輸入電腦:你可能會犯許多錯誤。記錄這些錯誤,以便在真正的面試中多加注意。
盡量多做模擬面試。你和朋友可以輪流擔任面試官,給彼此出題。擔任面試官也能讓你學到很多,因為你會理解面試官視角。
What You Need To Know(必備知識)#
許多公司的資料結構與演算法題不是知識測驗,但確實預設你有基礎知識。
Core Data Structures, Algorithms, and Concepts(核心知識清單)#
| 資料結構 | 演算法 | 概念 |
|---|---|---|
| Linked Lists | Breadth-First Search | Bit Manipulation |
| Trees, Tries & Graphs | Depth-First Search | Memory (Stack vs. Heap) |
| Stacks & Queues | Binary Search | Recursion |
| Heaps | Merge Sort | Dynamic Programming |
| Vectors / ArrayLists | Quick Sort | Big O Time & Space |
| Hash Tables |
對於每個主題,確保你理解如何使用和實作它,以及其時間與空間複雜度。
Hash Table 是極其重要的資料結構,務必非常熟悉。如果你對上述任何資料結構或演算法不夠熟悉,請從頭實作練習。
Powers of 2 Table(2 的冪次表)#
這張表對涉及擴展性或記憶體限制的題目很有用:
| 2 的冪次 | 精確值 | 近似值 | Bytes 換算 |
|---|---|---|---|
| 7 | 128 | ||
| 8 | 256 | ||
| 10 | 1,024 | 1 千 | 1 KB |
| 16 | 65,536 | 64 KB | |
| 20 | 1,048,576 | 1 百萬 | 1 MB |
| 30 | 1,073,741,824 | 1 十億 | 1 GB |
| 32 | 4,294,967,296 | 4 GB | |
| 40 | 1,099,511,627,776 | 1 兆 | 1 TB |
Walking Through a Problem(解題步驟)#
書中提供了一張解題流程圖,依序為 7 個步驟:
1. Listen Carefully(仔細聆聽)#
不只是確保聽對題目,更要記住題目中所有獨特的資訊。
- 題目說「兩個已排序的陣列」→ 排序這個特性很可能對最優解很重要
- 題目說「設計一個在伺服器上重複執行的演算法」→ 這意味著可能適合預計算或快取
許多應試者一開始聽得很清楚,但 10 分鐘後就忘掉了關鍵細節,導致無法找到最優解。
2. Draw an Example(畫一個例子)#
例子能大幅提升你解題的能力,但要畫一個好的例子:
- 具體:使用真實的數字或字串(不要只畫空框)
- 夠大:大多數例子太小了,大約需要比直覺多 50%
- 不是特殊情況:要非常小心,特殊情況很容易讓你誤解問題
3. State a Brute Force(說出暴力解)#
一旦有了例子,先說出暴力解。這是可以預期的,面試官知道初始解通常不是最優的。
- 說明暴力解的時間和空間複雜度
- 然後再討論如何改進
不要因為覺得暴力解太明顯或太差就跳過這步。即使對你來說很顯然,面試官也想看到你能識別簡單解法。
4. Optimize(最佳化)#
有了暴力解後,努力最佳化。幾個有效的技巧:
- 尋找任何未使用的資訊——題目給你的資訊幾乎都有用
- 使用新的例子,有時換個例子能讓你看出模式
- 「錯誤地」解決問題,然後思考為什麼失敗,能否修正?
- 時間與空間的取捨——使用 Hash Table 往往非常有用
- 預計算資訊——重新組織資料(排序等)或提前計算某些值
- 使用 Hash Table——在面試題中極為常用
- 思考最佳可能執行時間(BCR)
5. Walk Through(完整走一遍)#
確定了最優解後,不要立刻開始寫程式碼。先完整走過你的演算法:
- 確保真正理解每個細節
- 知道哪些變數以及何時會改變
在白板上寫程式碼很慢,而且很難修改,所以盡量一開始就寫接近完美的程式碼。
6. Implement(實作)#
現在你知道要寫什麼了,開始實作:
- 從白板左上角開始寫(你會需要空間)
- 避免「線條偏斜」(每行都歪著寫)
好的程式碼具備以下特性:
- Correct(正確):對所有預期和非預期的輸入都能正確運作
- Efficient(高效):在時間和空間上都盡可能高效,包含漸進效率和實際效率
- Simple(簡單):如果 10 行能做到,不要寫 100 行
- Readable(易讀):其他開發者應該能看懂你的程式碼,適時加入必要的註解
- Maintainable(可維護):程式碼應易於修改和維護
在實作中展示好程式碼的方法:
- 善用資料結構:設計適當的資料結構,而非把所有東西塞進陣列
- 適當複用程式碼(Code Reuse):找出可以共用的邏輯,寫一個通用函式
- 模組化(Modular):將相對獨立的邏輯分離成自己的方法,使程式碼更易讀、測試和維護
- 彈性與健壯(Flexible and Robust):考慮更通用的情況,避免 hardcode 特定值
- 錯誤檢查(Error Checking):不假設輸入的合法性,進行驗證
好的程式設計師不假設輸入,而是驗證輸入是否符合預期。若錯誤檢查很繁瑣,可以說明「我會寫這些檢查」並在旁邊留空間,先完成主要邏輯。
7. Test(測試)#
不要在真實世界提交未測試的程式碼,面試中也一樣。
聰明的測試順序:
- 概念測試(Conceptual Test):像 code review 一樣逐行讀你的程式碼,它做了你認為它做的事嗎?
- 奇怪的程式碼(Weird-looking Code):注意
x = length - 2這樣的行、從 1 開始的迴圈等容易出錯的地方 - 熱點(Hot Spots):遞迴的 base case、整數除法、二元樹的 null 節點、linked list 的首尾
- 小測試案例(Small Test Cases):使用 3~4 個元素的陣列,而非 8 個元素的大陣列——速度更快,效果一樣好
- 特殊情況(Special Cases):null 或單個元素的值、極端值、其他特殊情況
找到 bug 時,仔細分析為什麼會發生,確保你的修正是最好的方案,而非第一個想到的修正。
Optimize & Solve Techniques(最佳化與解題技巧)#
Technique #1: Look for BUD#
BUD 是最有用的最佳化方法,代表三種演算法常見的「浪費」:
- Bottlenecks(瓶頸):演算法中拖慢整體速度的部分
- Unnecessary Work(不必要的工作):可以省略的計算
- Duplicated Work(重複的工作):一再重複計算相同的東西
Bottleneck 範例:給定整數陣列,計算差為 k 的整數對數量。
- 暴力解:O(N²)——對每個元素搜尋另一個元素
- 瓶頸是「搜尋」:將陣列放入 hash table,搜尋變成 O(1)
- 最終:O(N)
Unnecessary Work 範例:找出 a³ + b³ = c³ + d³ 的所有整數解(1 到 1000)。
- 暴力解是四層迴圈 O(N⁴)
- 發現 d 可以直接計算:d = ∛(a³ + b³ - c³),省去最內層迴圈
- 降至 O(N³)
Duplicated Work 範例:同樣的問題,對每個 (a,b) 都重新計算所有 (c,d)。
- 預先計算所有 (c,d) 組合放入 hash map
- 查詢時直接 lookup,降至 O(N²)
Technique #2: DIY (Do It Yourself)#
人類的直覺往往比我們自己意識到的更強。把演算法問題想成一個真實的例子,然後直覺地手動解決它。
範例:給定小字串 s 和大字串 b,找出 s 在 b 中所有排列的位置。
大多數人看到這個題目會想:「生成 s 的所有排列,然後在 b 中搜尋」——這是 O(S! × B) 的爛解法。
但如果你給自己一個具體的例子,手動找答案,幾乎所有人都會自然地用「滑動視窗」的方法。把你直覺的解法逆向工程出來,就是好演算法。
Technique #3: Simplify and Generalize#
- 先簡化問題的某個限制(例如把資料型態從 string 改成 character)
- 解決簡化後的問題
- 將解法推廣回原始的複雜問題
Technique #4: Base Case and Build#
- 先解決 n = 1 的 base case
- 嘗試從 n-1 的解構建出 n 的解
- 當 n = 3 或 4 時,用先前的解推導
Base Case and Build 演算法通常會自然地引導出遞迴解法。
Technique #5: Data Structure Brainstorm#
有時候只需要腦力激盪各種資料結構,嘗試套用每一種,看哪種最合適。
範例:數字隨機生成後儲存到陣列,如何追蹤中位數?
- Linked List?不太好,存取和排序都不方便
- Array?需要保持排序,成本高
- Binary Tree?可行,平衡 BST 的中間節點接近中位數
- Heap?最棒!兩個 heap(一個 min-heap、一個 max-heap)可以追蹤大半和小半的元素
做的題目越多,你對「哪種資料結構適合哪種問題」的直覺就越精準。
Best Conceivable Runtime (BCR)#
BCR(最佳可想像執行時間) 是你能合理想像的對一個問題的最快解法。
- 找出兩個長度 A 和 B 的陣列的共同元素:BCR 是 O(A + B),因為你至少要看過每個元素一次
- 印出陣列中所有整數對:BCR 是 O(N²),因為有 N² 對要印出
BCR 與 Best Case Runtime 完全不同!BCR 是針對「問題」,描述任何演算法都無法突破的理論下界。Best Case Runtime 是針對「特定演算法」,幾乎沒什麼用。
BCR 的實際用途#
- 告訴你還有多少優化空間:若你的演算法已達到 BCR,就不用再優化時間複雜度了,可以轉而考慮空間複雜度。
- 提示你需要減少什麼:若 BCR 是 O(N) 但你的演算法是 O(N log N),你需要消除那個 log N 因子。
- 「免費的」工作:任何在 BCR 以內的工作都是「免費的」,不影響整體複雜度。
BCR 範例:在兩個各有 N 個元素的有序陣列中找共同元素。
- 暴力解:O(N²)
- 改進(Binary Search):O(N log N)
- BCR:O(N)
BCR 告訴我們目標是 O(N)。既然搜尋的工作量是 O(N) 以內都是「免費」的,可以把 B 的所有元素放入 hash table(O(N)),再遍歷 A 進行 O(1) 查詢,達到 O(N) 時間、O(N) 空間。
若還想優化空間,利用兩個陣列都已排序的特性,用雙指針線性掃描,達到 O(N) 時間、O(1) 空間——達到 BCR 且空間最小。
Handling Incorrect Answers(如何面對錯誤答案)#
面試中最普遍也最危險的謠言是:應試者需要把每道題都答對。這不完全正確。
- 面試評估不是二元的「對或錯」,而是綜合評估:解法有多優、花了多長時間、需要多少提示、程式碼有多乾淨
- 你的表現是和其他應試者相比的:難題允許有更多錯誤
- 強力應試者通常需要 20~30 分鐘才能解出設計上的難題
- 就算是拿到錄取通知的人,也幾乎都犯了錯誤
When You’ve Heard a Question Before(聽過這道題)#
如果你之前看過這道題,誠實地告訴面試官。面試官問這道題是要評估你的解題能力;如果你已經知道答案,就剝奪了他們評估你的機會。此外,面試官可能會覺得你不誠實。如果你承認,反而會得到誠信加分。
The “Perfect” Language for Interviews(面試語言的選擇)#
許多頂尖公司的面試官不在乎你用什麼語言,他們更關心你解決問題的能力。不過,如果你有多個熟悉的語言可以選擇,考慮以下因素:
- Prevalence(普及度):讓面試官能看懂你的程式碼,使用較廣泛的語言(如 Java、Python、C++)會更理想
- Language Readability(易讀性):有些語言(如 Java)因為和 C/C++ 語法相近,對大多數人來說很易讀;Scala 或 Objective-C 則差異較大
- Potential Problems(潛在問題):C++ 有記憶體管理和指針問題,可能增加不必要的複雜度
- Verbosity(冗長性):Java 比 Python 更冗長。在白板上寫 Java 的 HashMap 初始化需要好幾行,而 Python 一行就搞定。可以縮寫後向面試官說明
- Ease of Use(易用性):Python 能輕鬆從函式回傳多個值;Java 則需要一個新的 class
若被允許選擇語言,選擇你最熟悉的語言。熟悉度比語言特性更重要。
What Good Coding Looks Like(好程式碼的樣貌)#
好的程式碼具備以下特性:
- Correct(正確):對所有預期和非預期的輸入都正確運作
- Efficient(高效):時間和空間都盡可能高效(包含漸進效率和實際效率)
- Simple(簡單):能 10 行解決就不寫 100 行
- Readable(易讀):其他開發者能看懂並理解其運作方式,必要時加入簡潔的註解
- Maintainable(可維護):在產品生命週期中易於修改,對他人和原作者都容易維護
在面試中具體展現這些特性的方法:
Use Data Structures Generously(善用資料結構)#
與其用 primitive array 或兩個平行的陣列,不如設計一個適當的資料結構(class/struct)。
Appropriate Code Reuse(適當複用程式碼)#
寫通用函式,讓程式碼能在不同情境下複用,減少重複邏輯。
Modular(模組化)#
將相對獨立的邏輯分離成獨立的函式,這讓程式碼更容易讀、測試和維護。
Flexible and Robust(彈性與健壯)#
不要 hardcode 特定值(如 3×3 的棋盤大小)。使用變數,讓程式碼能處理更通用的情況。
Error Checking(錯誤檢查)#
謹慎的工程師不假設輸入的合法性,而是透過 ASSERT 或 if-statement 驗證輸入。
Don’t Give Up!(不要放棄!)#
面試題可能讓人感到不知所措,但這正是面試在測試的:當你面對困難問題時,你是迎難而上還是退縮?展現你主動應對挑戰問題的精神。
面試題設計本來就很難,遇到很難的題目不應該讓你感到意外。對解決難題表現出興奮感,會加分。