【C語言】函數遞歸

2021-12-23 CobaNcy

頭文件

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>

函數遞歸

什麼是遞歸?

先看看 XX 百科的定義:

程序調用自身的編程技巧稱為遞歸( recursion)。遞歸作為一種算法在程序設計語言中廣泛應用。一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解,遞歸策略只需少量的程序就可描述出解題過程所需要的多次重複計算,大大地減少了程序的代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。一般來說,遞歸需要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回。

這種定義看完是不是一臉懵逼?

不舉例子一定不知道他在說什麼,這種定義有還不如沒有,直接上例題說不定還能讓人更好理解

那麼,遞歸到底是什麼?

我們換個詞,遞歸換成回歸,或者遞迴

設想一個場景:n 個人排成了一條隊,第 1 個人在隊尾,第 n 個人在隊首

現在第 1 個人想知道第 n 個人名字,怎麼辦?

第 1 個人讓前面的人幫忙傳話,一人接一人的傳

內容是 「我想知道你的名字」,傳到第 n 個人耳朵裡

第 n 個人在一張紙條上寫上自己的名字,一人接一人的遞迴來

遞迴到第 1 個人手裡,於是第 1 個人知道了第 n 個人的名字

這就是遞歸

根據涉及有限步數的規則或公式,通過對一個或多個前面的元素進行運算來確定一系列元素(例如數字或函數)

遞歸的主要思考方式在於:把大事化小

把大事(自己跑隊首去要人名字)化小(叫人傳話遞迴來)

遞歸的兩個必要條件:

存在限制條件,當滿足這個限制條件的時候,遞歸便不再繼續

舉慄說明:

最簡單的遞歸

int main()
{
 main();
 return 0;
}

很容易就看出,這是一個無限遞歸

會產生 Stack overflow 的錯誤

Stack overflow,就是常說的棧溢出

內存的劃分如下表所示:

名稱作用棧區(stack)存放局部變量、函數形參堆區(heap)存放動態開闢的內存(malloc,calloc)靜態區存放全局變量(static修飾的變量)常量區存放常量代碼區存放只讀代碼

調用一次 main 函數,就會向棧區申請一塊空間,main 函數中又再調用 main 函數,無限遞歸後,棧區空間耗盡,於是棧溢出

有一個網站叫 stackoverflow.com,這是程式設計師的知乎

練習練習 1

接收一個整型值(無符號),按照順序列印它的每一位

例如輸入 1234 ,輸出 1 2 3 4

void print(int n)
{
 if (n > 9)//能停下來的條件
 {
  print(n / 10);//可以滿足那個條件
 }
 printf("%d ", n % 10);
}
int main()
{
 unsigned int num = 0;
 scanf("%d", &num);//1234
 //遞歸
 print(num);
 return 0;
}

遞歸思想:

print(1234)
print(123) 4
print(12) 3 4
print(1) 2 3 4

練習 2

編寫函數不允許創建臨時變量,求字符串長度

int my_strlen(char* str)
{
 //計算字符串(bit - b i t \0)的長度
 //str 存的是 arr 第一個元素的地址
 //判斷 *str的值,不為 \0,就 str++
    //往右邊挪一個長度往第二個元素找
 int count = 0;
 while (*str != '\0')
 {
  count++;
  str++;
 }
 return count;
}

上面代碼用到了臨時變量 count,但規定了不能創建臨時變量

遞歸的函數,仍然用遞歸的方法解決問題

遞歸思想:

my_strlen("bit")
1+my_strlen("it")
1+1+my_strlen("t")
1+1+1+my_strlen("")
1+1+1+0
= 3

重新寫代碼:

int my_strlen(char* str)
{
 if (*str != '\0')
  return 1+ my_strlen(str+1);
 else
  return 0;
}

int main()
{
 char arr[] = "bit";
 //int len = strlen(arr);
 //printf("%d\n", len);
 //模擬實現一個 strlen 函數
 int len = my_strlen(&arr);
 printf("len = %d\n", len);
 
 return 0;
}

