遞迴教學-深入淺出解釋與應用

更新 發佈閱讀 12 分鐘

前導

遞迴就是函式在執行過程中呼叫自身,並通過結束條件呼叫堆疊來解決問題。

這種方式通常用於解決可以分解為相同問題的子問題的情況。

在下面圖片中,如果將呼叫函式 B 改為呼叫函式 A 本身,那麼這就是一個典型的遞迴結構。

raw-image

具體來說:

  1. 函式 A 在執行過程中呼叫了自身(即 A)。
  2. 每次呼叫都會產生一個新的執行環境(包括參數、區域變數、返回值址等)。
  3. 這些執行環境會按照呼叫順序被壓入呼叫堆疊(Call Stack),直到遞迴結束條件被滿足。

遞迴的關鍵要素

  1. 遞迴結束條件(Base Case)
    • 這是遞迴的終止條件,確保遞迴不會無限進行下去。
    • 例如,在計算階乘時,factorial(0) = 1 就是一個結束條件。
  2. 遞迴呼叫(Recursive Call)
    • 函式在執行過程中呼叫自身,並將問題分解為更小的子問題。
    • 例如,factorial(n) = n * factorial(n-1)
  3. 呼叫堆疊(Call Stack)
    • 每次遞迴呼叫都會產生一個新的執行環境,這些環境會被壓入堆疊。
    • 當遞迴結束條件滿足時,堆疊會開始彈出(最後放進堆疊的最先被取出),也就是依次返回結果。

現在我們先來實作圖片中的程式碼:

程式碼實作

#include <stdio.h>

// 函式 B
void B() {
printf("進入函式 B\n");
printf("離開函式 B\n");
}

// 函式 A
void A() {
printf("進入函式 A\n");
B(); // 在 A 中呼叫 B
printf("離開函式 A\n");
}

// 主函式
int main() {
printf("Start\n");
A(); // 在 main 中呼叫 A
printf("End\n");
return 0;
}

輸出:

Start
進入函式 A
進入函式 B
離開函式 B
離開函式 A
End

遞迴就只是呼叫本身並添加結束條件而已:

#include <stdio.h>

// 函式 A(遞迴函式)
void A(int n) {
printf("進入函式 A,n = %d\n", n);
if (n > 0) {
A(n - 1); // 遞迴呼叫 A
}
printf("離開函式 A,n = %d\n", n);
}

// 主函式
int main() {
printf("進入函式 main\n");
A(2); // 在 main 中呼叫 A,並傳入參數 n = 2
printf("離開函式 main\n");
return 0;
}

輸出:

Start
進入函式 A,n = 2
進入函式 A,n = 1
進入函式 A,n = 0
離開函式 A,n = 0
離開函式 A,n = 1
離開函式 A,n = 2
End

進階遞迴使用示例

接著,我們以遞迴實現迴文檢查的邏輯,幫助你更加理解。

#include <stdio.h>
#include <string.h>

// 遞迴函式:檢查字串是否為迴文
int isPalindrome(char str[], int left, int right) {
// 當 left >= right 時,表示檢查完成,是迴文(結束條件或稱基本情況)
if (left >= right) {
return 1; // 是迴文
}
// 如果左右字符不相等,則不是迴文(結束條件或稱基本情況)
if (str[left] != str[right]) {
return 0; // 不是迴文
}
// 遞迴檢查下一對字符
return isPalindrome(str, left + 1, right - 1);
}

// 主函式
int main() {
char str[] = "madam"; // 要檢查的字串
int length = strlen(str);

// 呼叫遞迴函式檢查是否為迴文
if (isPalindrome(str, 0, length - 1)) {
printf("'%s' 是迴文\n", str);
} else {
printf("'%s' 不是迴文\n", str);
}

return 0;
}

步驟解析

例子 1:"abcda"

字串內容:

索引: 0 1 2 3 4
字符: a b c d a

