63、递归算法:原理、应用与实例分析

递归算法:原理、应用与实例分析

1. 递归的基本概念

递归是一种强大的编程技术,可用于解决重复性问题。若一个问题能分解为一系列与整体问题相同的较小子问题,那么就可以使用递归方法来解决。不过,递归并非解决问题的必要手段,任何能用递归解决的问题,也可以通过迭代(使用循环)来解决。实际上,递归算法通常比迭代算法效率低,因为每次方法调用时,JVM 需要执行一些操作,如为参数和局部变量分配内存,存储方法结束后控制返回的程序位置地址,这些操作被称为开销,而循环则不需要这些开销。

但对于某些重复性问题,使用递归比迭代更容易解决。虽然迭代算法可能执行速度更快,但程序员可能能更快地设计出递归算法。

一般来说,递归方法的工作方式如下:
- 如果问题现在可以不使用递归解决,那么方法就直接解决并返回结果。
- 如果问题现在无法解决,那么方法将其简化为一个更小但相似的问题,并调用自身来解决这个更小的问题。

为了应用这种方法,首先要确定至少一个可以不使用递归解决问题的情况,这被称为基本情况(base case);其次,要确定在其他所有情况下使用递归解决问题的方法,这被称为递归情况(recursive case)。在递归情况下,必须始终将问题简化为原始问题的较小版本。通过每次递归调用减少问题规模,最终会达到基本情况,递归也会停止。

2. 递归在数学中的应用:阶乘计算

在数学中, n! 表示数字 n 的阶乘。非负整数的阶乘可以通过以下规则定义:
- 如果 n = 0 ,则 n! = 1
- 如果 n > 0 ,则 n! = 1 × 2 × 3 × ... × n

n! 替换为 factorial(n) ,规则可重写为:
- 如果 n = 0 ,则 factorial(n) = 1
- 如果 n > 0 ,则 factorial(n) = 1 × 2 × 3 × ... × n

当设计一个递归算法来计算任意数字的阶乘时,基本情况是 n = 0 ,即:

if (n == 0)
    factorial(n) = 1;

n > 0 时,这是递归情况,可表示为:

if (n > 0)
    factorial(n) = n × factorial(n - 1);

以下是用 Java 实现的阶乘递归方法:

private static int factorial(int n)
{
    if (n == 0)
        return 1;   // 基本情况
    else
        return n * factorial(n - 1);
}

以下是一个演示该方法的程序:

import javax.swing.JOptionPane;

/**
 * 这个程序演示了递归阶乘方法。
 */
public class FactorialDemo
{
    public static void main(String[] args)
    {
        String input;  // 用于保存用户输入
        int number;    // 用于保存一个数字

        // 从用户那里获取一个数字
        input = JOptionPane.showInputDialog("输入一个非负整数:");
        number = Integer.parseInt(input);

        // 显示该数字的阶乘
        JOptionPane.showMessageDialog(null,
                number + "! 是 " + factorial(number));

        System.exit(0);
    }

    /**
     * 阶乘方法使用递归计算其参数的阶乘,假设参数是非负数字。
     * @param n 用于计算的数字
     * @return n 的阶乘
     */
    private static int factorial(int n)
    {
        if (n == 0)
            return 1;   // 基本情况
        else
            return n * factorial(n - 1);
    }
}

在这个程序的示例运行中,当 factorial 方法的参数 n 为 4 时,由于 n 不等于 0, if 语句的 else 子句会执行 return n * factorial(n - 1); 语句。但在返回值确定之前,必须先确定 factorial(n - 1) 的值。 factorial 方法会递归调用,直到第 5 次调用时, n 参数将被设置为 0。

这也说明了为什么递归算法必须在每次递归调用时减少问题规模。最终,递归必须停止才能得到解决方案。如果每次递归调用处理的是问题的较小版本,那么递归调用就会朝着基本情况推进。基本情况不需要递归,因此它会停止递归调用链。通常,问题通过在每次递归调用时使一个或多个参数的值变小来简化。在阶乘方法中,参数 n 的值在每次递归调用时逐渐接近 0,当 n 达到 0 时,方法会返回一个值,而不再进行另一次递归调用。

3. 直接递归和间接递归

