大厂算法面试之leetcode精讲10.递归&分治

视频教程(高效学习):点击学习

目录:

1.开篇介绍

2.工夫空间复杂度

3.动静布局

4.贪婪

5.二分查找

6.深度优先&广度优先

7.双指针

8.滑动窗口

9.位运算

10.递归&分治

11剪枝&回溯

12.堆

13.枯燥栈

14.排序算法

15.链表

16.set&map

17.栈

18.队列

19.数组

20.字符串

21.树

22.字典树

23.并查集

24.其余类型题

递归三要素

  • 递归函数以及参数
  • 递归终止条件
  • 递归单层搜寻逻辑

递归伪代码模版

function recursion(level, param1, param2, ...) {  //递归终止条件  if (level > MAX_LEVEL) {    // output result    return;  }  //解决以后层  process_data(level, data, ...);  //进入下一层  recursion(level + 1, p1, ...);  //重置状态  reverse_state(level);}

什么是分治:

分治会将大问题拆解成小问题,拆解到最小问题之后,开始一直合并后果,递归是分治实现的一种模式或者是分治实现的一部分,分治包含三分局部,合成、计算、合并。分治的场景很多,例如疾速排序,归并排序。

分治伪代码模版:

function divide_conquer(problem, param1, param2, ...){  if(problem === null){    // return result  }  //宰割问题  subproblem = split_problem(problem, data)  //计算子问题  subResult1 = divide_conquer(subproblem[0], p1, ...)  subResult2 = divide_conquer(subproblem[1], p1, ...)  subResult3 = divide_conquer(subproblem[2], p1, ...)  ...  result = process_resule(subResult1, subResult2, subResult3,...)}

举例

计算n! n! = 1 * 2 * 3... * n

function factorial(n) {  if (n <= 1) return 1;  return n * factorial(n - 1);}factorial(6);6 * factorial(5);6 * 5 * factorial(4);//...6 * 5 * 4 * 3 * 2 * factorial(1);6 * 5 * 4 * 3 * 2 * 1;6 * 5 * 4 * 3 * 2;//...6 * 120;720;

斐波那契数列F(n)=F(n-1)+F(n+2)

function fib(n) {  if (n === 0 || n === 1) {    return n;  }  return fib(n - 1) + fib(n - 2);}

50. Pow(x, n) (medium)

办法1:分治

  • 思路:当n是偶数的时候,对n进行分治,拆解为x*xn/2的次方,当n为奇数的时候拆分成x * myPow(x,n-1),留神当n是正数或者是0的非凡状况
  • 复杂度剖析:工夫复杂度:O(logn), n是进行二进制拆分的工夫复杂度。空间复杂度:O(logn), n为递归深度

js:

var myPow = function (x, n) {    if (n === 0) return 1 // n=0间接返回1    if (n < 0) {                   //n<0时 x的n次方等于1除以x的-n次方分        return 1 / myPow(x, -n)    }    if (n % 2) {    //n是奇数时 x的n次方 = x*x的n-1次方        return x * myPow(x, n - 1)    }    return myPow(x * x, n / 2) //n是偶数,应用分治,一分为二,等于x*x的n/2次方 }

Java:

class Solution {    public double myPow(double x, int n) {        long N = n;        return N >= 0 ? pow(x, N) : 1.0 / pow(x, -N);    }    public double  pow(double  x, long y) {        if (y == 0) {            return 1.0;        }        double ret = pow(x, y / 2);        return y % 2 == 0 ? ret * ret : ret * ret * x;    }}
办法2:二进制

  • 思路:对n的二进制一直右挪动,判断n的二进制最初一位是否是1, 如果是1则将后果乘以x。
  • 复杂度剖析:工夫复杂度O(logn): n为对 n 进行二进制拆分的工夫复杂度,空间复杂度O(1)

js:

var myPow = function (x, n) {    if (n < 0) {        x = 1 / x;        n = -n;    }    let result = 1;    while (n) {        if (n & 1) result *= x;  //判断n的二进制最初一位是否是1, 如果是1则将后果乘以x        x *= x;        n >>>= 1;        //进行无符号右移1位,此处不能应用有符号右移(>>)        //当n为-2^31转换成负数时的二进制位“10000000000000000000000000000000” ,           //如果采纳有符号右移时会取最左侧的数当符号即(1),所以返回的后果是 -1073741824        /*          C++ 中只有一种右移运算符——>>。它的定义如下:移出的低位舍弃;          如果是无符号数,高位补0;如果是有符号数,高位补符号位。          而JavaScript中有两种右移运算符——>>和>>>。其中>>是有符号右移,          即高位补符号位(可能会呈现正数变负数,负数变正数的异常情况);>>>是无符号右移,高位无脑补0。        */    }    return result;}

Java:

class Solution {    public double myPow(double x, int n) {        if(x == 0.0f) return 0.0d;        long b = n;        double result = 1.0;        if(b < 0) {            x = 1 / x;            b = -b;        }        while(b > 0) {            if((b & 1) == 1) result *= x;            x *= x;            b >>= 1;        }        return result;    }}

169. 少数元素(easy)

办法1.排序
  • 思路:排序数组,如果有一个数字呈现的频率大于n/2,则在数组nums.length / 2的地位就是这个数
  • 复杂度剖析:工夫复杂度:O(nlogn),快排的工夫复杂度。空间复杂度:O(logn),排序须要logn的空间复杂度

js:

var majorityElement = function (nums) {    nums.sort((a, b) => a - b);    return nums[Math.floor(nums.length / 2)];};

Java:

class Solution {    public int majorityElement(int[] nums) {        Arrays.sort(nums);        return nums[nums.length / 2];    }}
办法2.哈希表
  • 思路:循环数组,用哈希表存储数字和对应的个数,如果数字呈现的个数大于n/2则返回这个数
  • 复杂度剖析:工夫复杂度:O(n),n为nums数组的长度。空间复杂度:O(n),哈希表须要的空间

js:

var majorityElement = function (nums) {    let half = nums.length / 2;    let obj = {};    for (let num of nums) {        obj[num] = (obj[num] || 0) + 1;        if (obj[num] > half) return num;    }};

Java:

class Solution {    public int majorityElement(int[] nums) {        Map<Integer,Integer> obj = new HashMap<>();        for(int num : nums){            obj.put(num, obj.getOrDefault(num, 0) + 1);            if(obj.get(num) > nums.length / 2) return num;        }        return 0;    }}
办法3:对消

js:

//[1,1,2,2,2]const majorityElement = nums => {    let count = 1;    let majority = nums[0];    for (let i = 1; i < nums.length; i++) {        if (count === 0) {            majority = nums[i];        }        if (nums[i] === majority) {            count++;        } else {            count--;        }    }    return majority;};

java:

class Solution {    public int majorityElement(int[] num) {        int majority = num[0];        int count = 1;        for (int i = 1; i < num.length; i++) {            if (count == 0) {                count++;                majority = num[i];            } else if (majority == num[i]) {                count++;            } else {                count--;            }        }        return majority;    }}
办法4.分治

  • 思路:一直从数组的两头进行递归宰割,直到每个区间的个数是1,而后向上合并左右区间个数较多的数,向上返回。
  • 复杂度剖析:工夫复杂度:O(nlogn),一直二分,复杂度是logn,二分之后每个区间须要线性统计left与right的个数,复杂度是n。空间复杂度:O(logn),递归栈的耗费,一直二分。

Js:

var majorityElement = function (nums) {    const getCount = (num, lo, hi) => {//统计lo到hi之间num的数量        let count = 0;        for (let i = lo; i <= hi; i++) {            if (nums[i] === num) count++;        }        return count;    };    const getMode = (lo, hi) => {        if (lo === hi) return nums[lo];                  //拆分成更小的区间        let mid = Math.floor((lo + hi) / 2);        let left = getMode(lo, mid);        let right = getMode(mid + 1, hi);        if (left === right) return left;        let leftCount = getCount(left, lo, hi);//统计区间内left的个数        let rightCount = getCount(right, lo, hi);//统计区间内right的个数        return leftCount > rightCount ? left : right;//返回left和right中个数多的那个    };        return getMode(0, nums.length - 1);};

Java:

class Solution {    private int getCount(int[] nums, int num, int lo, int hi) {        int count = 0;        for (int i = lo; i <= hi; i++) {            if (nums[i] == num) {                count++;            }        }        return count;    }    private int getMode(int[] nums, int lo, int hi) {        if (lo == hi) {            return nums[lo];        }        int mid = (hi - lo) / 2 + lo;        int left = getMode(nums, lo, mid);        int right = getMode(nums, mid + 1, hi);        if (left == right) {            return left;        }        int leftCount = getCount(nums, left, lo, hi);        int rightCount = getCount(nums, right, lo, hi);        return leftCount > rightCount ? left : right;    }    public int majorityElement(int[] nums) {        return getMode(nums, 0, nums.length - 1);    }}

124. 二叉树中的最大门路和 (hard)

办法1.递归

  • 思路:从根节点递归,每次递归分为走右边、左边、不动 3种状况,用以后节点加上左右子树最大门路和不断更新最大门路和
  • 复杂度:工夫复杂度O(n),n为树的节点个数。空间复杂度O(n),递归深度,最差状况下为数的节点数

js:

const maxPathSum = (root) => {    let maxSum = Number.MIN_SAFE_INTEGER;//初始化最大门路和    const dfs = (root) => {        if (root == null) {//遍历节点是null 返回0           return 0;        }        const left = dfs(root.left);   //递归左子树最大门路和        const right = dfs(root.right); //递归右子树最大门路和        maxSum = Math.max(maxSum, left + root.val + right);      //更新最大值          //返回以后子树的门路和 分为走右边、左边、不动 3种状况        const pathSum = root.val + Math.max(0, left, right);        return pathSum < 0 ? 0 : pathSum;    };    dfs(root);    return maxSum; };

java:

class Solution {    int maxSum = Integer.MIN_VALUE;    public int dfs(TreeNode root){        if (root == null) {           return 0;        }        int left = dfs(root.left);        int right = dfs(root.right);        maxSum = Math.max(maxSum, left + root.val + right);        int pathSum = root.val + Math.max(Math.max(0, left), right);        return pathSum < 0 ? 0 : pathSum;    }    public int maxPathSum(TreeNode root) {        dfs(root);        return maxSum;    }}

53. 最大子序和 (easy)

办法1:动静布局
  • 思路:以后最大子序和只和后面的子序和相干,循环数组,不断更新最大子序和
  • 复杂度:工夫复杂度O(n),空间复杂度O(1)

js:

var maxSubArray = function(nums) {    const dp = [];    let res = (dp[0] = nums[0]);//初始化状态    for (let i = 1; i < nums.length; ++i) {        dp[i] = nums[i];        if (dp[i - 1] > 0) {//后面的状态是负数 则加上            dp[i] += dp[i - 1];        }        res = Math.max(res, dp[i]);//更新最大值    }    return res;};//状态压缩var maxSubArray = function(nums) {    let pre = 0, maxAns = nums[0];    nums.forEach((x) => {        pre = Math.max(pre + x, x);        maxAns = Math.max(maxAns, pre);    });    return maxAns;};

java:

class Solution {    public int maxSubArray(int[] nums) {        int pre = 0, maxAns = nums[0];        for (int x : nums) {            pre = Math.max(pre + x, x);            maxAns = Math.max(maxAns, pre);        }        return maxAns;    }}
办法2.分治
  • 思路:一直宰割,直到每个局部是一个数字为止,而后一直合并,返回左右和左右合并之后,3个最大子序和中的最大的一个
  • 复杂度:工夫复杂度O(nlogn),二分复杂度O(logn),二分之后每一层统计左右和合并之后的最大子序和复杂度是O(n),所以之后的复杂度是O(nlogn)。空间复杂度O(logn),递归的栈空间,因为是二分,每次数据规模减半

js:

function crossSum(nums, left, right, mid) {    if (left === right) {//左右相等 返回右边的值        return nums[left];    }    let leftMaxSum = Number.MIN_SAFE_INTEGER;//右边最大值初始化    let leftSum = 0;    for (let i = mid; i >= left; --i) {        leftSum += nums[i];        leftMaxSum = Math.max(leftMaxSum, leftSum);//更新右边最大子序和    }    let rightMaxSum = Number.MIN_SAFE_INTEGER;    let rightSum = 0;    for (let i = mid + 1; i <= right; ++i) {        rightSum += nums[i];        rightMaxSum = Math.max(rightMaxSum, rightSum);//更新左边最大子序和    }    return leftMaxSum + rightMaxSum;//返回左右合并之后的最大子序和}function _maxSubArray(nums, left, right) {    if (left === right) {//递归终止条件        return nums[left];    }    const mid = Math.floor((left + right) / 2);    const lsum = _maxSubArray(nums, left, mid);//右边最大子序和    const rsum = _maxSubArray(nums, mid + 1, right);//左边最大子序和    const cross = crossSum(nums, left, right, mid);//合并左右的之后的最大子序和    return Math.max(lsum, rsum, cross);//返回3中子序和中最大的}var maxSubArray = function(nums) {    return _maxSubArray(nums, 0, nums.length - 1);};

java:

public class Solution {    public int maxSubArray(int[] nums) {        int len = nums.length;        if (len == 0) {            return 0;        }        return _maxSubArray(nums, 0, len - 1);    }    private int crossSum(int[] nums, int left, int mid, int right) {        int sum = 0;        int leftSum = Integer.MIN_VALUE;        for (int i = mid; i >= left; i--) {            sum += nums[i];            if (sum > leftSum) {                leftSum = sum;            }        }        sum = 0;        int rightSum = Integer.MIN_VALUE;        for (int i = mid + 1; i <= right; i++) {            sum += nums[i];            if (sum > rightSum) {                rightSum = sum;            }        }        return leftSum + rightSum;    }    private int _maxSubArray(int[] nums, int left, int right) {        if (left == right) {            return nums[left];        }        int mid = left + (right - left) / 2;        return max3(_maxSubArray(nums, left, mid),                _maxSubArray(nums, mid + 1, right),                crossSum(nums, left, mid, right));    }    private int max3(int num1, int num2, int num3) {        return Math.max(num1, Math.max(num2, num3));    }}

938. 二叉搜寻树的范畴和 (easy)

办法1:dfs
  • 复杂度:工夫复杂度O(n),空间复杂度O(n)

js:

var rangeSumBST = function(root, low, high) {    if (!root) {        return 0;    }    if (root.val > high) {        return rangeSumBST(root.left, low, high);    }    if (root.val < low) {        return rangeSumBST(root.right, low, high);    }    return root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);};

java:

class Solution {    public int rangeSumBST(TreeNode root, int low, int high) {        if (root == null) {            return 0;        }        if (root.val > high) {            return rangeSumBST(root.left, low, high);        }        if (root.val < low) {            return rangeSumBST(root.right, low, high);        }        return root.val + rangeSumBST(root.left, low, high) + rangeSumBST(root.right, low, high);    }}
办法2:bfs
  • 复杂度:工夫复杂度O(n),空间复杂度O(n)

js:

var rangeSumBST = function(root, low, high) {    let sum = 0;    const q = [root];    while (q.length) {        const node = q.shift();        if (!node) {            continue;        }        if (node.val > high) {            q.push(node.left);        } else if (node.val < low) {            q.push(node.right);        } else {            sum += node.val;            q.push(node.left);            q.push(node.right);        }    }    return sum;};

java:

class Solution {    public int rangeSumBST(TreeNode root, int low, int high) {        int sum = 0;        Queue<TreeNode> q = new LinkedList<TreeNode>();        q.offer(root);        while (!q.isEmpty()) {            TreeNode node = q.poll();            if (node == null) {                continue;            }            if (node.val > high) {                q.offer(node.left);            } else if (node.val < low) {                q.offer(node.right);            } else {                sum += node.val;                q.offer(node.left);                q.offer(node.right);            }        }        return sum;    }}