如何正確地使用遞歸

循環

刷了一段時間劍指offer的算法題,收獲了很多,其中對遞歸的印象尤其深。遞歸給我一種四兩撥千斤的感覺。

背景

遞歸,一種能夠裝逼的編程技巧。一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它通常把一個大型復雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重復計算,大大地減少了程序的代碼量,在算法題中有廣泛的應用。

馬上開始裝逼啦!

掌握遞歸的4個要點

寫好一個遞歸算法,主要是把握好如下三個方面:

1. 正確的時機退出。

遞歸很容易造成死循環,而造成死循環的原因就是沒有正確地退出遞歸。

2. 遞歸調用語句的位置

遞歸函數中,位于遞歸調用前的語句和各級被調函數具有相同的順序.如果在遞歸調用語句后面的語句,則按逆序執行,下面會有具體講解。

3. 需要重復執行的邏輯。

遞歸的精髓就是loop,也是遞歸的核心思想,出現重復邏輯一定是必不可少的。但是重復的邏輯需要抽象。抽象出來一個干凈利落的可循環邏輯對程序編寫的幫助很大。

4. 控制邏輯邊界。

控制邊界保證了程序在正確的范圍運行。因為抽象出來的邏輯在一個正確范圍內保證其可以遞歸執行。寫程序也經常因為邊界把控的不準確容易留下bug,比如說數組越界會造成Crash。合理控制邊界,可以提高遞歸效率,避免不必要的調用,進一步還可以幫助遞歸正確退出。

如何正確退出遞歸

在遞歸調用語句前面加上邏輯判斷,在特定情況下不再遞歸調用,以此結束遞歸。舉個栗子,讓程序員寫一個按順序打印數組,普通程序員一般會咔咔咔寫下如下代碼:

std::vector<int>array = {1,2,3,4,5,6,7,8,9};
for (int i = 0; i < array.size() ; i++) {
    printf("%d",array[i]);
}
// 輸出結果:123456789

上面的代碼一目了然,這當然不是我們需要的效果,而文藝程序員是怎么干的?下面的例子使用遞歸的方式按順序打印數組,并且準確地在邊界處結束遞歸。

int main(int argc, const char * argv[]) {
    std::vector<int>array = {1,2,3,4,5,6,7,8,9};
    printfNumber(array, 0);
    return 0;
}

// 順序打印 
void printfNumber(std::vector<int>array, int i) {

    if (i == array.size()) return;
    printf("%d",array[i++]);
    printfNumber(array, i);
}
// 輸出結果:123456789

上面的代碼乍一看遞歸的方式比使用for循環的方式需要更多的代碼,但是在比較復雜的調用中,遞歸才是簡潔代碼,四兩撥千斤的高手。

遞歸調用語句的位置

遞歸調用語句的位置十分敏感,決定了其他語句什么時候執行,上面我們知道了如何按順序打印數組,現在我們只需要將printf語句移至遞歸語句的后面,執行printf語句的順序將倒置過來。

// 逆序打印
void reversePrintfNumber(std::vector<int>array, int i) {    

    if (i == array.size()) return;
    reversePrintfNumber(array, ++i);
    printf("%d",array[--i]);
}
// 輸出結果reverse:987654321

總結下來就是:在循環調用中,遞歸語句前面的函數都是按順序執行,遞歸語句后面的函數都是逆序執行(也就是在最后一個循環的語句反而最先被調用),就是這么簡單。

中場休息

找出重復執行的邏輯

翻轉二叉樹很好地詮釋了遞歸的魅力,只需一個臨時變量,幫助左右子樹交換即可,子樹的子樹翻轉交給下一個遞歸循環過程。

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};*/
class Solution {
public:
    // 翻轉二叉樹
    void Mirror(TreeNode *pRoot) {
        if (pRoot == NULL) return;
        TreeNode *tempNode = pRoot->left;
        pRoot->left = pRoot->right;
        pRoot->right = tempNode;
        // 翻轉左子樹
        Mirror(pRoot->left);
        // 翻轉右子樹
        Mirror(pRoot->right);
    }
};

控制邏輯邊界

查找機器人運動范圍(劍指offer全文最后一道算法題),每次基于當前點,可以向上下左右四個方向移動,已經走過的路徑不再次計數。而下面的例子加入了移動方向進行判斷,如果剛剛是走過得位置,則不再去探索,同時已經走過的路徑也不再進行探索,這樣可以減少不必要的重復操作,提高運行效率,這就是合理控制邏輯邊界。這里使用多種邏輯邊界組合幫助遞歸正確退出。

