前端算法与数据结构之算法思想(四)

466 阅读5分钟

前端算法与数据结构之栈、队列、链表(一)

前端算法与数据结构之集合、字典、树(二)

前端算法与数据结构之图、堆、搜索排序(三)

14、 算法思想之“分而治之”

14-1分而治之是什么?
  1. 分而治之是算法设计中的一-种方法。
  2. 它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。
14-1-1分而治之场景-归并排序
  1. 分:讲数组从中间一分为二
  2. 解:递归地对两个子数组进行归并排序。(直到把数组分成长度为1的子数组)
  3. 合:合并有序数组
14-1-2分而治之场景-快速排序
  1. 分:选择一个基准,按基准把数组分成两个子数组。
  2. 解:递归地对两个子数组进行快速排序。(直到把数组分成长度为1的子数组)
  3. 合:合并有序数组
14-2算法题
374. 猜数字大小

解题步骤:

  1. 分:计算中间元素,分割数组。
  2. 解:递归地在较大或者较小子数组进行二分搜索。
  3. 合:不需要此步,因为在子数组中搜到就返回了。
/**
 * @param {number} n
 * @return {number}
 */
var guessNumber = function(n) {
    const rec = (low, heigh) => {
        if(low > heigh) {return}
        const mid = Math.floor( (low + heigh) / 2);
        const res = guess(mid) //  int guess(int num) 来获取猜测结果
        if(res == 0) {
            return mid;
        }else if( res == 1) {
            return rec(mid + 1, heigh)
        }else {
            return rec(1, mid -1 )
        }
    }
    return rec(1, n) // 1 到 n 随机选择一个数字
};
226. 翻转二叉树

解题思路:

  1. 分:获取左右二叉树
  2. 解:递归左右子树,再进行兑换
  3. 合:返回一个树(翻转后的)
/**
 * @param {TreeNode} root
 * @return {TreeNode}
 */
var invertTree = function(root) {
    if(!root) {return null;}
    // 获取左右二叉树
    // let left = root.left;
    // let right = root.right;
    return {
        val: root.val,
        left: invertTree(root.right),
        right: invertTree(root.left)
    }
};
100. 相同的树

解题思路:

  1. 分:获取左右二叉树
  2. 解:递归左右子树,判断两个树的左子树和右子树是否相同
  3. 合:中上结果合并,如果根节点也相同,那么树相同。
/**
 * @param {TreeNode} p
 * @param {TreeNode} q
 * @return {boolean}
 */
var isSameTree = function(p, q) {
    if(!q && !p) { return true;}
    if(p && q && q.val == p.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right)) {
        return true;
    }else {
        return false;
    }
};
101. 对称二叉树

解题思路:

  1. 分:获取左右二叉树
  2. 解:解:递归地判断树1的左子树和树2的右子树是否镜像,树1的右子树和树2的左子树是否镜像。
  3. 合:中上结果合并,如果根节点镜像也相同,那么树是对称二叉树。
/**
 * @param {TreeNode} root
 * @return {boolean}
 */
var isSymmetric = function(root) {
    if(!root) {return true;}
    const isMirror =(l, r) => {
        if(!l&&!r) { return true}
        if(l && r && l.val === r.val && isMirror(l.left, r.right)&&isMirror(l.right, r.left)){
            return true;
        }
        return false;
    }
    return isMirror(root.left, root.right)
};

15、 算法思想之“动态规划”

15-1动态规划是什么?
  1. 动态规划是算法设计中的一种方法。
  2. 它将一个问题分解为相互重叠子问题,通过反复求解子问题,来解决原来的问题。

举个栗子:斐波那契数列

1624506160989.png

  1. 定义子问题: F(n) = F(n-1) + F(n-2)
  2. 反复执行:从2循环到n,执行上述公式。
15-2算法题
70. 爬楼梯
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if(n < 2) {return 1;}
    const dp = [1,1];
    for(let i = 2; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i - 2]
    }
    return dp[n]
};
198. 打家劫舍

解题思路:

  1. f(k) =从前k个房屋中能偷窃到的最大数额。
  2. Ak =k个房屋的钱数。
  3. f(k) = max(f(k-2) + Ak, f(k- 1))

解题步骤:

  1. 定义子问题: f(k) = max(f(k-2) + Ak, f(k- 1))
  2. 反复执行:从2循环到n,执行上述公式。
