從leetcode-Letter Combinations of a Phone Number歸納遞迴操作
【原創文章 作者:NicoleHe】
這是一道非常簡單的遞迴呼叫的題,但是我在思考時產生了一些混亂,隱隱約約覺得很熟悉,就是不能用程式碼實現出來。
因此這裡針對這類“依次新增”的問題給出大致的遞迴操作步驟。以後會更新對應的棧操作實現。
首先放出原題:Letter Combinations of a Phone Number
Given a string containing digits from
2-9
inclusive, return all possible letter combinations that the number could represent.A mapping of digit to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.
- 這個題目的意思是,就像我們用九鍵手機輸入法,輸入幾個數字,那麼我們需要給出所有的字母可能組合。
- 初看起來,題目非常簡單,只要一個一個迴圈就好了。但是要怎麼做到“一個一個迴圈”呢?如果只用for迴圈,我們是無法知道需要多少層for的,因為層數取決於輸入數字的個數,是一個不確定的值。那麼這個時候,就需要遞迴呼叫了。
- 其實這個問題和漢諾塔問題存在著類似的關係,因為漢諾塔問題雖然也是來回折騰每個碟片,初看起來需要迴圈,但是碟片數目不確定,因此使用遞迴。
- 那麼是不是可以在腦海裡有這樣的印象:對於需要逐個解決的元素,元素的個數是不確定的,而又需要記錄元素處理的過程,這個時候就可以考慮遞迴。
- 這裡的元素是指:在這個問題中是數字對應的字母,在漢諾塔問題中就是不同位置的碟片。
當然,漢諾塔和數字字母轉換問題有一點微妙的區別,那就是漢諾塔的解決方案只有一種,但是數字字母轉換問題有多個可能結果,也就是需要儲存每次呼叫的“路徑”。這個對演算法結構沒有影響,只是在多個可能值多條路徑的時候,可以考慮把上一輪處理後的臨時結果作為引數,傳到下一輪遞迴中,遞迴終止的時候,把得到的可能結果/路徑之一記錄到result中。(當然,這裡的result需要作為遞迴呼叫的引用引數,這樣在遞迴止的時候才能把結果記錄進去;如果需要逆序,就將元素處理結果作為遞迴函式的返回值,呼叫返回後即可得到該元素對應的結果)
現在回到這個數字轉成字母的問題:首先我構造了一個字串向量,記錄每個數字可能對應的字母。(Letters[0]表示數字2對應的可能字母,用字串"abc"表示)
1 vector<string> letterCombinations(string digits) { 2 int len = digits.size(); 3 vector<string> ans; 4 if(len == 0) return ans; 5 6 string Letters[8]; 7 Letters[0] = "abc"; 8 Letters[1] = "def"; 9 Letters[2] = "ghi"; 10 Letters[3] = "jkl"; 11 Letters[4] = "mno"; 12 Letters[5] = "pqrs"; 13 Letters[6] = "tuv"; 14 Letters[7] = "wxyz"; 15 16 string s = ""; 17 getLetter(0, ans, s, digits, Letters); 18 return ans; 19 }
注意:實際上的數字和vector索引是相差2的。
來分析一下這個遞迴函式的引數:
- 需要儲存臨時結果。那麼這個空字串s就起到這個作用。
- 其次就是ans,也是前面提到的能儲存最終結果的資料結構result,把它的引用作為遞迴函式的引數,那麼遞迴要結束的時候,就可以把得到的字母序列儲存進去了。
- 0是輸入digits的索引,表示現在將要對第0個數字進行處理。這個Index說明了這個遞迴進行到了第幾個數字,這樣才能確定是否已經開始對最後一個數字進行操作了,便於確定遞迴結束條件。
- digits作為引數,是為了在遞迴函式內部能夠方便地取到即將要進行操作的數字。
- Letters就是儲存數字-字母對應關係的資料結構,我的初衷是將其作為全域性變數,這裡每次都作為引用引數傳進遞迴函式,效果是一樣的。
現在具體來看getLetter遞迴函式的實現:
1 void getLetter(int index, vector<string>& ans, string s, string& digits, string* Letters) { 2 if(index == digits.size()) { 3 ans.push_back(s); 4 return; 5 } 6 int num = digits[index] - '0' - 2; 7 int sub_len = Letters[num].size(); 8 for(int i = 0; i < sub_len; i++) { 9 getLetter(index+1, ans, s+Letters[num][i], digits, Letters); 10 } 11 }
來看遞迴函式的兩大元素:
- 遞迴條件:用Index和digits的長度進行比較,index表示即將處理第index+1個數字(index從0開始),但是總共只有Index==digits.size()個數字,說明此時對所有的數字都處理完了,將臨時結果s儲存到ans中就可以返回了。
- 遞迴體:如果還不能返回,那麼首先通過傳進來的index和digits確定我們這個時候需要處理的數字digits[index],用表示,通過Letters得到num所有可能對應的字母(我這裡是將所有可能的字母用一個string來標識),這個時候就可以使用for迴圈,得到每個可能的字母Letters[num][i],與已有的臨時結果s相加,作為新的臨時結果傳進下一次遞迴呼叫中。
再來解釋下s+Letters[num][i]; Letter是我定義的字串陣列,這個陣列的索引+2就是對應的數字,所以第0個字串對應數字2的可能取值,第1個字串對應數字3的可能取值等,Letters[num]就是對應數字可能取到的所有字母組成的字串;字串也是支援索引操作的,所以我用了for迴圈,i表示這個字串的第i+1個字母,也就是用for迴圈把這個數字所有可能取到的字母遍歷一遍。
大體流程如下:
取第1個數字 ==> 對第1個數字的所有可能字母,新建臨時結果s(此時只有一個字母)==> 遞迴呼叫,取第2個數字 ==> 對第二個數字的所有可能字母,新建臨時結果為s+a,其中a為可能取到的字母(通過for迴圈遍歷所有情況)==> 遞迴呼叫,取第3個數字 ==> ... ==> 取完了最後一個數字,將最終的臨時結果s儲存到ans中。
如輸入234:
其中每個處理數字後的花括號都是一次遞迴呼叫過程,花括號內部是迴圈遍歷每種可能的字母情況,其中的s是每次遞迴呼叫作為臨時結果的傳遞引數,走到最後一步的時候,將臨時結果s儲存到ans中,這樣,ans就儲存了每種可能的結果。
我的第一篇原創博文~如果大家有任何對於格式或者內容上的建議或評價,請不吝賜教,謝謝!