11-算法-递归(阶乘、斐波拉契、八皇后)

314 阅读2分钟

递归的基本概念

有一句编程的至理名言是这样的:“要理解递归,首先要理解递归。”

递归是一种解决问题的方法,它从解决问题的各个小部分开始,直到解决最初的大问题。递归通常涉及函数调用自身。

递归的逻辑

// 递归的逻辑
function doSomething(something){
    
    // 递归就是做一些事情
    if(!something){
        
        // 当有一个明确的结果,或是操作处理完成的时候,就退出
        return;
    }
    
    // 没有处理完成的时候就继续执行后续操作
    thing(); // 这里代指一系列具体操作

    return doSomething(); // 继续做
}

用递归实现阶乘

// 阶乘
function factorial(n){

    // 递归方法必须有一个出口
    if(n === 1 || n === 0){ // 出口,满足什么条件就跳出递归
        return 1;
    }

    // 递归还需要一个入口
    return n * factorial(n-1); // 满足什么条件就进入递归
}

factorial(5);

/*
* 细分问题:
*   5的阶乘是5 * 4的阶乘
*   4的阶乘是4 * 3的阶乘
*   3的阶乘是3 * 2的阶乘
*   2的阶乘是2 * 1的阶乘
*   1的阶乘是1 * 0的阶乘
*   所以只需要知道1和0的阶乘,就知道后面所有的阶乘
*   1的阶乘是1,0的阶乘是1
*
* */

递归求斐波拉契数列

    /*
    * 斐波拉契 的第n项
    * 斐波拉契数列 每一项等于前两项之和
    * 1 1 2 3 5 8 13 21 ...
    *
    * */

    /*
    * 已知前两项的结果都为1
    * fb(1) = 1
    * fb(2) = 1
    * 假设 fb(n) 表示第n项的值 根据规律任意一项等于前两项的和 可得出下列公式
    * fb(n) = fb(n-2) + fb(n-1)
    * 如果递归没有记忆 就会消耗很多时间来计算结果
    *
    * */

    // 银老版本的递归求斐波拉契数列
    let list = {} // 创建一个对象,表示将之前算的结果,都存储到这个对象中
    
    function fb(n){ // 这个函数很有问题 相同的一项会被重复算两次 所以要做个表记录
        
        if(list[n]) return list[n] // 表示如果有已经算出来的结果 就直接查表
        
        if(n < 3) { 
            list[n] = 1 // 对象[属性名] = 属性值 表示在对象list{}中 将属性名为的1的属性值赋值为1 将属性名为的2的属性值赋值为1
            return list[n] // 结束条件 return的是n小于等于2的两个情况的值
        }
        
        list[n] = fb(n-2) + fb(n-1)
        
        return list[n] // 只要解决了最基本的前两项fb(1)和fb(2)的值 一层层往上就能得到第n项的值
    }
    // fb(40) = fb(39) + fb(38)
    // fb(39) = fb(38) + fb(37) // fb(38)被算了两次 很浪费时间

    console.log(fb(25))
// 万老版本的递归求斐波那契数列
/*
* 记忆函数:
*   接收一个函数,并且把这个函数改造成可以缓存数据的一个函数
*
* */
function memorize(fn){
    
    // 用一个闭包来存储缓存结果
    let cache = {} // 存储缓存数据的结构,通过key来查找value。key(键)是fn参数所组成的,value(值)就是在某个参数下的执行结果
    
    return function (){
        let key = arguments.length + Array.prototype.join.call(arguments,",")
        
        // console.log(key)
        
        if(key in cache){ // 如果存在值
            return cache[key]
        }else{
            return cache[key] = fn.apply(this,arguments)
        }
    }
}

function factorial(n){

    // 递归方法必须有一个出口
    if(n === 1 || n === 0){ // 出口,满足什么条件就跳出递归
        return 1
    }

    // 递归还需要一个入口
    return n * fb(n-1) // 满足什么条件就进入递归,注意这里的fb函数是下面的记忆函数所返回的内容
}

let fb = memorize(factorial) // 记忆函数的返回值赋值给fb

console.log(fb(10))
console.log(fb(11)) // 因为有记忆函数的缘故,fb(11)只需要在fb(10)的基础上再算1次即可

八皇后

image.png

八皇后规则:

每一行只能有一个皇后,每一列只能有一个皇后,左上到右下的对角线上只能有一个皇后,右上到左下的对角线上只能有一个皇后

棋盘模拟成二维数组,n * n的棋盘,这里是5 * 5 的棋盘,

let p = [

[00,01,02,03,04],

[10,11,12,13,14],

[20,21,22,23,24],

[30,31,32,33,34],

[40,41,42,43,44]

]