執行過程:

  1. 初始呼叫
    • isPalindrome("abcda", 0, 4)
    • 比較 str[0] ('a') 和 str[4] ('a'),相等。
    • return isPalindrome("abcda", 1, 3)。意思就是返回一個遞迴函式。所以返回的結果有待遞迴函式執行。
  2. 第一次遞迴
    • isPalindrome("abcda", 1, 3)
    • 比較 str[1] ('b') 和 str[3] ('b'),不相等。
    • 返回 0(不是迴文)。
  3. 返回結果
    • 因為isPalindrome("abcda", 1, 3)的返回結果為0isPalindrome("abcda", 0, 4)中的return isPalindrome("abcda", 1, 3)即為 return 0;
    • 最終結果為 (不是迴文)。

輸出:

'abcda' 不是迴文

例子 2:"madam"

字串內容:

索引: 0 1 2 3 4
字符: m a d a m

執行過程:

  1. 初始呼叫
    • isPalindrome("madam", 0, 4)
    • 比較 str[0] ('m') 和 str[4] ('m'),相等。
    • return isPalindrome("madam", 1, 3)。意思就是返回一個遞迴函式。所以返回的結果有待遞迴函式執行。
  2. 第一次遞迴
    • isPalindrome("madam", 1, 3)
    • 比較 str[1] ('a') 和 str[3] ('a'),相等。
    • return isPalindrome("madam", 2, 2)。意思就是返回一個遞迴函式。所以返回的結果有待遞迴函式執行。
  3. 第二次遞迴
    • isPalindrome("madam", 2, 2)
    • 因為 left == right,情況達成,返回 1(是迴文)。
  4. 返回結果
    • 所有遞迴呼叫都返回 1,最終結果為 1(是迴文)。

輸出:

'madam' 是迴文

河內塔遞迴問題

最後,河內塔是一個經典的遞迴問題,問題描述:

  1. 有三根柱子,分別稱為 ABC
  2. 在柱子 A 上有 n 個大小不同的圓盤,圓盤從上到下依序變大。
  3. 目標是將所有圓盤從柱子 A 移動到柱子 C

移動需要遵守以下規則:

  1. 一次只能移動一個圓盤。
  2. 不能將較大的圓盤放在較小的圓盤上。
  3. 可以使用一根輔助柱子。

遞迴解法

我們可以用遞迴的方式來解決這個問題。以下是遞迴的思路:

  1. 基本情況:如果只有一個圓盤(n = 1),直接將它從柱子 A 移動到柱子 C。也就是直接將圓盤從 source 移動到 target
  2. 遞迴情況
    • 將 n-1 個圓盤從柱子 A 移動到柱子 B(使用柱子 C 作為輔助)。
    • 將第 n 個圓盤(最大的圓盤)從柱子 A 移動到柱子 C
    • 將 n-1 個圓盤從柱子 B 移動到柱子 C(使用柱子 A 作為輔助)。

程式碼

#include <stdio.h>

// 河內塔遞迴函式
void hanoi(int n, char source, char auxiliary, char target) {
if (n == 1) {
// 基本情況:只有一個圓盤,直接移動
printf("將圓盤 1 從 %c 移動到 %c\n", source, target);
} else {
// 遞迴步驟:
// 1. 將 n-1 個圓盤從 source 移動到 auxiliary(使用 target 作為輔助)
hanoi(n - 1, source, target, auxiliary);

// 2. 將第 n 個圓盤從 source 移動到 target
printf("將圓盤 %d 從 %c 移動到 %c\n", n, source, target);

// 3. 將 n-1 個圓盤從 auxiliary 移動到 target(使用 source 作為輔助)
hanoi(n - 1, auxiliary, source, target);
}
}

// 主函式
int main() {
int n = 3; // 圓盤數量
printf("河內塔解決方案(n = %d):\n", n);
hanoi(n, 'A', 'B', 'C'); // 將圓盤從 A 移動到 C,使用 B 作為輔助
return 0;
}

程式碼解析

  1. hanoi 函式
    • 這是遞迴函式,用於解決河內塔問題。
    • 參數:
      • n:當前要移動的圓盤數量。
      • source:起始柱子。
      • target:目標柱子。
      • auxiliary:輔助柱子。
    • 基本情況: 當 n == 1 時,直接將圓盤從 source 移動到 target
    • 遞迴情況:
      • 將 n-1 個圓盤從 source 移動到 auxiliary(使用 target 作為輔助)。
      • 將第 n 個圓盤從 source 移動到 target
      • 將 n-1 個圓盤從 auxiliary 移動到 target(使用 source 作為輔助)。
  2. main 函式
    • 定義圓盤數量 n
    • 呼叫 hanoi 函式,開始解決問題。