#pragma mark - 記錄機器人的運動范圍
int movingCount(int threshold, int rows, int cols)
{
    
    for (int i = 0; i < rows; i++) {
        std::vector<int> row;
        for (int j = 0; j < cols; j++) {
            row.push_back(0);
        }
        recordHistory.push_back(row);
        
    }
    recordPath(threshold, 2,0,0);
    printf("----%d",count);
    return count;
}

/* 記錄路徑
 direct
   1
 4 0 2
   3
*/
int count = 0;
std::vector<std::vector<int>> recordHistory;
void recordPath(int threshold, int direct, int positionX, int positionY) {
    if (recordHistory[positionY][positionX] != 1 && getSum(positionX)+getSum(positionY) <= threshold) {
        recordHistory[positionY][positionX] = 1;
        std::vector<int> rowCount = recordHistory[0];
        count++;
        // 上
        if (positionY >= 1 && direct != 3) {
            recordPath(threshold, 1, positionX,positionY-1);
        }
        // 右
        if (positionX < rowCount.size()-1  && direct != 4) {
            recordPath(threshold, 2, positionX+1,positionY);
        }
        // 下
        if (positionY < recordHistory.size()-1  && direct != 1) {
            recordPath(threshold, 3, positionX,positionY+1);
        }
        // 左
        if (positionX >= 1  && direct != 2) {
            recordPath(threshold, 4, positionX-1,positionY);
        }
    }
}

//計算位置的數值
int getSum(int number) {
    int sum=0;
    while(number>0) {
        sum+=number%10;
        number/=10;
    }
    return sum;
}

最后,雖然遞歸有邏輯簡單,代碼清晰的優點,但是并不建議首先考慮用遞歸解決問題。好的程序還是需要通過深入解析寫出更快捷、更巧妙的算法,而不是把問題交給機器暴力解決。當然遞歸加剪枝可以避開一些不必要的搜索,不過大部分還是有替代的辦法。

遠古老司機

我不認識的某些遠古老司機對遞歸的評價

老司機A:遞歸可讀性好這一點,對于初學者可能會反對。實際上遞歸的代碼更清晰,但是從學習的角度要理解遞歸真正發生的什么,是如何調用的,調用層次和路線,調用堆棧中保存了什么,可能是不容易。但是不可否認遞歸的代碼更簡潔。一般來說,一個人可能很容易的寫出前中后序的二叉樹遍歷的遞歸算法,要寫出相應的非遞歸算法就比較考驗水平了,恐怕至少一半的人搞不定。所以說遞歸代碼更簡潔明了

老司機B:遞歸其實是方便了程序員難為了機器。它只要得到數學公式就能很方便的寫出程序。優點就是易理解,容易編程。但遞歸是用棧機制實現的(c++),每深入一層,都要占去一塊棧數據區域,對嵌套層數深的一些算法,遞歸會力不從心,空間上會以內存崩潰而告終,而且遞歸也帶來了大量的函數調用,這也有許多額外的時間開銷。所以在深度大時,它的時空性就不好了。

重要指示

我追求的是極限裝逼,開玩笑啦!以上內容是我自己的理解,僅供參考,現在圈內環境太惡劣,寫個文章都有可能被針對,連大神都不能避免,甚至有公開撕逼、針鋒相對的,太可怕了??。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容

  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執行單位為行(line),也就是一...
    悟名先生閱讀 4,192評論 0 13
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,733評論 18 399
  • 夜半醒了! 并不是睡眠不好,小家伙“騰”地從熟睡中起身,囈語“尿尿”還扯著睡褲!得!!!尿床了!一年了非得要...
    桀驁孤風閱讀 345評論 0 2
  • 八打 八不打 詮釋: 八打 (1)眉頭雙眼 眉頭指眉弓,當用力擊打時會引起眶上血管及神經損傷:雙眼當受到擊打時會使...
    零一間閱讀 1,378評論 0 0
  • 最近得知親戚A、同學B等得了糖尿病,我想安慰下,希望他們引起注意。但是了解甚少,表達出來,好像在說教。好...
    林依Evelyn閱讀 489評論 0 0