/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    if(nums.length === 0) { return 0;}
    const dp = [0 ,nums[0]];
    for(let i = 2; i <= nums.length; i++) {
        dp[i] = Math.max(dp[i - 2] + nums[i -1], dp[i - 1])
    }
    return dp[nums.length]
};

16、 算法思想之“贪心算法”

16-1贪心算法
  1. 贪心算法是算法设计中的一种方法。
  2. 期盼通过每个阶段的局部最优选择,从而达到全局的最优。
  3. 结果并不一定是最优

贪心算法不一定是最优解,举个栗子

零钱兑换

1、
输入:coins = [1,2,4] a, amount = 11;
输出:3;
解释:11 = 5 + 5 + 1; 优先选择金额较大的

2、
输入:coins = [1,3,4] a, amount = 6;
输出:3;
解释:6 = 4 + 1 + 1; 优先选择金额较大的。
但是我们可以选择3+3 从而让输出结果是2,这才是最优解。
16-2算法题
455. 分发饼干

解题思路

  1. 局部最优:既能满足孩子,还消耗最少。
  2. 先将“较小的饼干”分给“胃口最小"的孩子。

解题步骤

  1. 对饼干数组和胃口数组升序排序。
  2. 遍历饼干数组,找到能满足第一个孩子的饼干。
  3. 然后继续遍历饼干数组,找到满足第二、三、.... 、n个孩子的饼干。
/**
 * @param {number[]} g
 * @param {number[]} s
 * @return {number}
 */
var findContentChildren = function(g, s) {
    g = g.sort((a,b) => a - b)
    s = s.sort((a,b) => a - b);
    let j = 0;
    for(let i = 0; i< s.length; i++) {
        if(s[i] >= g[j]) {
            j++;
        }
    }
    return j;
};
122. 买卖股票的最佳时机 II

解题思路

  1. 前提: 上帝视角,知道未来的价格。
  2. 局部最优:见好就收,见差就不动,不做任何长远打算。

解题步骤

  1. 新建一个变量,用来统计总利润。
  2. 遍历价格数组,如果当前价格比昨天高,就在昨天买,今天卖,否则就不交易。
  3. 遍历结束后,返回利润。
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    let profits = 0; // 利润
    if(prices.length == 0) {return 0;}
    for(let i = 0; i < prices.length; i++) {
        if(prices[i+1] > prices[i]) {
            profits += prices[i+1] - prices[i]
        }
    }
    return profits
};

17、 算法思想之“回溯算法”

17-1回溯算法是什么?
  1. 回溯算法是算法设计中的一种方法。
  2. 回溯算法是一种渐进式寻找并构建问题解决方式的策略。
  3. 回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。

所谓回溯,就是走了一条路,发现走不通,拐回来原点再走另一条路

什么问题可以用回溯算法?

  1. 有很多路。(比喻)
  2. 这些路里,有死路,也有出路。(比喻)
  3. 通常需要递归来模拟所有的路。(比喻)
17-2算法题
46. 全排列

解题思路

  1. 要求: 1、所有排列情况; 2、没有重复元素。
  2. 有出路、有死路。
  3. 考虑使用回溯算法。

解题步骤

  1. 用递归模拟出所有情况。
  2. 遇到包含重复元素的情况,就回溯(递归结束)。
  3. 收集所有到达递归终点的情况,并返回(数组长度多少,就是多少)。
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var permute = function(nums) {
    let res = [];
    const backTrack =(path) => {
        if(path.length === nums.length) {
            res.push(path);
            return;
        }
        nums.forEach(n => {
            if(path.includes(n)) {return;} // 有重复数字的,之前返回,不走递归
            backTrack(path.concat(n))
        })
    }
    backTrack([]);
    return res;
};
78. 子集

解题思路

  1. 要求: 1、所有子集 2、没有重复元素。
  2. 有出路、有死路。
  3. 考虑使用回溯算法。

解题步骤

  1. 用递归模拟出所有情况。
  2. 保证接的数字都是后面的数字。
  3. 收集所有到达递归终点的情况,并返回。
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var subsets = function(nums) {
    let res = [];
    const backTrack =(path,l,start) => {
        if(path.length === l) {
            res.push(path);
            return;
        }
        for(let i = start; i<nums.length; i++) {
            backTrack(path.concat(nums[i]), l, i + 1) // 保证子集是有序的。
        }
    }
   for(let i = 0; i<= nums.length; i++) {
    //    i:路径长度 ,0 起始位置
        backTrack([],i,0)
   }
    return res;
};