实用指南:方法-递归/算法-递归

递归,直观的解释就是在方法中调用自己。

在目标明确的方法中(如计算y=3x+1)看上去不太合理,以至于有些同学可能不太理解为什么要自己调用自己,我们来聊一聊这个话题。

以求最大公约数(gcd)为例,如果用while写,大概是下面的结构:

    //用while循环求最大公约数
    int max_gcd = a;
    while(b!= 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    cout<<"最大公约数为:"<

(这里就不解释辗转相除的原理了)

注意这个算法中a、b一直在不停的变化,直到满足了某个条件(b==0)循环停止,输出结果(当前最新的a)。

对于这种参数不停变化、有明确结束条件的算法,我们可以用递归来实现:

int gcd(int a, int b) {
    if(b == 0)
        return a;
    else
        return gcd(b, a%b);
}

第一次看到递归的做法,可能有的同学是难以理解的:为什么它可以通过调用自己来实现类似循环的效果。

其实最好的理解方式,是带入一个a、b,然后模拟这个递归方法调用的全过程,来帮助你理解递归的工作原理。

这里带入a=16,b=12

turn 1:a=16  b=12

if(b==0) 不成立

那么把返回结果交给 gcd(12,4),然后开始第二轮调用(第二次调用gcd)

turn 2:a=12,b=4

if(b==0) 不成立

那么把返回结果交给 gcd(4,0),然后开始第三轮调用(第三次调用gcd)

turn 3:a=4,b=0

if(b==0) 成立

第三次调用gcd的结果是4,向第二轮调用返回a的值4

turn2: 回到了第二轮调用 gcd(4,0)的位置,这个值是4,这个值返回到第一轮调用gcd的位置

turn1:回到了第一轮调用gcd(12,4)的位置,这个值是4,意味着最开始gcd(16,12)的值返回4,返回结果4

可能有同学想说对于递归使用gcd是否多此一举,明明循环可以做到的事情,为什么要用递归来做呢,又复杂又不便于理解。

确实,对例如算阶乘、斐波那契数列等递归并不是必须的,但是递归思想你必须得理解,因为有些特定的场合是非递归不可。

场景1:深度优先搜索

广搜和深搜都是先定义好方向的顺序,然后开始搜索。与广度优先搜索(按方向螺旋扩散)不同,深度优先搜索思想按一个方向走到黑,然后再退回来尝试其他方向。

用递归非常契合深度优先搜索的感念,通过递归散开每一个层级,每一个层级都有4个搜索方向,但是因为代码已经规定好了搜索的方向顺序,所以第一个方向会被一直调用,直到这个方向无法往下走,转到当前层级的下一个方向。

//迷宫深度优先搜索算法
int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};
bool getRoad=0;
//x,y表示当前点的坐标,n,m表示迷宫的行列数
void dfs(int x, int y, int n, int m) {
    visited[x][y] = 1;
    if(x == n-1 && y == m-1) {
        //cout<<"找到路径!"<= 0 && nx < n && ny >= 0 && ny < m && map[nx][ny] == 0 && visited[nx][ny] == 0) {
            return dfs(nx, ny, n, m);
        }
    }
    //第一个点的4个方向都探索完了
    //本例不需要返回值,所以这里无需处理
}

在本例中,可以继续探索的前提是当前点没有超过边界、当前点可以走、没有被走过,直到所有的点都探索完毕就没有点可以继续走,程序自然退出结束,程序也可以继续优化为当已经有通路了就返回不再探索(深搜是搜不出来最佳路径的,只能搜出来通路)

深度优先搜索并不是不能用非递归来实现,但是这样会让代码更加复杂(使用stack),所以递归被视为深度优先搜索的最佳方案。

场景2:非确定数量输入的全排列

输入:

3

a b c

第一行的3表示有3个字符,第二行表示的是这3个字符

要求输出a b c 的不重复的全排列( abc、acb、bac、bca、cab、cba)

本地最大的挑战在于数据字符数量的不确定性,如果是固定3个数并不难, 写一个3重循环即可。但是当数量不确定时,我怎么知道要写几重循环呢?如果有9种情况,是不是1-9要做9次判断,那代码要多长?

本题通过递归的特性,巧妙的解决这个问题:

int a;
char c[9];
char out[9];
void permute(int k) {
    if (k == a) {
        for (int i = 0; i < a; i++) {
            cout << out[i];
        }
        cout << endl;
        return;
    }
    /*
     * 如果当前字符已经被标记为已使用(即c[i]等于'\0'),则通过continue跳过该循环迭代。
     * 否则,将当前字符c[i]赋值给输出数组out[k],表示当前位置k使用了字符c[i]。
        将c[i]标记为已使用,即c[i] = '\0'。
        递归调用permute(k + 1),尝试下一个位置的字符排列。
        递归调用返回后,恢复c[i]的值,以便尝试其他字符的排列(即c[i] = out[k])。/
     */
    for (int i = 0; i < a; i++) {
        if (c[i] == '\0') {
            continue;
        }
        out[k] = c[i]; //使用当前字符
        c[i] = '\0'; //标记当前字符已使用
        permute(k + 1); //递归调用,下一轮
        c[i] = out[k];//恢复当前字符的值
    }
}
int main() {
    cin >> a;
    for (int i = 0; i < a; i++) {
        cin >> c[i];
    }
    permute(0);
    return 0;
}

要完全理解递归代码中循环的意义,需要带入数值模拟。

本例在递归的循环中,通过k控制递归的深度(也就是字符的数量),通过循环i控制备选输出的长度。本例稍微修改一下,可以实现如“输入5个字符,取3个做全排列(也就是A(5,3)个))”的全排列输出。

对于循环次数都不确定的问题,通过两个因子去控制递归的总次数,从而实现非递归代码难以实现的复杂问题,是递归的优势之一。

总结:

在小部分场合,还真是非递归不可,比如求a的k次方对p的余数(快速幂,当k很大时),掌握递归的算法思想在信息学学习中是非常必要的。

posted @ 2025-11-08 22:07  clnchanpin  阅读(8)  评论(0)    收藏  举报