1. 程式人生 > >從leetcode-Letter Combinations of a Phone Number歸納遞迴操作

從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的。

來分析一下這個遞迴函式的引數:

  1. 需要儲存臨時結果。那麼這個空字串s就起到這個作用。
  2. 其次就是ans,也是前面提到的能儲存最終結果的資料結構result,把它的引用作為遞迴函式的引數,那麼遞迴要結束的時候,就可以把得到的字母序列儲存進去了。
  3. 0是輸入digits的索引,表示現在將要對第0個數字進行處理。這個Index說明了這個遞迴進行到了第幾個數字,這樣才能確定是否已經開始對最後一個數字進行操作了,便於確定遞迴結束條件。
  4. digits作為引數,是為了在遞迴函式內部能夠方便地取到即將要進行操作的數字。
  5. 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   }

來看遞迴函式的兩大元素:

  1. 遞迴條件:用Index和digits的長度進行比較,index表示即將處理第index+1個數字(index從0開始),但是總共只有Index==digits.size()個數字,說明此時對所有的數字都處理完了,將臨時結果s儲存到ans中就可以返回了。
  2. 遞迴體:如果還不能返回,那麼首先通過傳進來的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就儲存了每種可能的結果。

 

 

我的第一篇原創博文~如果大家有任何對於格式或者內容上的建議或評價,請不吝賜教,謝謝!