算法02-深入解析递归与汉罗塔算法

算法02-递归

一、定义

1、思想

程序调用自身的编程技巧称为递归(recursion)。

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的能力在于用有限的语句来定义对象的无限集合。

一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

递归的经典应用:斐波那契数列、汉诺塔算法。

2、调用一次

递归只调用一次时,调用位置前面的代码是正循环,调用位置后面的代码是反循环:

public static int reset(int i) {
    //结束条件
    if (i <= 0) {
        return i;
    }
    //递归开始前,正序循环
    String i1 = "正序: " + i;
    System.out.println(i1);
    //递归开始
    int m = reset(--i);
    //递归开始后,逆序循环
    String i2 = "逆序: " + i;
    System.out.println(i2);
    return m;
}

reset(3);

//打印结果
//正序: 3
//正序: 2
//正序: 1
//逆序: 0
//逆序: 1
//逆序: 2

3、调用多次

这里以调用两次为例,来说明:

/**
 * 树的递归遍历:两次递归
 *
 * @param root 根节点
 * @param type 遍历方式:0代表前序,1代表中序,2代表后序
 */
public void ergodicRecursion(Node<String> root, int type) {
    //结束条件
    if (root == null) {
        return;
    }
    //第一次递归开始前,正序循环
    if (type == 0) {
        ToolShow.log("前序:" + root.data);
    }
    //第一次递归
    ergodicRecursion(root.left, type);

    //第一次递归开始前,逆序循环:下面所有的代码都属于第一次递归的逆序循环。由于下面还有一次递归,所以还需要将他们分成第二次递归前、中、后,来循环执行。
    //第二次递归开始前,正序循环
    if (type == 1) {
        ToolShow.log("中序:" + root.data);
    }
    //第二次递归
    ergodicRecursion(root.right, type);
    //第二次递归开始后,逆序循环
    if (type == 2) {
        ToolShow.log("后序:" + root.data);
    }
}

4、递归的转化

递归就是用栈实现的。

所以任何递归都可以转化为栈的循环。只需要掌握好出栈时机。

/**
 * 用栈实现树的遍历
 * @param curr 当前节点
 * @param type 遍历方式:0代表前序,1代表中序,2代表后序
 */
public void ergodicStack(Node<String> curr, int type) {
    //存放节点
    Stack<Node<String>> stack = new Stack<>();
    //记录已经添加过的节点
    List<Node<String>> nodeList = new ArrayList();
    stack.push(curr);
    nodeList.add(curr);
    while (curr != null) {
        //第一次递归之前:只执行一次
        if (type == 0 && !nodeList.contains(curr.left) && !nodeList.contains(curr.right)) {
            ToolShow.log("前序:" + curr.data);
        }
        //相当于第一次递归:已经添加过的节点不能重复添加
        if (curr.left != null && !nodeList.contains(curr.left)) {
            curr = curr.left;
            stack.push(curr);
            nodeList.add(curr);
        } else {
            //第二次递归之前:只执行一次
            if (type == 1 && !nodeList.contains(curr.right)) {
                ToolShow.log("中序:" + curr.data);
            }
            //相当于第二次递归
            if (curr.right != null && !nodeList.contains(curr.right)) {
                curr = curr.right;
                stack.push(curr);
                nodeList.add(curr);
                //两次递归结束
            } else {
                if (type == 2) {
                    ToolShow.log("后序:" + curr.data);
                }
                stack.pop();
                curr = stack.isEmpty() ? null : stack.lastElement();
            }
        }
    }
}

二、斐波那契数列

1、基本思想

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>2,n∈N*)。

2、代码实现

/**
 * @param n 代表下标
 */
public static int fibonacciSequence(int n) {
    if (n == 0 || n == 1) {
        return 1;
    }
    return fibonacciSequence(n - 2) + fibonacciSequence(n - 1);
}

三、汉诺塔算法

1、基本思想

汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

汉诺塔问题转化为数学问题:从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去, 期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数。

实现这个算法可以简单分为三个步骤:

  1. 先把A上面的n-1个盘子由A 移到 B;
  2. 把A最下面的第n个盘子由 A移到 C;
  3. 最后B上面的n-1个盘子由B 移到 C;

2、代码实现

 /**
 * @param n 汉诺塔层数
 * @param A 开始柱子
 * @param B 中间柱子
 * @param C 结束柱子
 */
public static void hanoi(int n, String A, String B, String C) {
    if (n == 1) {
        ToolShow.log("从" + A + "到" + C);
        return;
    }

    hanoi(n - 1, A, C, B);
    ToolShow.log("从" + A + "到" + C);
    hanoi(n - 1, B, A, C);
}

demo已上传gitee,需要的同学可以下载!

上一篇:算法01-蛮力法

下一篇:算法03-分治法

猜你喜欢

转载自blog.csdn.net/dshdhsd/article/details/79964053
今日推荐