面試經驗反思
To 十分感謝點進來的您
Q1: Merge Sort
思路整理
- 在每次遞迴呼叫中,我們會將原始的 list 拆分為兩個子問題:一個是從索引 1 到 m,另一個是從 m+1 到 n,也就是將整個問題劃分為左右兩個子區間。
- 接著針對這兩個子問題分別遞迴處理,並在每一層遞迴結束後,對兩個已排序的子序列進行一次合併(merge)操作,以合併成一個整體有序的序列。
- 此演算法的整體時間複雜度為 O(n log n),其中 log n 表示將問題遞迴劃分為兩半所需的層數,而每一層的合併操作都會對整個序列進行線性掃描,因此每層的合併操作是 O(n),總體複雜度即為 O(n log n) 且空間複雜度會是 O (1) (考慮遞迴的 stack 會是 O(log n)。
面試中的問題
- 有考量到拆分子問題的解法,並且針對兩個子問題遞迴處理的想法也是對的,但在遞迴式子的建立上出現了問題,面試官也有提示在最小的子問題會是有序的,並且從 bottom up merge 上來的答案也會是有序的,這部分完全是我算法實作程度的問題,沒有將面試官好意的提示撰寫出來。
- 有想到可以將各個 node 轉換為陣列裡的元素,並直接進行 sort() 排序後在使用 iteration 建立整個鏈結串列,但這個解法首先空間複雜度會來到 O(n) ,並且這題的出發點就是想要我去實作出算法,但這個方法是選擇用現成的解法,雖然都是同樣的時間複雜度,但就失去題目的意義。
- 在面試中太緊張,導致還沒定義好 function 就想著要 return 什麼結果,並且像是獲取中間點這種操作是可以將他寫成獨立 function的,而不是直接撰寫,這樣代碼的可讀性跟可維護性也會比較高。
- 在與面試官的溝通中,因為過於緊張而沒有讓面試官有感受到雙向溝通,我應該要把我腦中癥結的點與面試官確認,並尋求指引,而非自己埋頭苦惱。
- 太緊張了。
演算法實作
應該要先將會用到的 function 定義出來,而不是直接開始撰寫代碼
getMid
面試過程中的想法與實作是沒問題的,可利用快慢指標去獲取中間點
merge
對「有序」的兩個鏈結串列可以進行排列,並且善用指標的指標去指定剩餘串列
mergeSort
在這主程式要進行 divide and conquer 的操作,利用先前定義的兩個函式去進行整個鏈結串列的排序
Q2: KMP(Knuth–Morris–Pratt)演算法
思路整理
- 題目給定兩個字串,分別為長度為 n 的 s 以及長度為 m 的 t ,希望能找出所有 s 的子字串中等於 t 的起始點。
- 這題當下直觀的想法是做一個暴力搜尋,透過一整個 iteration 對 s 的每個字元當作起始點去確認是否與 t 相同,若相同則加進答案列裡。
- 這樣情況下的時間複雜度會來到 O(n * m) ,因為在最糟的情境會需要對每一個字元做長度為 m 的檢查,並且空間複雜度會來到 O(n) <–答案陣列的空間。
- 以題目敘述來說有更好的解法,但當下沒想出來,只有想到可能與 prefix 有相關。
面試中的問題
- 沒有將更好的算法名稱主動說出來,也沒有與面試官討論更好的解法的思路,像是我有想到可能與 prefix 相關但沒提出進行討論。
- 不知道最優解的完整思路,也沒將最優解代碼撰寫出來
學習演算法
TODO: 先閱讀 Knuth-Morris-Pratt Algorithm Visually Explained 的圖解,理解算法,並且 trace 算法的每一步驟。
拆解演算法
核心思路是不希望在找到當下兩個字串的字元不 match 的情況下從頭來過,那這個算法希望利用 當下字串與字串本身最長的相等的前綴與後綴長度,當 s[i] != t[j] 發生時,我們不需要讓 j = 0 重頭比對,可以跳回 j = lps[j - 1](前一個字元的最長前後綴的長度),這樣能避免多餘比較,這樣就能降低整體的時間複雜度,可以將搜尋特定字串的時間複雜度從 O(n * m) 優化至 O(n + m),並且在空間複雜度的部分會是 O(max(n, m))。
首先 KMP 會去針對每個字元與此字串做最長前後綴的陣列
像是對於字串 "ABABCDEAB" 來舉例:
lps[0] = 0("A" -> 只有一個字元)
lps[1] = 0("AB" -> 沒有前後綴)
lps[2] = 1("ABA" -> 擁有相同前後綴 "A")
lps[3] = 2("ABAB" -> 擁有相同前後綴 "AB")
…
那這個前後綴陣列在字串的比較中,是為什麼可以讓當下兩個字元不匹配時,跳回他前一項的最長前後綴的長度呢?
那是因為前面的所有字元都「已經確定」與 s 中的某一段是匹配的,所以我們可以保留這些資訊,不需要重比,總結的話就,我們跳回的 lps[j - 1],就是「在前一段已知匹配的子字串中,能保留最長、且仍可能匹配的部分」,因此能跳過無效比對,提高效率。
舉個例子:
理解完演算法來實現代碼:
演算法實現
createLPSArray
- 首先定義前一個 LPS 的長度 prev 以及下一個要計算的 LPS 位置 i
- 如果匹配成功便代表在 cur 這個 index 的 LPS 應該為 prev + 1,並且prev 以及 cur 都繼續往前匹配。
- 若匹配失敗且前一個位置 prev 已經為 0,則 lps[cur] 為 0,cur 繼續往前匹配。
- 若匹配失敗,且前一個位置 prev 不為 0,則代表可以透過 LPS (最長相等前後綴)來回到應該重新匹配的位置。
KMP
當我們匹配失敗的時候,都要先透過 LPS 陣列找到可以重新嘗試的地方先匹配看看,以避免每一次都從頭開始匹配的時間浪費。