練習 3

求 n 的階乘(不考慮溢出)

int factorial(int n)
{
 if (n <= 1)
  return 1;
 else
  return n * factorial(n - 1);
}
int main()
{
 int n = 0;
 int ret = 0;
 printf("請輸入一個數:>");
 scanf("%d", &n);
 ret = factorial(n);
 printf("%d 階乘是 %d\n", n, ret);
 return 0;
}

練習 4

遞歸與迭代

求第 n 個斐波那契數(不考慮溢出)

1,1,2,3,5,8,13,21,34,55,...

int Fib(int n)
{
 if (n <= 2)
  return 1;
 else
  return Fib(n - 1) + Fib(n - 2);
}
//這樣寫效率非常低,試試 n = 50,看看要花多長時間算出來
int main()
{
 int n = 0;
 int ret = 0;
 scanf("%d", &n);
 //TDD - 測試驅動開發
 ret = Fib(n);
 printf("ret = %d\n", ret);
 return 0;
}

可以加一點代碼

if (n == 3)
{
    count++;
}
printf("%d\n", count);

查看求第 40 個斐波那契數的時候,第 3 個斐波那契數被重複計算了多少次

答案是 39088169 次

重複計算量非常大,在這裡,遞歸不是一個好辦法

重新寫代碼:

int Fib(int n)
{
 int a = 1;
 int b = 1;
 int c = 1;
 while (n > 2)
 {
  c = a + b;
  a = b;
  b = c;
  n--;
 }
 return c;
}
int main()
{
 int n = 0;
 int ret = 0;
 scanf("%d", &n);
 ret = Fib(n);
 printf("ret = %d\n", ret);
 return 0;
}

這樣運算速度就快多了,循環迭代更適合求斐波那契數

總結許多問題是以遞歸的形式進行解釋的,這只是因為它比非遞歸的形式更為清晰但是這些問題的迭代實現往往比遞歸實現效率更高,雖然代碼的可讀性稍微差些當一個問題相當複雜,難以用迭代實現時,此時遞歸實現的簡潔性便可以補償它所帶來的運行時的開銷

遞歸就算寫正確了,也不代表就不會出問題

void test(int n)
{
 if (n < 10000)
 {
  test(n + 1);
 }
}
int main()
{
 test(1);
 return 0;
}

這個代碼就會棧溢出

提升

函數遞歸的經典題目

漢諾塔

漢諾塔問題是一個經典的問題。漢諾塔(Hanoi Tower),又稱河內塔,源於印度一個古老傳說。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,任何時候,在小圓盤上都不能放大圓盤,且在三根柱子之間一次只能移動一個圓盤。問應該如何操作?

遞歸思想:

假如只有 1 個盤子,直接從從塔 a 移動到塔 c

n 個盤子,從上到下為 1 到 n,從塔 a 移動到塔 c,以塔 b 中轉

不管中間過程如何,第 n 個盤子(最大那個),一定得想辦法先移到塔 c

既然第 n 個盤子移到塔 c,剩下的 n-1 個盤子不可能仍在塔 a,它們一定移到了塔 b

這樣,我們第一步要做的就是:

把第 n 個盤子移到塔 c,剩下的 n-1 個盤子移到塔 b

不管中間過程如何,這個邏輯是絕對正確的

下一步就變成了:

n-1 個盤子,從上到下為 1 到 n-1,從塔 b 移動到塔 c,以塔 a 中轉

重複以上過程,遞歸就出來了

偽代碼:

func:
if n!=0 then            ;預定值
  func(n-1, a, c, b)    ;將n-1個盤子由a移動到b,以c為輔助柱子(注意參數順序)
  move a[n] to c        ;將a上的最後一個盤子移動到c
  func(n-1, b, a, c)    ;將n-1個盤子由b移動到c,以a為輔助柱子
