面試總結之-遞歸算法分析

遞歸的分析

         首先是,“你這個遞歸能不能寫成個不需要棧空間的遞歸?”,答:“尾遞歸(tail-recursive)”。

好冷~不過真有一個面試官這樣問我了。不過尾遞歸不怎麼考,因爲你如果能寫成尾遞歸,說明系統也不需要用棧空間來保存遞歸的狀態了,那你把它改成迭代的形式就不難了。

         遞歸的基本形式是:

 T Recursive(arg_list* args){
 if(args==BASIC_CASE)
    return T(BASIC_CASE);
 for(int i=0;i<n;i++)
    t[i] = Recursive(args+i);
 //combine results
}

         或者說:

T Recursive(arg_list* args){
 if(args==BASIC_CASE)
    return T(BASIC_CASE);
 //process the data
 for(int i=0;i<n;i++)
    t[i] = Recursive(args+i);
}

         兩種遞歸的方式,你也可以說是一樣的,但是我覺得這是兩種不同的子問題的邏輯。第一種是先遞歸,再處理(比如MergeSort),一種是先處理,再遞歸(比如qsort)。一般遞歸的開始都有一個終止條件,當然也可以沒有,只要當到達終止條件之後,所有判斷需不需要繼續遞歸下去的條件都爲false,自然就終止(比如qsort),不過我覺得,終止條件還是寫在函數的遞歸的開頭比較好,這樣的話別人看你的遞歸代碼會更加容易理解。遞歸也是面試考察的重點,在寫遞歸前,首先得想清楚這個題目是不是需要用遞歸解決(遞歸寫起來更方便?題目定義本來就是遞歸形式?考官要求遞歸?= =!),這樣說是因爲,一般遞歸程序在時間和空間上都比不上迭代的程序,時間上來說,即使不考慮保存臨時變量壓棧出棧的消耗,遞歸的過程也是在解空間搜索,到底再一層層返回最頂層(把你的遞歸想象成一棵多叉樹吧),而迭代只需要搜索到最底層就完成任務了;空間來說,就是棧空間了,爲了保存當前程序的狀態,遞歸到下一重時,棧空間的消耗也是不能忽略的,遞歸太深有時候還會stack overflow。

         講到棧空間,要提一句的是,面試時有時候會碰到面試官要求一個constant space的程序,這時候你應該問一下面試官,遞歸的棧空間消耗算不算space。嚴格來說,這當然算space(棧空間不是空間啊!),不過有時候面試官就是腦抽了,自己覺得遞歸裏面自己不用額外空間就不算了,那你也只能跟着他思維走了。

         確定要寫遞歸之後,要想清楚你的遞歸要怎麼寫,幾個關鍵步驟:

1. 遞歸終止條件,也就是那個問題是你不需要遞歸解決的,比如if(root==NULL)

2. 先遞歸還是先處理,參照上面的兩個框架

3. 處理的邏輯怎麼寫,這部分特別搞笑~貌似見過不少人忘記寫這一塊的了,直接遞歸下去就覺得程序可以幫你解決這個問題,就像MergeSort,分段去MergeSort之後,回來卻不把結果Merge。

         遞歸面的不少,而且而且,經常是等你寫完一個遞歸程序之後,讓你寫一個非遞歸程序,這就轉入下一part,怎麼遞歸轉非遞歸。

         理論上,所有遞歸都能寫成非遞歸,你只要想一下在運行遞歸程序時,計算機是怎麼搞的,然後在你的非遞歸代碼中模擬遞歸的邏輯就好了:首先,進入遞歸前,要保存所有在遞歸結束後需要用到的變量,然後我們定義一個結構體,把這些變量都扔進去;另外,系統的話可以保存函數運行到哪裏的信息,遞歸結束後直接到達遞歸代碼的下一行繼續運行,人工模擬遞歸時一般不會這樣搞,這樣我們可以需要檢查棧頂的結構體,來判斷,現在應該怎麼做。一般來說,加一個bool變量,表示是不是回溯就好(這個不是一定行得通,有時候可能是其他參數)。具體通過下面一個例子描述:生成輸入數組的全排列(假設數組元素都不同)

遞歸代碼:

voidGetPermutation(vector<int>& arr,vector<vector<int>>&result,int k){
 if(k==arr.size()-1){
    result.push_back(arr);
    return;
 }
 for(int i=k;i<arr.size();i++){
    std::swap(arr[i],arr[k]);//i,k交換,然後生成k+1到n的所有排列
    GetPermutation(arr,result,k+1);
    std::swap(arr[i],arr[k]);//還原
 }
}
vector<vector<int>>GetPer(vector<int>& arr){
 vector<vector<int>> result;
 GetPermutation(arr,result,0);
 return result;
}