輸出:

河內塔解決方案(n = 3):
將圓盤 1 從 A 移動到 C
將圓盤 2 從 A 移動到 B
將圓盤 1 從 C 移動到 B
將圓盤 3 從 A 移動到 C
將圓盤 1 從 B 移動到 A
將圓盤 2 從 B 移動到 C
將圓盤 1 從 A 移動到 C

補充示意圖

  • n=2 的遞迴呼叫樹:
hanoi(2, A, B, C)
├── hanoi(1, A, C, B) → 將圓盤 1 從 A 移動到 B
├── 將圓盤 2 從 A 移動到 C
└── hanoi(1, B, A, C) → 將圓盤 1 從 B 移動到 C
  • n=3 的遞迴呼叫樹:
hanoi(3, A, B, C)
├── hanoi(2, A, C, B)
│ ├── hanoi(1, A, B, C) → 將圓盤 1 從 A 移動到 C
│ ├── 將圓盤 2 從 A 移動到 B
│ └── hanoi(1, C, A, B) → 將圓盤 1 從 C 移動到 B
├── 將圓盤 3 從 A 移動到 C
└── hanoi(2, B, A, C)
├── hanoi(1, B, C, A) → 將圓盤 1 從 B 移動到 A
├── 將圓盤 2 從 B 移動到 C
└── hanoi(1, A, B, C) → 將圓盤 1 從 A 移動到 C
  • 這個樹狀結構反映了河內塔的主要遞迴邏輯:

先遞迴移動 n−1 個圓盤 → 再移動最大圓盤 → 最後遞迴移動 n−1 個圓盤。

