递归算法笔记

125 阅读4分钟

1 递归

  • 分析问题和子问题的关系:采用自顶而下的策略;比如求解f(n),就必须先求出n*f(n-1)

2. 递归的三大要素

递归前提条件:求解一个大问题,这个大问题可以划分为多个子问题求解,并且每个子问题的规模都一样;如果规模不一样则没法使用递归

1. 明确递归函数函数想要干什么
2. 寻找递归结束条件
3. 找出函数的等价关系式(注意是单分支还是双分支)

2.1 明确递归函数函数想要干什么

  • 先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么,这个函数的功能是什么
// 算 n 的阶乘(假设n不为0)
function f(n){
}

2.2 寻找递归结束条件

  • 我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么

2.3 找出函数的等价关系式

注意是单分支还是双分支

  • 我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变

3 递归题

3.1 斐波那契(单分支)

// 题目:斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34….,即第一项 f(1) = 1,第二项 f(2) = 1…..,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。

function f(n){
    // 参数终止条件
    if (n == 1 || n == 2){
        return 1
    }
    // 等价关系 fn = f(n-1)+f(n-2)
    return f(n-1)+f(n-2);
}

3.2 小青蛙跳台阶(多分支)

// 题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
function f(n){
    if (n <= 2){
        return n;
    }
    return f(n-1)+f(n-2)
}

3.3 反转单链表

// 题目:反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1
function reverseList(Node head){
    if(head == null || head.next == null){
3        return head;
4    }
    // 递归2->3->4 返回4->3->2; 此时head还是指向1,因为1没动
    Node newList = reverseList(head.next);
    // 改变 1,2节点的指向。
    Node tmp = head.next;
    tmp.next = head;
    head.next = null;
    return newList;
}

3.4 求n!的阶乘

function f(n){
    if (n <= 1){
        return 1;
    }
    return n*f(n-1);
}

3.5 反转二叉树

function Node(value){
    this.left = null;
    this.right = null;
    this.value = value;
}
function reverseBinaryTree(Node root){
    // 叶子结果不能翻转
    if(root == null){
        return null;
    }
    // 翻转左节点下的左右节点
    Node leftRoot = reverseBinaryTree(root.left);
    // 翻转右节点下的左右节点
    Node rightRoot = reverseBinaryTree(root.right);

    // 左右节点下的二叉树翻转好后,翻转根节点的左右节点
    root.left = rightRoot;
    root.right = leftRoot;
    return root;
}

3.6 汉诺塔

// 题目:从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数
// 思路:以下几点递归思想至关重要
1. 定义问题的递归函数,明确函数的功能,我们定义这个函数的功能为:把 A 上面的 n 个圆盘经由 B 移到 C
2. 分析问题和子问题的关系:要采用自上而下的分析方式要将 n 个圆盘从A经由 B 移到 C 柱上去,首先需要将n-1看成一个整体,接着将n-1从A经由C移动到B,再将第n个盘子从A移动到C,再将n-1个盘子从B经由A移动到C。
3. 解决问题思想:采用从下往上,比如这里A只有两个盘子,那么需要将A最上面的盘子移动到B上,再将A的最下面的盘子移动到C上,再将B上面的盘子移动到C上。是一种循环思路,不过在找问题的过程中 切忌把子问题层层展开,到汉诺塔这个问题上切忌再分析 n-3,n-4 怎么移,这样会把你绕晕,只要找到一层问题与子问题的关系得出可以用递归表示即可

// 以上分析可以得到递推公式
move(A from C) = move(n-1 from A to B) + move(A to C) + move(n-1 from B to C)
// 汉诺塔
// 第一步:明白函数含义是: 将 n个盘子 从A 经由B 移动到C
function hanoid(n, A, B, C){
    // 第二步:终止条件是A上没有盘子可移动
    if (n <= 0){
        return;
    }
    // 将上面的  n-1 个圆盘 从A 经由 C 移到 B
    hanoid(n-1, A, C, B);

    // 此时将 A 底下的那块最大的圆盘移到 C
    move(A, C);

    // 再将 B 上的 n-1 个圆盘 从B 经由A 移到C上
    hanoid(n-1, B, A, C);
}

function move(a, b){
    console.log(`${a}->${b}`)
}

3.7 二叉树先序遍历

function preOrder(Node root){
    if(root == null){
        return;
    }
    console.log(root.data);
    preOrder(root.left);
    preOrder(root.right);
}

3.8 二叉树中序遍历

function inOrder(Node root){
    if(root == null){
        return;
    }
    inOrder(root.left);
    console.log(root.data);
    inOrder(root.right);
}

3.9 二叉树后续遍历

function postOrder(Node root){
    if(root == null){
        return;
    }
    postOrder(root.left);
    postOrder(root.right);
    console.log(root.data);
}

3.10 快速排序

// 快速排序图解:
基准这里默认是数组第一个数 m = 6
[6, 5, 2, 7, 3, 9, 8, 4, 10, 1]
 i                           j      m=6

arr[j] < m 所以需要将 arr[i] = arr[j]
[1, 5, 2, 7, 3, 9, 8, 4, 10, 1]
 i                           j      m=6

循环比较arr[i] < m  ++i 一直到 arr[i] > m 停
[1, 5, 2, 7, 3, 9, 8, 4, 10, 1]
          i                  j      m=6

arr[i] > m  所以需要将 arr[j] = arr[j]
[1, 5, 2, 7, 3, 9, 8, 4, 10, 1]
          i                  j      m=6
[1, 5, 2, 7, 3, 9, 8, 4, 10, 7]
          i                  j      m=6

循环比较arr[j]>m  --j 一直到 arr[j]<m 停
[1, 5, 2, 7, 3, 9, 8, 4, 10, 7]
          i           j             m=6


[1, 5, 2, 4, 3, 9, 8, 4, 10, 7]
          i           j             m=6

[1, 5, 2, 4, 3, 9, 8, 4, 10, 7]
                i     j             m=6

[1, 5, 2, 4, 3, 9, 8, 9, 10, 7]
                i     j             m=6

[1, 5, 2, 4, 3, 9, 8, 9, 10, 7]
                i     j             m=6

[1, 5, 2, 4, 3, 9, 8, 9, 10, 7]
                i                   m=6
                j
i==j时,arr[i] = m 将m放入基准位置
[1, 5, 2, 4, 3, 6, 8, 9, 10, 7]
                i                   m=6
                j

// 快排解析
从最右边j开始每次用arr[j]和基准m比大小,如果arr[j]>m, 则--j,否则将arr[j]直接赋值给arr[i], 接着比较arr[i]<m,则++i
如果arr[i]>m, 那么直接将arr[i]赋值给arr[j], 再比较arr[j]和m大小,依次类推;最终找到一个基准位置将m的值放到基准位,这样m左边都比m小,右边都比m大; 然后再对左右两边递归

function findMidIndex(arr, left, right){
    let i = left;
    let j = right;
    let key = arr[i];
    while(i<j){
        while(i < j && arr[j] >= key){
            --j;
        }
        arr[i] = arr[j];
        while(i < j && arr[i] <= key){
            ++i;
        }
        arr[j] = arr[i];
    }
    arr[i] = key;
    return i;
}
function quickSort(arr, left, right){
    if (left < right){
        let m = findMidIndex(arr, left, right);
        quickSort(arr, left, m-1);
        quickSort(arr, m+1, right);
    }
}


var arr = [6,5,2,7,3,9,8,4,10,1];
quickSort(arr, 0, arr.length-1);
console.log(arr);

4. 递归算法复杂度

递归算法的时间复杂度:子问题个数乘以解决一个子问题需要的时间