到目前为止讨论的示例展示了直接调用自身的递归方法,这被称为直接递归。在程序中也可能创建间接递归,即方法 A 调用方法 B,而方法 B 又调用方法 A,甚至可能涉及多个方法。例如,方法 A 调用方法 B,方法 B 调用方法 C,方法 C 再调用方法 A。

4. 递归方法的实例分析
4.1 数组元素范围求和

rangeSum 方法使用递归对数组元素的指定范围进行求和。该方法接受三个参数:包含要求和元素范围的整数数组、范围的起始元素索引和范围的结束元素索引。

以下是 rangeSum 方法的定义:

public static int rangeSum(int[] array, int start, int end)
{
    if (start > end)
        return 0;
    else
        return array[start] + rangeSum(array, start + 1, end);
}

该方法的基本情况是当 start 参数大于 end 参数时,方法返回 0。否则,方法返回 array[start] 加上递归调用 rangeSum 方法的返回值。在递归调用中,范围的起始元素索引为 start + 1 ,本质上是“返回范围中第一个元素的值加上范围中其余元素的和”。

以下是演示该方法的程序:

/**
 * 这个程序演示了递归 rangeSum 方法。
 */
public class RangeSum
{
    public static void main(String[] args)
    {
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        System.out.print("元素 2 到 5 的和是 " + rangeSum(numbers, 2, 5));
    }

    /**
     * rangeSum 方法计算数组中指定范围元素的和。
     * @param start 指定起始元素
     * @param end 指定结束元素
     * @return 范围的和
     */
    public static int rangeSum(int[] array, int start, int end)
    {
        if (start > end)
            return 0;
        else
            return array[start] + rangeSum(array, start + 1, end);
    }
}

程序输出:

元素 2 到 5 的和是 18
4.2 绘制同心圆

Circles 小程序使用递归绘制同心圆。同心圆是大小不同、一个套一个且具有共同中心点的圆。以下是小程序的代码:

import javax.swing.*;
import java.awt.*;

/**
 * 这个小程序使用递归方法绘制同心圆。
 */
public class Circles extends JApplet
{
    /**
     * init 方法
     */
    public void init()
    {
        getContentPane().setBackground(Color.white);
    }

    /**
     * paint 方法
     * @param g 小程序的 Graphics 对象
     */
    public void paint(Graphics g)
    {
        // 绘制 10 个同心圆。最外层圆的包围矩形应位于 (5, 5),宽 300 像素,高 300 像素。
        drawCircles(g, 10, 5, 300);
    }

    /**
     * drawCircles 方法绘制同心圆。
     * @param g Graphics 对象
     * @param n 要绘制的圆的数量
     * @param topXY 最外层圆的包围矩形的左上角坐标
     * @param size 最外层圆的包围矩形的宽度和高度
     */
    private void drawCircles(Graphics g, int n, int topXY, int size)
    { 
        if (n > 0)
        {
            g.drawOval(topXY, topXY, size, size);
            drawCircles(g, n - 1, topXY + 15, size - 30);
        }
    }
}

drawCircles 方法从 paint 方法调用,使用递归绘制同心圆。 n 参数表示要绘制的圆的数量,当 n 为 0 时,方法达到基本情况。否则,它调用 g 对象的 drawOval 方法绘制一个圆。 topXY 参数是包围矩形的左上角坐标, size 参数是包围矩形的宽度和高度。绘制完一个圆后, drawCircles 方法会递归调用,参数值会调整为绘制下一个圆。

4.3 斐波那契数列计算

斐波那契数列是一个著名的数学序列,在该序列中,除前两个数字外,每个数字都是前两个数字的和。斐波那契数列可以定义为:
- 如果 n = 0 ,则 Fib(n) = 0
- 如果 n = 1 ,则 Fib(n) = 1
- 如果 n > 2 ,则 Fib(n) = Fib(n - 1) + Fib(n - 2)

以下是计算斐波那契数列中第 n 个数字的递归 Java 方法:

