递归-回溯算法宇宙观(简单理解迷宫、皇后问题)

最近在刷题的时候,对于一些使用递归的算法都不太理解,经常没有思路。经过刷了几个题,终于把抽象的递归形象化了,记录在此,帮助记忆。

很多递归的教程都是从斐波那契数列开始,但我想从一个更简单的题目开始切入。

题目为:给定一个整数n,请输出倒序再整数输出n0,0n,以空格分离。
示例,当n=1时,输出1, 0, 0, 1
当n=3时,输出3, 2, 1, 0, 0, 1, 2, 3

题目很简单,一般思路就是从n开始,循环递减打印,如下

// 打印数字
public static void printNum(int n) {
    for (int i = n; i >=0; i--) {
        System.out.print(i + " ");
    }
    for (int i =0; i <= n; i++) {
        System.out.println(i + " ");
    }
}

这么写完全正确,那如果用递归来写怎么实现呢?下面这样写对吗?

public class MyTest{

    public static void main(String[] args) {
        printNum(3);
    }

    // 打印数字
    public static void printNum(int n) {    // 第8行
        // n小于0,不打印了,返回
        if (n < 0) {
           return;
       }
       System.out.print(n + " "); // 第13行
       n--;
       printNum(n); // 第15行
       System.out.println(n + " "); // 第16行
    }
}

上面代码的控制台输出是,显然代码是错的:

3 2 1 0 -1 0 1 2 

我们来看看为什么会导致这个错误,代码第15行之前这些这些代码虽然是递归,但是按人类正常顺序思路来看,很容易理解。在第8行printNum这个方法中,当n=5时,先打印数字5,第14行减1,第15行再递归打印数字4,以此类推,直到小于0则不打印了,此时栈中的各个方法应该执行第16行。第15行以后就不好理解了,为什么打印了-1呢?

递归的宇宙

为了便于理解递归,我们定义一个方法宇宙的概念。我们知道,每个方法调用的时候都用各自的栈空间,方法返回则栈销毁。如此,我们不就可以将一个方法看成一个宇宙,方法内的代码在这个宇宙中运行,方法返回宇宙消失。

递归递归,顾名思义,分为递和归两个过程

历史宇宙

如此,再来理解上面的代码。假设当n=3,起初,代码在第8行和第15行之间一直递归,我们把递归看成历史宇宙,代码运行逻辑:(递)n=3历史宇宙里打印了数字3 => n=2历史宇宙里打印数字2 => n=1历史宇宙里打印了数字1 => n=0历史宇宙打印了数字0 => n=-1的宇宙直接返回了 => (printNum执行完,归) => n=0历史宇宙第十四行执行了n--,故第十六行打印-1 => n=1宇宙同理,打印0。

找到原因,这时我们知道怎么改了,因为第14行代码将n--了,所以第15行历史宇宙回归后,我们需要加回来,即n++。代码改为如下:如下添加了第16行。

public class MyTest{

    public static void main(String[] args) {
        printNum(3);
    }

    // 打印数字
    public static void printNum(int n) { // 第8行
        // n小于0,不打印了,返回
        if (n < 0) {
           return;
       }
       System.out.print(n + " "); // 第13行
       n--;
       printNum(n); // 第15行
       n++; // 第16行
       System.out.print(n + " "); // 第17行 
    }
}

输出正如我们所料,如下:

3 2 1 0 0 1 2 3

得出结论,整个方法是一个宇宙,第15行printNum方法之前是递的过程,通过第15行printNum方法进到历史宇宙最深处,第15行后是在每个历史宇宙做的事,事情做完回归。如下图:

20191108171025.png

如此,甚至能在0号历史宇宙做的手脚呢?如下,在0号历史宇宙打印一句话。

public class MyTest{

    public static void main(String[] args) {
        printNum(3);
    }

    // 打印数字
    public static void printNum(int n) { // 第8行
        // n小于0,不打印了,返回
        if (n < 0) {
           return;
       }
       System.out.print(n + " "); // 第13行
       n--;
       printNum(n); // 第15行
       n++; // 第16行
       System.out.print(n + " "); // 第17行 
       if (n == 0) {
           System.out.print("++我在0历史宇宙,我要回归啦++ ");
       }
    }
}

如上代码,输出如下:

3 2 1 0 0 ++我在0历史宇宙,我要回归啦++ 1 2 3

平行宇宙

除了历史宇宙,我们还能通过循环,进入平行宇宙。假如我们要在1号宇宙的时候,再分化出一个平行宇宙,怎么做呢?代码如下:

public class MyTest{

    public static void main(String[] args) {
        printNum(3);
    }

    // 打印数字
    public static void printNum(int n) { // 第8行
        // n小于0,不打印了,返回
        if (n < 0) {
           return;
       }
       System.out.print(n + " "); // 第13行
       n--;
       // 1号宇宙分化出一个平行宇宙,再进入历史宇宙
       if (n == 1) {
            for (int i =0; i < 2; i++) {
                printNum(n);
            }
       }else {
           printNum(n);
        // 其他号宇宙进入历史宇宙
       }
       n++; 
       System.out.print(n + " "); 
        if (n == 0) {
            System.out.print("++我在0历史宇宙,我要回归啦++ ");
        }
    }
}

