我將在這篇文章中帶你瞭解我從頭至尾解決程式設計問題的策略。我既在谷歌的日常工作中使用這一策略,也在和各種水平的程式設計人員(訓練營、大學生和實習生等)合作時使用它(幫助他們學習和成長)。應用這一結構化的流程能儘可能地縮減令人沮喪的除錯過程,在更少的時間裡得到更清晰更正確的程式碼。
一步步介紹
我將使用一個示例練習題來進行說明。
問題:「給定兩個字串 sourceString 和 searchString,返回 searchString 出現在 sourceString 中的第一個索引。如果 searchString 未在 sourceString 中出現,則返回 -1。」
第一步:畫出來
直接從寫程式碼開始實際上是一個荒謬又懶惰的思路。在寫一篇文章之前,你首先會搞清楚你的假設和證據,以確保你的論證是合理的。如果你不這樣做,後面當你發現你寫的內容整體無法合適地組合起來時,你又要重新開始,這會浪費很多時間。寫程式碼也是這樣,甚至可能更糟,就像把洗髮水揉進眼睛裡那麼糟。
通常而言,一個問題的解決方案並不是淺顯直觀的,即使可能乍看起來很簡單。在紙上琢磨一下能讓你找到解決方案並驗證該方案在一些不同場景中的效果。所有這些工作都應在寫下任何一行程式碼之前完成。
所以先不要寫程式碼。甚至不要去想程式碼。後面你會有足夠的時間去新增分號和括號。現在你只需要搞清楚你作為一臺人體計算機,會如何解決這個問題。
畫圖。使用箭頭。在小方框裡填寫數字。只要能讓你視覺化這個問題,就去做吧。你的目標是解決問題,你能用紙張和鉛筆盡情發揮,而不受限於鍵盤。
首先找一些簡單的輸入。比如,假設你的函式是「取一個字串」:「abc」。搞清楚正確的結果是什麼。然後去想「如何」解決這個問題以及所涉及到的步驟。
我們假設我們的字串有以下值:
sourceString: "abcdyesefgh"
searchString: "yes"
我的想法就會像下面這樣展開:
好吧,我看到 sourceString 包含 searchString。但我是怎麼看出來的?嗯,我首先從 sourceString 的開頭開始閱讀,檢查每 3 個字元構成的片段是否與詞「yes」匹配,直到結束。比如,我看了 abc、bcd、cdy 等組合。當我看到索引 4 時,我找到了 yes,所以我確定找到了匹配並且是從索引 4 開始。
當我們寫下我們的演算法時,我們需要確保我們表達了一切並能夠處理所有可能的情況。當我們確實能找到匹配時,返回正確答案當然很好,但我們也需要在沒有匹配時也能返回正確答案。
我們再以另一對字串為例:
sourceString: "abcdyefg"
searchString: "yes"
這裡,我們同樣首先從 sourceString 的開頭開始閱讀,檢查每 3 個字元構成的片段是否與詞「yes」匹配,直到結束。當我們看到索引 4 時,找到了「yef」,差點就匹配了,但卻並沒有完全匹配,因為第三個字元不同。所以我們繼續檢查,直到字串末尾,最後確定其中沒有匹配,因此返回 -1。
我們已經確定瞭解決這個問題的一系列步驟(在程式設計領域,我們稱之為「演算法」),我們也已經嘗試了一些不同場景,每一次都得到了正確的結果。現在,我們已經很相信我們的演算法有效了,現在是時候形式化這個演算法了,這就是下一個步驟。
第二步:寫成普通話
我們想想在第一步確定的演算法,然後用平實易懂的語言把它寫出來。這能使這些步驟顯得具體,讓我們在寫程式碼時能夠回顧參考。
從字串開頭開始檢查
檢查每 3 個字元構成的片段(具體字元數量視 searchString 而定)
如果其中有任何片段等於 searchString,就返回當前索引
如果直到該字串結束也沒找到任何匹配,就返回 -1
看起來不錯喲!
第三步:寫虛擬碼
虛擬碼並不是真正的程式碼,但卻模仿了程式碼的結構。下面是我為上面的演算法寫的虛擬碼:
for each index in sourceString,
there are N characters in searchString
let N chars from index onward be called POSSIBLE_MATCH
if POSSIBLE_MATCH is equal to searchString, return index
at the end, if we haven't found a match yet, return -1.
我還可以讓虛擬碼更接近程式碼一點,比如這樣寫:
for each index in sourceString,
N = searchString.length
POSSIBLE_MATCH = sourceString[index to index+N]
if POSSIBLE_MATCH === searchString:
return index
return -1
虛擬碼與程式碼的接近程度完全由你決定,隨著時間的推移,你會發現最適合自己的風格!
第四步:將你能做到的部分翻譯成程式碼
注:對於更簡單的問題,這個步驟可以與上一步一起完成。
這是整個流程中我們第一次必須考慮句法、函式引數和語言規則。你也許沒法寫出所有一切,但沒有關係,把你知道的部分寫出來!
function findFirstMatch(searchString, sourceString) {
let length = searchString.length;
for (let index = 0; index < sourceString.length; index++) {
let possibleMatch = <the LENGTH chars starting at index i>
if (possibleMatch === searchString) {
return index;
}
}
return -1;
}
注意,我在這段程式碼中留了一些空白。這是故意的!我不確定 JavaScript 中切分字串的句法是什麼,所以我要在下一步查一下。
第五步:不要猜測
我看到程式碼新人常會犯一個錯誤:在網際網路上找一些寫著「可能有效」的東西,然後不加測試地將其插入到自己的程式中。你的程式中你不理解的片段越多,你就越不可能最後得到正確的解決方案。
每增加一個你不確定的東西,你的程式可能出錯的方式都會翻一倍。對某事不確定?沒有問題——如果你的程式碼無效,那麼這裡可能就是問題所在。
附註:你的程式可能出錯的方式遵循梅森序列:a(n) = (2^n) — 1
首先測試你的新程式碼。在網上找東西是可以的,但你在將其插入你的程式之前應該在另一個單獨的小空間裡測試一下它,以確保其工作方式和你所想的一樣。
在前一步中,我並不確定在 JavaScript 中該如何選擇一個字串的特定部分。所以我谷歌了一下:
https://www.google.com/search?q=how+to+select+part+of+a+string+in+javascript
第一個結果來自 w3schools,有點舊了,但通常比較可靠。
https://www.w3schools.com/jsref/jsref_substr.asp
基於這個結果,我猜想我應該使用
substr(index, searchString.length)
來提取 sourceString 中的各部分。但這只是一個假設,僅此而已。所以,我首先建立了一個小例子來測試其行為。
>> let testStr = "abcdefghi"
>> let subStr = testStr.substr(3, 4); // simple, easy usage
>> console.log(subStr);
"defg"
>> subStr = testStr.substr(8, 5); // ask for more chars than exist
"i"
現在,我已經確定這個函式的效果了。所以當我將其插入到我的程式中時,我知道如果我的程式沒有效果,那麼原因不會是我加入的這段新程式碼。
有了這樣的保證,我就可以完成程式的最後一部分了。
function findFirstMatch(searchString, sourceString) {
let length = searchString.length;
for (let index = 0; index < sourceString.length; index++) {
let possibleMatch = (
sourceString.substr(index, length));
if (possibleMatch === searchString) {
return index;
}
}
return -1;
}
總結
如果你讀到了這裡,我現在只想說:試試這種策略。回到你上週因為受挫而擱置一旁的程式設計問題。我保證你能立馬看到效果。祝你好運,程式設計快樂!
原文連結:https://blog.usejournal.com/how-a-googler-solves-coding-problems-ec5d59e73ec5