八皇后1.png 0 0中前一个数字,表示横行row 从0开始 到n-1

0 0中后一个数字,表示竖列col 从0开始 到n-1

转换成数学语言就是:

八皇后2.png 同一row横行不能继续放

同一col竖列不能继续放

从左上到右下:第row+col根对角线不能继续放

从右上到左下:第row-col+(n-1)根对角线不能继续放

为啥是加上n-1,因为一共就2n-1根,其中一半是row-col为负数即-(n-1),一半是row-col为正数n-1

所以最小值到最大值就是-(n-1)(n-1),这时两边同时加上(n-1)就每一根都变成正数,可以作为数组下标了

实现代码如下:

/*
    八皇后:
        每一行只能有一个皇后
        每一列只能有一个皇后
        左上到右下的对角线上只能有一个皇后
        右上到左下的对角线上只能有一个皇后
*/

// 把棋盘模拟成2维数组, n*n的棋盘,其中1表示可以下的地方
let p = [
    [1, 0, 0, 0, 0],// 行 从0 开始 到 n-1
    [0, 0, 1, 0, 0],
    [0, 0, 0, 0, 1],
    [0, 1, 0, 0, 0],
    [0, 0, 0, 1, 0]
    // 列 从0 开始 到 n-1
];

function calQueen(n){
    let queenPostion = []; // 存储最终的位置数据
    
    // 计算n皇后问题
    
    let leftTopToRightBottom = []; // 存储从左上到右下的对角线
    
    let rightTopToLeftBottom = []; // 存储从右上到左下对角线
    
    let columns = []; // 用于存储列的数组
    
    function tag(row, col, bool){ // 标记能否继续下棋子的函数
        
        // 当我们在第row 第col列下了一颗棋子的时候,然后我们来标记一下 具体的该行列或是对角线的可用情况
        
        leftTopToRightBottom[row + col] = bool; // 从左上到右下的对角线的情况
        
        rightTopToLeftBottom[row - col + n -1 ] = bool; // 从右上到左下的对角线的情况
        
        columns[col] = bool; // 数组内对应列col的情况,为啥只存储列col的情况,因为下面chair函数中是从某一行开始下,遍历该行中每一列的情况来判断该位置是否能下棋
    }
    
    function chair(row, currentChessboard){ // 开始下棋,从某一行开始下
        // 每一次下棋都会依据当前棋盘的局势来判断该行能否继续下棋,所以currentChessboard代表当前局势
        
        // 递归的出口
        if(row > n-1){ 
            queenPostion.push(currentChessboard)
            return; // 棋子下完了
        }
        
        // 递归的入口
        for (let col = 0; col < n; col++) { // 因为下面chair(0, [])是从行开始下的,所以这里要遍历同一行中每一列的情况
            if(
                !columns[col]&& // 当这一列没有棋子
                !leftTopToRightBottom[row + col]&& // 当左上到右下的对角线没有棋子
                !rightTopToLeftBottom[row - col + n -1] // 当右上到左下的对角线没有棋子
            ){ // 在以上情况同时满足为真时,可以下棋子
                tag(row,col,true); // 开始下第1局:第0行 第0列 true表示可以下,我下了, 然后 按照规则, 对应的对角线和行列 就不能再下了
                
                chair(row+1,currentChessboard.concat(col)); // 上一局下完了,开始下第2局棋,从第1行开始下,并且传入第1局的棋局,这里便进入递归,在第1行内逐列判断能否下棋子
                
                tag(row,col,false); // 在上一行代码递归执行的时候,这一行代码暂时不执行,等其递归出结果,将不能下的地方打标记
            }
        }
    }
    
    chair(0, []); // 从第0行开始下,循环第0行中每一列的情况。初始化第1局的起点:第0行 空棋局[]
    
    let result = []; // 结果数组
    
    queenPostion.forEach(item =>{
        result.push(boardCreater(item,n)); // 将每一局棋以“+”“-”模拟位置,并push添加到结果数组
    })
    
    return result; // 返回结果数组
}


function boardCreater(currentChessboard, n){ // 将所有棋局的局势以“+”“-”模拟出来
    let board = [];
    for (let index = 0; index < n; index++) {
        board[index] = []
        for (let i = 0; i < n; i++) {
            board[index].push('-'); // 填充了-标记的就是不能下棋子的地方
        }
    }
    for(let i = 0 ,len = currentChessboard.length; i< len; i++){
        board[i][currentChessboard[i]] = '+'; // 填充了+标记的就是可以下棋子的地方
    }
    return board;
}

calQueen(5); // 5*5棋局