endif                   ;完成

代碼實現:

void hanoi(int n,char a,char b,char c)
{
 if (n == 1)
 {
  printf("從塔%c 移到到塔%c\n", a, c);
 }
 else
 {
  hanoi(n - 1, a, c, b);//n-1個盤子,從塔a移動到塔b,以塔c中轉
  printf("從塔%c 移到到塔%c\n", a, c);//將塔a的盤子移動到塔c
  hanoi(n - 1, b, a, c);//n-1個盤子,從塔b移動到塔c,以塔a中轉
 }
}
int main()
{
 int n = 0;
 printf("請輸入要從塔 a 轉移到塔 c 的盤子個數:>");
 scanf("%d", &n);
 hanoi(n, 'a', 'b', 'c');
 return 0;
}

相關焦點

  • Python進階之遞歸函數的用法及其示例
    作者 | 程式設計師adny責編 | 徐威龍封圖| CSDN│下載於視覺中國出品 | AI科技大本營(ID:rgznai100)本篇文章主要介紹了Python進階之遞歸函數的用法及其示例,現在分享給大家,也給大家做個參考
  • C語言程序設計試題與答案B卷
    每小題1分,共20分)1、一個C語言程序是由( )。A)本程序的main函數開始,到main函數結束 B)本程序文件的第一個函數開始,到本程序文件的最後一個函數結束C)本程序的main函數開始,到本程序文件的最後一個函數結束D)本程序文件的第一個函數開始,到本程序main函數結束
  • 如何優雅地使用javascript遞歸畫一棵結構樹
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫遞歸和尾遞歸簡單的說,遞歸就是函數自己調用自己,它作為一種算法在程序設計語言中廣泛應用。
  • C語言函數調用過程中的內存變化解析
    相信很多編程新手村的同學們都會有一個疑問:C 語言如何調用函數的呢?局部變量的作用域為什麼僅限於函數內?這個調用不是指C 語言上的函數調用的語法,而是在內存的視角下,函數的調用過程。本文將從C 語言調用實例,內存視角,反彙編代碼來探討C 語言函數的調用過程,也可以說是C 語言函數調用過程圖解。通過這個C 語言函數調用過程圖解,同學們將會知道,C 語言函數在調用時,內存空間是怎樣變化的。 要想理解這一個過程還好涉及到函數棧幀的概念。
  • 箭頭函數=> 的使用與局限 - ES6中JavaScript新特性之函數
    長期以來,JavaScript 語言的`this`對象一直是一個令人頭痛的問題,在對象方法中使用`this`,必須非常小心。箭頭函數」綁定」`this`,很大程度上解決了這個困擾。,所以一些函數式程式語言將其寫入了語言規格。
  • C語言程序設計試題1
    學年期末考試級專業()《C語言程序設計f;,將數學表達式C= (F-32)能正確表示成C語言賦值表達式的是(   ) A.c=5*(f-32)/9 B.c=5/9(f-32) C.c=5/9*(f-32) D.c=5/(9*(f-32))6.設int i=10;,表達式30-i<=i<=9的值是(   )A.0 B.1
  • 知識分享:C 語言函數指針之回調函數
    回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。 回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應。
  • 函數式編程那些事兒
    相反,函數式程式語言依賴於遞歸進行迭代。遞歸是使用遞歸函數實現的,遞歸函數會重複調用自己,直到達到基本情況為止。引用透明性一旦在函數式程式語言中定義了變量,就不允許在程序執行期間更改它們持有的值。這稱為引用透明性。它確保相同的語言表達式給出相同的輸出。功能程序沒有任何賦值語句。
  • C語言一百例(21-30)
    =%e\n",s);}26,題目:利用遞歸方法求5!。    程序分析:遞歸公式:fn=fn_1*4!程序原始碼:#include <stdio.h>main(){int i;int fact();for(i=0;i<5;i++) printf("\40:%d!
  • C語言的幾個入門階段
    C語言,在大學裡普遍被當作第一門程式語言,用於編程入門,以及數據結構和算法的教學。雖然比C++要容易點,但因為指針的存在,C其實就是個高級的彙編:(並不是很好學,尤其是它的壓軸章節:指針。說明適應了程式語言從0開始數數,已經會使用兩層的for循環,並且會使用printf列印排序結果,適應了計算機不會選擇最大的和逆序最多的數字,而只能使用「笨辦法」的特點。2,會寫快速排序。
  • 5分鐘弄懂的五大常用算法之「分治法」,以C語言歸併排序算法為例
    所謂「不能拆分」,其實就是數組中只有一個元素,也即 start 等於 end,這一過程非常適合使用遞歸實現。)/2;divide(arr, start, mid);divide(arr, mid+1, end);}關於 divide() 遞歸函數的理解,我之前的這篇文章詳細討論過。
  • C Primer Plus怎樣高效學?C語言大神案例值得借鑑!
    我們常常聽到有人爭論「Python、Java、PHP......是這個世界上最好的語言」,卻很少聽到有人誇讚C語言,為什麼呢?因為C語言實在是太太太太難了......為什麼這麼多人學不會C語言呢?因為很多人覺得用C語言作為入門語言覺得太難了,裡面還有指針,回調,遞歸之類的操作太難了。
  • c語言中sscanf函數的高級用法
    sscanf函數用來從給定字符串中讀取所需數據,用在一些數據轉換時比較方便。常見用法和scanf類似,用%s,%d等獲取字符串和整數。但在%號後可以支持更多的格式,甚至是正則表達式,這樣一來sscanf的功能就比較強大了。
  • 用遞歸和後綴表達式解決「24 點遊戲」
    一是遞歸方式;一是後綴表達式(也稱為逆波蘭表達式)方法。下面我們分別來介紹這兩種方法。一:遞歸方式思路如下:4個數字中任取2個數,使用一種運算得到一個數,將這個數和另外兩個沒有參與運算的數放在一起,這3個數再任取2個數進行運算,得到的結果再與另外一個數放在一起。
  • C語言 主函數 int main() 與 return 0;
    收錄於話題 #初學C語言     我們已經有了合適的開發環境 Dev-C++ ,又有了一些好用的  「工具箱」 比如:#include<stdio.h> 。
  • C語言程序設計試題及答案
    A) 程序行 B) 語句 C) 函數 D) 字符2、C語言規定,在一個源程序中main函數的位置( )。A) 必須在最開始 B) 必須在系統調用的庫函數的後面C) 可以任意 D) 必須在最後3、下列符號串中符合C語言語法的標識符是( )。
  • c語言代碼表白公式_表白代碼c語言
    c語言代碼表白公式?表白代碼c語言?
  • C語言基礎知識
    C 傳遞指針給函數通過傳遞指針給函數,可以直接修改原參數(實參),而不是引用實參到形參。pow(x, y)函數C 和 Python 語言的 pow(x, y) 方法都是用於返回 (x 的 y 次方) 的值,C 語言中其原型為:double pow(double x, double y)。
  • 任性的C語言之父:因拒付裝訂費錯失博士學位,論文52年後重見天日
    我自己更喜歡過程式語言,而不是函數式語言。」且不論這些自我評價是否客觀,Ritchie 選擇的道路的確將他帶到了一個讓自己大放異彩的領域。儘管 Ritchie 在計算機領域享有盛名,但鮮為人知的是,他的博士學位論文沒有幾個人親眼見過,因為這份論文——遺失了。
  • C語言項目中.h和.c文件的關係和概念
    理論上來說C文件與頭文件裡的內容,只要是C語言所支持的,無論寫什麼都可以的,比如你在頭文件中寫函數體,只要在任何一個C文件包含此頭文件就可以將這個函數編譯成目標文件的一部分。