這是一個比較標準的遞歸生成所有排列的程序,在把它改成迭代之前,先說說這種程序。這裏用到的遞歸框架應該屬於第二種:先處理再遞歸。遞歸後面又多做一次swap是因爲我改變了原來的數組,如果你copy一個新的數組出來,在後面也可以不用還原(但是會慢)。還有一個特點是,這種遞歸是在遞歸的最深處得到結果(其實就是因爲先處理再遞歸嘛~),也由於這個原因,這種遞歸經常把解作爲參數傳入,相反,對於那種先遞歸再處理的,經常把解return回來。

         怎麼改成迭代的程序呢,遞歸的部分是GetPermutation(arr,result,k+1),首先要想想,從這裏遞歸,我們需要什麼信息,才能在遞歸結束之後接着遞歸後面的語句繼續工作,怎麼看需要記錄什麼信息呢,其實就是看這個遞歸結束之後還需要做什麼操作,這些操作需要用到的信息都記錄下來就好了。這裏應該需要記錄i和k的值。也就是,先把pair(0,0)壓棧,每次取出棧頂元素處理,如果棧頂元素的k==arr.size()-1,那麼本層遞歸結束,把當前結果保存到result;否則進入循環遞歸(參考遞歸代碼),如果top.i<arr.size(),表示成功進入循環,這時候首先swap,然後注意了,這裏是最容易出錯的地方,由於你不知道現在棧頂元素(對應於遞歸樹的某個節點)是第一次經過還是回溯時經過,所以沒法判斷是不是需要pop掉。我在代碼中多加了一個bool的變量來記錄是不是回溯,如果是,就不進入子樹遞歸了(也就是不加入新的節點入棧了)。這個過程比較繞,覺得亂的話就根據idea自己想一下要怎麼做,然後自己實現一遍再重新看。

Code:

struct Node{
 int i,k;
 bool isBacktrack;
 Node(int a,int b){i=a;k=b;isBacktrack=false;}
};
vector<vector<int>>GetPer(vector<int>& arr){
 vector<vector<int>> result;
 stack<Node> stk;
 stk.push(Node(0,0));
 while(!stk.empty()){
    Node tmp = stk.top();
    if(tmp.k==arr.size()-1){ //這種情況相當於遞歸代碼的終止條件,得pop一下
      result.push_back(arr);
      stk.pop();
    }else if(tmp.i<arr.size()){ //這種情況相當於遞歸代碼在執行for循環
      std::swap(arr[tmp.i],arr[tmp.k]);
      if(!tmp.isBacktrack){   //這部分是for循環的第一個swap語句
        stk.top().isBacktrack = true;
        stk.push(Node(tmp.k+1,tmp.k+1)); //執行完第一個語句之後,會有一個往下一重的遞歸,所以有一個壓棧動作
      }
      else{        //else表示現在是第二個swap,所以要pop掉舊的,進入下一重遞歸
        stk.pop();    
        stk.push(Node(tmp.i+1,tmp.k));
      }
    }else stk.pop();  //for循環搞完了,相當於另一個終止條件,還得pop一下
  }
 return result;
}

    主要內容寫在代碼註釋了。代碼有不少if,else,都是在判斷現在到底在相當於遞歸程序的那一部分。這個代碼有點繁瑣,邏輯上如果沒有遞歸代碼的對比的話,也非常不好懂,寫在這裏主要是爲了舉個例子怎麼樣把遞歸代碼轉換成非遞歸代碼。這個轉換方法總結下就是:

1. 想好遞歸需要壓棧的信息,封裝成一個struct;

2. 想想遞歸的邏輯(你不能直接保存一個函數的指針),在代碼中利用第一步的struct保存的信息來重構這個邏輯。

         要寫出一個程序的非遞歸程序,先想遞歸再轉換成非遞歸不是必須的,因爲可能本來就有更好的非遞歸寫法;真要這樣轉換時,模仿計算機保存所有需要的信息入棧也不是必須的,只能說這樣做是一個普遍適用的方法,作爲一個智能生物,對於具體的程序,想到具體的遞歸轉非遞歸的方法也不奇怪(不像計算機,只能機械解決)。
         最後加幾道leetcode上關於遞歸的題目(關於遞歸的題目是很多的了~),可以想想非遞歸解法:

Generate Parentheses

Palindrome Partitioning

Unique Binary Search Trees II

發佈了12 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章