留言
avatar-img
留言分享你的想法!
avatar-img
電資鼠 - 您的學習好夥伴
13會員
229內容數
在當今數位時代,電資領域人才需求爆發式成長,不論是前端網頁設計、嵌入式開發、人工智慧、物聯網還是軟硬體整合,這些技術都在改變世界。而掌握 C/C++、Python、數位邏輯、電路學與嵌入式開發等大學電資領域的課程,正是進入這個高薪、高需求產業的關鍵!
2025/03/09
雙向串列 (Double Linked List, DLL) 是一種鏈結資料結構,本章節將以完整註解,搭配關鍵操作地方的圖示輔助學習,讓你輕鬆搞懂複雜觀念,並透過C語言實作。
Thumbnail
2025/03/09
雙向串列 (Double Linked List, DLL) 是一種鏈結資料結構,本章節將以完整註解,搭配關鍵操作地方的圖示輔助學習,讓你輕鬆搞懂複雜觀念,並透過C語言實作。
Thumbnail
2025/03/08
環狀鏈結串列是一種特殊的鏈結串列,其最後一個節點的指標指向第一個節點,而非 NULL,形成一個循環結構。本章節將以豐富圖示,引導讀者了解環狀串列在各種地方執行插入和刪除節點的步驟,輕鬆學會資工科的專業知識-環狀串列。
Thumbnail
2025/03/08
環狀鏈結串列是一種特殊的鏈結串列,其最後一個節點的指標指向第一個節點,而非 NULL,形成一個循環結構。本章節將以豐富圖示,引導讀者了解環狀串列在各種地方執行插入和刪除節點的步驟,輕鬆學會資工科的專業知識-環狀串列。
Thumbnail
2025/03/07
本章節將探討右上三角稀疏矩陣。
Thumbnail
2025/03/07
本章節將探討右上三角稀疏矩陣。
Thumbnail
看更多
你可能也想看
Thumbnail
本文章討論如何使用動態規劃和回頭查看技巧來計算最長遞增子序列的長度,並提供了相關的測試案例和範例。本文還包括了詳細的演算法和程式碼示例,以及時間和空間複雜度的分析。
Thumbnail
本文章討論如何使用動態規劃和回頭查看技巧來計算最長遞增子序列的長度,並提供了相關的測試案例和範例。本文還包括了詳細的演算法和程式碼示例,以及時間和空間複雜度的分析。
Thumbnail
探討如何使用DP動態規劃的方法來進行單字串接,包含了DP遞迴關係式、狀態定義、優化技巧和程式碼示例。同時分析了時間複雜度、空間複雜度和關鍵知識點。這是LeetCode的一個應用題,類似於Word Break I的延伸。
Thumbnail
探討如何使用DP動態規劃的方法來進行單字串接,包含了DP遞迴關係式、狀態定義、優化技巧和程式碼示例。同時分析了時間複雜度、空間複雜度和關鍵知識點。這是LeetCode的一個應用題,類似於Word Break I的延伸。
Thumbnail
這篇文章,會帶大家快速回顧DFS+回溯法框架(還沒看過或想複習的可以點連結進去)。 用DFS+回溯法框架,解開 直線排列Permutations 的全系列題目。 幫助讀者鞏固DFS+回溯法框架這個重要的知識點。 回顧 DFS+回溯法框架 白話的意思 # 列舉所有可能的情況,遞迴展開所有分
Thumbnail
這篇文章,會帶大家快速回顧DFS+回溯法框架(還沒看過或想複習的可以點連結進去)。 用DFS+回溯法框架,解開 直線排列Permutations 的全系列題目。 幫助讀者鞏固DFS+回溯法框架這個重要的知識點。 回顧 DFS+回溯法框架 白話的意思 # 列舉所有可能的情況,遞迴展開所有分
Thumbnail
這篇文章,會帶大家快速回顧DFS+回溯法框架(還沒看過或想複習的可以點連結進去)。 用DFS+回溯法框架,解開 組合數之和 Combination Sum 的全系列題目。 幫助讀者鞏固DFS+回溯法框架這個重要的知識點。 回顧 DFS+回溯法框架 白話的意思 # 列舉所以可能的情況,
Thumbnail
這篇文章,會帶大家快速回顧DFS+回溯法框架(還沒看過或想複習的可以點連結進去)。 用DFS+回溯法框架,解開 組合數之和 Combination Sum 的全系列題目。 幫助讀者鞏固DFS+回溯法框架這個重要的知識點。 回顧 DFS+回溯法框架 白話的意思 # 列舉所以可能的情況,
Thumbnail
這篇文章,會帶著大家複習以前學過的數列DP框架, 並且以費式數列、爬樓梯、骨牌拚接的應用與遞迴數列概念為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 數列DP與遞迴數列常見的形式 如果是遞迴數列,常常看到以函數型式表達
Thumbnail
這篇文章,會帶著大家複習以前學過的數列DP框架, 並且以費式數列、爬樓梯、骨牌拚接的應用與遞迴數列概念為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 數列DP與遞迴數列常見的形式 如果是遞迴數列,常常看到以函數型式表達
Thumbnail
這篇文章,會帶著大家複習以前學過的 區間DP框架, 並且以回文子字串、回文子序列的應用題與概念為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 回文字串的基本定義 s = s[::-1] 也就是說字串s的正序 和 逆序完全相同。 回文字串的基本結構 空字串"
Thumbnail
這篇文章,會帶著大家複習以前學過的 區間DP框架, 並且以回文子字串、回文子序列的應用題與概念為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 回文字串的基本定義 s = s[::-1] 也就是說字串s的正序 和 逆序完全相同。 回文字串的基本結構 空字串"
Thumbnail
分享在網路上看到的陣列題目。通常 for...of 的 value 是陣列中的每個值,那如果我們在迭代中對陣列操作會發生什麼事? 題目來源:https://x.com/_jayphelps/status/1774640511158022335?s=20
Thumbnail
分享在網路上看到的陣列題目。通常 for...of 的 value 是陣列中的每個值,那如果我們在迭代中對陣列操作會發生什麼事? 題目來源:https://x.com/_jayphelps/status/1774640511158022335?s=20
Thumbnail
這篇文章,會帶著大家複習以前學過的遞回框架, 並且鏈結串列的概念與應用為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 遞回框架 尋找共通模式(common pattern),對應到演算法的General case 確立初始條件(initial conditio
Thumbnail
這篇文章,會帶著大家複習以前學過的遞回框架, 並且鏈結串列的概念與應用為核心, 貫穿一些相關聯的題目,透過框架複現來幫助讀者理解這個演算法框架。 遞回框架 尋找共通模式(common pattern),對應到演算法的General case 確立初始條件(initial conditio
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News