public static int fib(int n)
{
    if (n == 0)
        return 0;
    else if (n == 1)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

该方法实际上有两个基本情况:当 n 等于 0 和当 n 等于 1 时,在这两种情况下,方法都会返回一个值,而不进行递归调用。以下是演示该方法的程序,它显示斐波那契数列的前 10 个数字:

/**
 * 这个程序演示了递归 fib 方法。
 */
public class FibNumbers
{
    public static void main(String[] args)
    {
        System.out.println("斐波那契数列的前 10 个数字是:");

        for (int i = 0; i < 10; i++)
            System.out.print(fib(i) + " ");

        System.out.println();
    }

    /**
     * fib 方法计算斐波那契数列中的第 n 个数字。
     * @param n 要计算的第 n 个数字
     * @return 第 n 个数字
     */
    public static int fib(int n)
    {
        if (n == 0)
            return 0;
        else if (n == 1)
            return 1;
        else
            return fib(n - 1) + fib(n - 2);
    }
}

程序输出:

斐波那契数列的前 10 个数字是:
0 1 1 2 3 5 8 13 21 34
4.4 最大公约数计算

计算两个正整数 x y 的最大公约数(GCD)也可以使用递归方法。 x y 的 GCD 定义如下:
- 如果 y 能整除 x ,则 gcd(x, y) = y
- 否则, gcd(x, y) = gcd(y, x % y)

以下是计算 GCD 的递归 Java 方法:

public static int gcd(int x, int y)
{
    if (x % y == 0)
        return y;
    else
        return gcd(y, x % y);
}

以下是演示该方法的程序:

import java.util.Scanner;

/**
 * 这个程序演示了递归 gcd 方法。
 */
public class GCDdemo
{
    public static void main(String[] args)
    {
        int num1, num2; // 用于计算 GCD 的两个数字

        // 创建一个 Scanner 对象用于键盘输入
        Scanner keyboard = new Scanner(System.in);

        // 从用户那里获取第一个数字
        System.out.print("输入一个整数: ");
        num1 = keyboard.nextInt();

        // 从用户那里获取第二个数字
        System.out.print("输入另一个整数: ");
        num2 = keyboard.nextInt();

        // 显示 GCD
        System.out.println("这两个数字的最大公约数是 " + gcd(num1, num2));
    }

    /**
     * gcd 方法计算传入的 x 和 y 参数的最大公约数。
     * @param x 一个数字
     * @param y 另一个数字
     * @return x 和 y 的最大公约数
     */
    public static int gcd(int x, int y)
    {
        if (x % y == 0)
            return y;
        else
            return gcd(y, x % y);
    }
}

程序输出示例:

输入一个整数: 49
输入另一个整数: 28
这两个数字的最大公约数是 7
递归算法的流程图示例
graph TD;
    A[开始] --> B{问题能否直接解决?};
    B -- 是 --> C[解决问题并返回结果];
    B -- 否 --> D[将问题简化为更小问题];
    D --> E[调用自身解决更小问题];
    E --> B;
    C --> F[结束];

通过以上这些实例可以看出,递归在解决各种不同类型的问题中都有广泛的应用,并且能够以简洁的方式实现复杂的逻辑。但在使用递归时,需要注意控制递归的终止条件,避免出现无限递归的情况。

5. 递归二分查找方法

二分查找算法也可以使用递归实现。递归二分查找算法比迭代版本更优雅、更易于理解,它也是将问题反复分解为更小部分直至解决的一个很好的例子。

递归二分查找算法的步骤如下:
- 如果 array[middle] 等于搜索值,则找到该值。
- 如果 array[middle] 小于搜索值,则在数组的上半部分进行二分查找。
- 如果 array[middle] 大于搜索值,则在数组的下半部分进行二分查找。

以下是实现该算法的 Java 方法:

public static int binarySearch(int[] array, int first, int last, int value)
{
    int middle;     // 搜索的中点
    // 测试值未找到的基本情况
    if (first > last)
        return -1;
    // 计算中间位置
    middle = (first + last) / 2;
    // 搜索值
    if (array[middle] == value)
        return middle;
    else if (array[middle] < value)
        return binarySearch(array, middle + 1, last, value);
    else
        return binarySearch(array, first, middle - 1, value);
}

该方法的参数说明如下:
| 参数 | 说明 |
| ---- | ---- |
| array | 要搜索的数组 |
| first | 搜索范围的第一个元素的下标 |
| last | 搜索范围的最后一个元素的下标 |
| value | 要搜索的值 |

该方法与迭代版本一样,如果找到值,则返回该值的下标;如果未找到,则返回 -1。以下是演示该方法的程序:

import java.util.Scanner;

/**
 * 这个程序演示了递归二分查找方法。
 */
public class RecursiveBinarySearch
{
    public static void main(String [] args)
    {
        int searchValue;   // 要搜索的值
        int result;        // 搜索结果
        String input;      // 输入的一行
        char again;        // 用于保存单个字符

        // 以下数组中的值按升序排序
        int numbers[] = {101, 142, 147, 189, 199, 207, 222,
                         234, 289, 296, 310, 319, 388, 394,
                         417, 429, 447, 521, 536, 600};

        // 创建一个 Scanner 对象用于键盘输入
        Scanner keyboard = new Scanner(System.in);

        do
        {
            // 获取要搜索的值
            System.out.print("输入一个要搜索的值: ");
            searchValue = keyboard.nextInt();

            // 搜索该值
            result = binarySearch(numbers, 0, (numbers.length - 1), searchValue);

            // 显示结果
            if (result == -1)
            {
                System.out.println(searchValue + " 未找到。");
            }
            else
            {
                System.out.println(searchValue + " 在元素 " + result + " 处找到。");
            }

            // 用户是否要再次搜索?
            System.out.print("是否要再次搜索? (Y 或 N): ");
            // 消耗剩余的换行符
            keyboard.nextLine();
            // 读取一行输入
            input = keyboard.nextLine();

        } while (input.charAt(0) == 'y' || input.charAt(0) == 'Y');
    }

    /**
     * 二分查找方法在整数数组上执行二分查找。
     * @param array 要搜索的数组
     * @param first 搜索范围的第一个元素
     * @param last 搜索范围的最后一个元素
     * @param value 要搜索的值
     * @return 如果找到值,则返回其下标;否则返回 -1
     */
    public static int binarySearch(int[] array, int first, int last, int value)
    {
        int middle;     // 搜索的中点

        // 测试值未找到的基本情况
        if (first > last)
            return -1;

        // 计算中间位置
        middle = (first + last) / 2;

        // 搜索值
        if (array[middle] == value)
            return middle;
        else if (array[middle] < value)
            return binarySearch(array, middle + 1, last, value);
        else
            return binarySearch(array, first, middle - 1, value);
    }
}

程序输出示例:

输入一个要搜索的值: 289
289 在元素 8 处找到。
是否要再次搜索? (Y 或 N): y
输入一个要搜索的值: 388
388 在元素 12 处找到。
是否要再次搜索? (Y 或 N): y
输入一个要搜索的值: 101
101 在元素 0 处找到。
是否要再次搜索? (Y 或 N): y
输入一个要搜索的值: 999
999 未找到。
是否要再次搜索? (Y 或 N): n
6. 汉诺塔问题

汉诺塔是一个数学游戏,常用于说明递归的强大之处。该游戏使用三个柱子和一组中间有孔的圆盘,圆盘按大小顺序堆叠在其中一个柱子上,最大的圆盘在底部。

游戏规则如下:
- 每次只能移动一个圆盘。
- 圆盘不能放在比它小的圆盘上面。
- 除移动时外,所有圆盘都必须放在柱子上。

游戏的目标是将所有圆盘从第一个柱子移动到第三个柱子,中间的柱子可以作为临时存放处。

对于不同数量的圆盘,解决方案如下:
- 如果只有一个圆盘,解决方案很简单:将圆盘从柱子 1 移动到柱子 3。
- 如果有两个圆盘,需要三步:
1. 将圆盘 1 移动到柱子 2。
2. 将圆盘 2 移动到柱子 3。
3. 将圆盘 1 移动到柱子 3。

随着圆盘数量的增加,移动的复杂度也会增加。移动三个圆盘需要七步。

整体解决方案可以描述为:使用柱子 2 作为临时柱子,将 n 个圆盘从柱子 1 移动到柱子 3。

以下是模拟该游戏解决方案的递归算法:

graph TD;
    A[开始] --> B{是否还有圆盘要移动?};
    B -- 是 --> C[将 n - 1 个圆盘从 A 移动到 B,使用 C 作为临时柱子];
    C --> D[将剩余的圆盘从 A 移动到 C];
    D --> E[将 n - 1 个圆盘从 B 移动到 C,使用 A 作为临时柱子];
    E --> B;
    B -- 否 --> F[结束];

以下是实现该算法的 Java 方法:

private void moveDiscs(int num, int fromPeg, int toPeg, int tempPeg)
{
    if (num > 0)
    {
        moveDiscs(num - 1, fromPeg, tempPeg, toPeg);
        System.out.println("将一个圆盘从柱子 " + fromPeg + " 移动到柱子 " + toPeg);
        moveDiscs(num - 1, tempPeg, toPeg, fromPeg);
    }
}

该方法的参数说明如下:
| 参数 | 说明 |
| ---- | ---- |
| num | 要移动的圆盘数量 |
| fromPeg | 要从哪个柱子移动圆盘 |
| toPeg | 要将圆盘移动到哪个柱子 |
| tempPeg | 作为临时柱子使用的柱子 |

如果 num 大于 0,则表示还有圆盘要移动。第一个递归调用 moveDiscs(num - 1, fromPeg, tempPeg, toPeg); 是将除最后一个圆盘外的所有圆盘从 fromPeg 移动到 tempPeg ,使用 toPeg 作为临时柱子。然后显示一条消息,表示将一个圆盘从 fromPeg 移动到 toPeg 。最后,另一个递归调用 moveDiscs(num - 1, tempPeg, toPeg, fromPeg); 是将除最后一个圆盘外的所有圆盘从 tempPeg 移动到 toPeg ,使用 fromPeg 作为临时柱子。

以下是使用该方法的类:

/**
 * 这个类显示汉诺塔游戏的解决方案。
 */
public class Hanoi
{
    private int numDiscs;   // 圆盘数量

    /**
     * 构造函数。
     * @param n 要使用的圆盘数量
     */
    public Hanoi(int n)
    {
        // 分配圆盘数量
        numDiscs = n;

        // 将所有圆盘从柱子 1 移动到柱子 3,使用柱子 2 作为临时存储位置
        moveDiscs(numDiscs, 1, 3, 2);
    }

    /**
     * moveDiscs 方法显示圆盘移动。
     * @param num 要移动的圆盘数量
     * @param fromPeg 要从哪个柱子移动
     * @param toPeg 要移动到哪个柱子
     * @param tempPeg 临时柱子
     */
    private void moveDiscs(int num, int fromPeg, int toPeg, int tempPeg)
    {
        if (num > 0)
        {
            moveDiscs(num - 1, fromPeg, tempPeg, toPeg);
            System.out.println("将一个圆盘从柱子 " + fromPeg + " 移动到柱子 " + toPeg);
            moveDiscs(num - 1, tempPeg, toPeg, fromPeg);
        }
    }
}

以下是演示该类的程序,它显示移动三个圆盘的说明:

/**
 * 这个类演示了 Hanoi 类,它显示解决汉诺塔游戏所需的步骤。
 */
public class HanoiDemo
{
    static public void main(String[] args)
    {
        Hanoi towersOfHanoi = new Hanoi(3);
    }
}

总结

递归是一种强大的编程技术,在解决各种问题中都有广泛的应用。它通过将问题分解为更小的子问题,利用基本情况和递归情况来,逐步解决问题。虽然递归算法通常比迭代算法效率低,但在某些情况下,它能让程序员更快速地设计出解决方案。

在使用递归时,需要注意以下几点:
- 明确基本情况,确保递归能够终止,避免无限递归。
- 每次递归调用时,要将问题规模缩小,使递归朝着基本情况推进。
- 注意递归的开销,对于大规模问题,可能需要考虑使用迭代方法。

通过阶乘计算、数组元素范围求和、同心圆绘制、斐波那契数列计算、最大公约数计算、二分查找和汉诺塔问题等实例,我们可以看到递归在不同场景下的应用。无论是数学计算还是数据搜索,递归都能以简洁的代码实现复杂的逻辑。希望通过本文的介绍,你能对递归有更深入的理解,并在实际编程中灵活运用递归技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值