递归与分治,以汉诺塔为例

795 阅读4分钟

分治法

分治法是一种将规模较大的问题分解为规模较小的子问题,先求解子问题,然后将子问题的解合并得到原问题的解的算法思路

递归

递归是一种自然的思考方式,思路清晰,易于实现,但是递归算法的具体执行步骤难以理解,坏的递归会大幅度提高算法的复杂度,所以要慎用递归。

汉诺塔问题

现存在三根高度一样的柱子,分别为起始柱A,辅助柱B,目标柱C,起始柱A上放着若干从下往上依次减小的圆盘,一次只能移动一个圆盘,求将所有圆盘从起始柱A转移到目标柱C所需要的最少次数,要求圆盘只能放在比起大的圆盘之上或者空柱子上。

解题思路

不要考虑一步走完下一步怎么走,只考虑当前的一步

首先我们能想到,先将A上最小的一个移动到C上,但圆盘只能放在比其大的圆盘之上或者空柱上,C为最终目的,如果将最小圆盘直接移动到C,那么A剩下的比第一个大的圆盘想要移动到C,就必须把已经放到C上的圆盘取出来,会增加不必要的步数,不论是不是先将最小移动到C,这种思考方式都是不对的,所以要把A上的所有圆盘移动到C上应该先把最大的移动到C上面:

    1. 将A上的n - 1个圆盘放到B上
    1. 将A上的最大放到C上
    1. 将B上的n - 1个圆盘放到C上 其中第2步能1步完成,但是1、3需要继续对其任务进行进一步细分,于是我们将问题的求解很容易抽象成递归函数:
  • 输入:待转移的圆盘数目n, 起始圆盘 X ,辅助圆盘 Y ,目标圆盘 Z
  • 逻辑:
    • 如果要转移的圆盘数目n为1个,直接转移到目标盘上
    • 如果要转移的圆盘数目n为多个,此时应该分为3步:
      • 把X上n - 1移动到Y,X为起始盘, Z为辅助盘,Y为目标盘,移动 n - 1个
      • 把X上 1 个圆盘移动到Z,X为起始盘 Y为辅助盘,Z为目标盘,移动 1 个 (1步不需要辅助)
      • 把Y上 n - 1 个圆盘移动到Z上,Y为起始盘,X为辅助盘,Z为目标盘,移动n - 1个

是否遵守了规则

在移动过程中我们会比较关注有没有违反只能小的圆盘放在大的圆盘的上面这一规则,可以进行如下考虑:

  • 在同一次递归中的3步,每进行一步就代表前面的步骤完成了
  • 实际上的移动,只会在需要移动一个的时候完成,移动一个不需要借助辅助圆柱,直接从起始柱到目标柱 感觉解释了个寂寞,在移动一个的时候你会发现要么移动到空柱,要么移动到下边圆盘更大的柱,可以自己用3个圆盘试一下

时间复杂度

用Xmind稍微画一下

不难看出递归函数的调用次数为 1 + 2^0 * 3 + 2^1 * 3 + ... + 2^(n - 2) * 3

等比数列求和+1, 变成 1 + 2^(n - 1) * 3

所以时间复杂度为 O(2^n)

JAVA代码

代码会输出每一次的移动时各个圆柱的具体情况

  • 测试类
import java.util.ArrayList;

public class HanoiTest {

	public static void main(String[] args) {
		ArrayList<Integer> A_nums = new ArrayList<>();
		A_nums.add(5);
		A_nums.add(4);
		A_nums.add(3);
		A_nums.add(2);
		A_nums.add(1);
		Tower A = new Tower("A", A_nums);
		Tower B = new Tower("B");
		Tower C = new Tower("C");
		Hanoi.move(A, B, C);
	}
}
  • 汉诺塔类
import java.util.ArrayList;

public class Tower{
	String name;
	ArrayList<Integer> nums;
	
	public Tower(String name) {
		this.name = name;
		this.nums = new ArrayList<Integer>();	
	}
	
	public Tower(String name, ArrayList<Integer> nums) {
		this.name = name;
		this.nums = new ArrayList<Integer>();	
		for(int num : nums) {
			this.nums.add(num);	
		}
	}
	
	public String toString() {
		return " " + name + ": " + nums.toString();
	}
	
	public boolean add(int num) {
		return this.nums.add(num);
	}
	
	public int remove() {
		return this.nums.remove(nums.size()-1);
	}
	
	public int size() {
		return nums.size();
	}	
}
  • 题解类
public class Hanoi {
	
	private static int step;
	
	private static Tower first;
	private static Tower second;
	private static Tower third;
	
	public static void move(Tower a, Tower b, Tower c) {
		step = 0;
		first = a;
		second = b;
		third = c;
		
		System.out.println(first.toString() + second.toString() + third.toString());
		moveSomeFromByTo(a.size(),a,b,c);
		step = 0;
	}
	
	public static void moveSomeFromByTo(int n,Tower org,Tower support, Tower destination) {
		
		//直接放  打印该步操作
		if(n == 1) {
			step++;
			System.out.println("step: "+ step + "  move " + org.nums.get(org.nums.size()-1) + " from " + org.name +" to " + destination.name);
			destination.add(org.remove());
			System.out.println(first.toString() + second.toString() + third.toString());
		}else {
			moveSomeFromByTo(n-1,org,destination,support);
			moveSomeFromByTo(1,org,support,destination);//可直接操作的一步
			moveSomeFromByTo(n-1,support,org,destination);
		}
	}

}

LeetCode题解

class Solution {
    public void hanota(List<Integer> A, List<Integer> B, List<Integer> C) {
        traceback(A.size(),A,B,C);
    }

    public void traceback(Integer num, List<Integer> A,List<Integer> B, List<Integer> C){
        if(num == 1){
            int tmp = A.remove(A.size() - 1);
            C.add(tmp);
        }else{
            traceback(num - 1,A,C,B);
            traceback(1,A,B,C);
            traceback(num-1,B,A,C);
        }
    }
}