输出如下,应该能看懂吧。
[图片上传失败...(image-3091f6-1573215792367)]

回溯算法

回溯算法可谓算法编程中的暴力美学啊,其本质就是通过DFS(深度优先遍历)枚举了问题中的所有情况,然后记录满足你的问题的解。回溯算法通过一步步试错,这一次走这条路,如果错了则倒退,走另外一条路,遍历完所有可能。为什么能回溯呢?不正好是递归里面的穿梭到历史宇宙再回来的情况吗。

回溯算法之所以能回溯撤退的原因,是因为递归能自动回退

迷宫问题

以一个9*8的长方阵表示迷宫,1表示迷宫中的通路、0表示障碍。入口为左上角,出口为右下角。设计程序,求出从入口到出口的所有通路。
{1, 1, 0, 1, 1, 1, 0, 1},
{1, 1, 0, 1, 1, 1, 0, 1},
{1, 1, 0, 1, 0, 0, 1, 0},
{1, 0, 0, 0, 1, 1, 0, 1},
{1, 1, 1, 0, 1, 1, 1, 1},
{1, 0, 1, 1, 1, 0, 1, 0},
{1, 0, 0, 0, 0, 1, 1, 0},
{0, 0, 1, 1, 1, 0, 1, 0},
{0, 0, 1, 1, 1, 1, 1, 1}

题目要求找出所有的通路,很明显,需要遍历整个迷宫,适合使用回溯算法。我们的思路是,从左上角开始,在每个宇宙里,都往上下左右四个方向走,首先一直往右走,每往前走一步时,先查查右边能否走通,走不通则走另外三个方向,走过的路则标记为数字6。代码如下,注意看注释。

public class MazeTest{

    //  迷宫,9行8列
    private static int ROW = 9;
    private static int COLUMN = 8;
    private static int[][] maze = {
        {1, 1, 0, 1, 1, 1, 0, 1},
        {1, 1, 0, 1, 1, 1, 0, 1},
        {1, 1, 0, 1, 0, 0, 1, 0},
        {1, 0, 0, 0, 1, 1, 0, 1},
        {1, 1, 1, 0, 1, 1, 1, 1},
        {1, 0, 1, 1, 1, 0, 1, 0},
        {1, 0, 0, 0, 0, 1, 1, 0},
        {0, 0, 1, 1, 1, 0, 1, 0},
        {0, 0, 1, 1, 1, 1, 1, 1}
    };


    public static void main(String[] args) {
        
        // 从左上角开始走迷宫
        walk(0, 0);
    }

    public static void walk(int i , int j){

        // 如果到达右下角,说明该路线可以走通
        if (i >= ROW-1 && j >= COLUMN-1) {
            //  打印当前迷宫痕迹
            System.out.println("找到一种解法如下:====");
            for (int r = 0; r < ROW; r++) {
                for (int c = 0; c < COLUMN; c++) {
                    System.out.print(maze[r][c]);
                }
                System.out.println();
            }
            return;
        } // ===打印迷宫结束

        // 向右走,先查查是否可以站在这个i, j+1 这个点上
        if (canStand(i, j+1)) {
            // 可以站在这个点上,站上去(标记已走过)
            maze[i][j] = 6;
            walk(i, j+1); // 往右走,进入下一个历史宇宙
            // 在最右边历史宇宙撞墙了,这个宇宙上面把这个点改为6了,需要改回来,否则篡改历史,会引发未知的蝴蝶效应
            maze[i][j] = 1;
        }

        // 在最右历史宇宙中走不下去,向下走
        if (canStand(i+1, j)) {
            maze[i][j] = 6;
            walk(i+1, j);
            maze[i][j] = 1;
        }

        // 向左走
        if (canStand(i, j-1)) {
            maze[i][j] = 6;
            walk(i, j-1);
            maze[i][j] = 1;
        }

        // 向上走
        if (canStand(i-1, j)) {
            maze[i][j] = 6;
            walk(i - 1, j);
            maze[i][j] = 1;
        }

    }

    private static boolean canStand(int i, int j) {
        //System.out.print("[" + i + "," + j + "]=>");
        //  走到边界了
        if (i >= ROW || j >= COLUMN || i < 0 || j < 0) {
            return false;
        }

        // 遇到障碍了
        if (maze[i][j] == 0) {
            return false;
        }

        // 走过了
        if (maze[i][j] == 6) {
            return false;
        }
        return true;
    }
}

上面代码8种走法,输出结果为:

找到一种解法如下:====
66011101
16011101
66010010
60001101
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
66011101
16011101
66010010
60006601
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
66011101
66011101
61010010
60001101
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
66011101
66011101
61010010
60006601
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
61011101
66011101
66010010
60001101
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
61011101
66011101
66010010
60006601
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
61011101
61011101
61010010
60001101
66606661
10666060
10000160
00111060
00111161
找到一种解法如下:====
61011101
61011101
61010010
60006601
66606661
10666060
10000160
00111060
00111161

如上,在入口(0, 0)处,可以往右或者往下,我们先一直往右走,碰到墙时,在最右的历史宇宙中往下走,再往右走。最后回溯到最左的入口历史宇宙时,会尝试往下走的做法。故可以完成遍历。

八皇后问题

待续

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容