分治、动态规划、回溯、贪心一锅炖


这是前端食堂的第32篇原创  


「观感度:🌟🌟🌟🌟🌟」
前端


「口味:东北一锅出」git

「烹饪时间:10min」github

本文已收录在Github github.com/Geekhyt,感谢Star。web

数据结构与算法系列专栏第四弹来袭,往期专栏连接以下:算法

初学者一听到算法思想,就会以为它们高深莫测,只能望而却步。编程

但若是你看过《事实》这本书,你就不会被大脑中的惯性思惟所影响。只要咱们理解算法思想的关键点,多作题练习并加深理解记忆。其实算法思想就像切菜同样简单。数组

上一篇算法系列专栏中咱们搞明白了递归。其实递归这种编程技巧是不少算法的基础。微信

还看过的同窗建议先移步这篇专栏你真的懂递归吗?cookie

好比本文讲到的这几种算法思想,大部分都是基于递归思想基础上的。数据结构

一句话理解四种算法思想

分治:分而治之,先解决子问题,再将子问题的解合并求出原问题。

贪心:一条路走到黑,选择当下局部最优的路线,没有后悔药。

回溯:一条路走到黑,手握后悔药,能够无数次重来。(英雄联盟艾克大招无冷却)。

动态规划:上帝视角,手握无数平行宇宙的历史存档,同时发展出无数个将来。

接下来咱们一块儿庖丁解牛,将这几种算法思想一锅炖。

分治算法 Divide and Conquer

分治算法思想很大程度上是基于递归的,也比较适合用递归来实现。顾名思义,分而治之。通常分为如下三个过程:

  • 分解:将原问题分解成一系列子问题。
  • 解决:递归求解各个子问题,若子问题足够小,则直接求解。
  • 合并:将子问题的结果合并成原问题。

比较经典的应用就是归并排序 (Merge Sort) 以及快速排序 (Quick Sort) 等。咱们来从归并排序理解分治思想,归并排序就是将待排序数组不断二分为规模更小的子问题处理,再将处理好的子问题合并起来。

上代码。

const mergeSort = function(arr{
    const len = arr.length;
    if (len > 1) {
        // 对半分解
        const middle = Math.floor(len / 2);
        const left = arr.slice(0, middle);
        const right = arr.slice(middle, len);
        let i = 0
        let j = 0;
        let k = 0;
        // 分别对左右进行排序
        mergeSort(left);
        mergeSort(right);
        while(i < left.length && j < right.length) {
            if (left[i] < right[j]) {
                arr[k] = left[i];
                i++;
            } else {
                arr[k] = right[j];
                j++;
            }
            k++;
        }
        // 检查余项
        while(i < left.length) {
            arr[k] = left[i];
            i++;
            k++;
        }
        while(j < right.length) {
            arr[k] = right[j];
            j++;
            k++;
        }
    }
    return arr;
}

复杂度分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

动态规划  Dynamic Programming

LeetCode真题

70. 爬楼梯 

https://leetcode-cn.com/problems/climbing-stairs/

虽然动态规划的最终版本 (降维再去维) 大都不是递归,但解题的过程仍是离开不递归的。新手可能会以为动态规划思想接受起来比较难,确实,动态规划求解问题的过程不太符合人类常规的思惟方式,咱们须要切换成机器思惟。

使用动态规划思想解题,首先要明确动态规划的三要素。

动态规划三要素

  • 重叠子问题
  • 最优子结构
  • 状态转移方程

重叠子问题

切换机器思惟,自底向上思考。

爬第 n 阶楼梯的方法数量,等于两部分之和:

  • 爬上 n-1 阶楼梯的方法数量
  • 爬上 n-2 阶楼梯的方法数量

最优子结构

子问题的最优解可以推出原问题的优解。

状态转移方程

dp[n] = dp[n-1] + dp[n-2]

具有三要素,确认边界条件,初始化状态,开始切菜:

  • dp[0] = 1
  • dp[1] = 1
const climbStairs = function(n{
    const dp = [];
    dp[0] = 1;
    dp[1] = 1;
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
};

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

优化

在此基础上,咱们还能够经过压缩空间来对算法进行优化。由于 dp[i]只与 dp[i-1]dp[i-2] 有关,没有必要存储全部出现过的 dp 项,只用两个临时变量去存储这两个状态便可。

const climbStairs = function(n{
    let a1 = 1;
    let a2 = 1;
    for (let i = 2; i <= n; i++) {
        [a1, a2] = [a2, a1 + a2];
    }
    return a2;
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

贪心算法 Greedy

最近某音很火的贪心土味情话

喂,不是吧。今天喝了脉动啊,吃了果冻啊,可是,仍是忍不住对你心动啊。

回到算法中,贪心算法动态规划算法的一个子集,能够更高效解决一部分更特殊的问题。实际上,用贪心算法解决问题的思路,并不总能给出最优解。由于它在每一步的决策中,选择目前最优策略,不考虑全局是否是最优。

LeetCode真题

LeetCode 455. 分发饼干

https://leetcode-cn.com/problems/assign-cookies/description/

思路

贪心算法+双指针求解。

  • 给一个孩子的饼干应当尽可能小而且能知足孩子,大的留来知足胃口大的孩子
  • 由于胃口小的孩子最容易获得知足,因此优先知足胃口小的孩子需求
  • 按照从小到大的顺序使用饼干尝试是否可知足某个孩子
  • 当饼干 j >= 胃口 i 时,饼干知足胃口,更新知足的孩子数并移动指针   i++ j++ res++
  • 当饼干 j < 胃口 i 时,饼干不能知足胃口,须要换大的 j++

关键点

将需求因子 g 和 s 分别从小到大进行排序,使用贪心思想配合双指针,每一个饼干只尝试一次,成功则换下一个孩子来尝试。

复杂度分析

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(1)
const findContentChildren = function (g, s{
    g = g.sort((a, b) => a - b);
    s = s.sort((a, b) => a - b);
    let gi = 0// 胃口值
    let sj = 0// 饼干尺寸
    let res = 0;
    while (gi < g.length && sj < s.length) {
        if (s[sj] >= g[gi]) {
            gi++;
            sj++;
            res++;
        } else {
            sj++;
        }
    }
    return res;
};

回溯算法 Backtracking

回溯算法本质上就是枚举,使用摸着石头过河的查找策略,还能够经过剪枝少走冤枉路。

LeetCode真题

LeetCode 17.电话号码的字母组合

https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/

思路

使用回溯法进行求解,回溯是一种经过穷举全部可能状况来找到全部解的算法。若是一个候选解最后被发现并非可行解,回溯算法会舍弃它,并在前面的一些步骤作出一些修改,并从新尝试找到可行解。究其本质,其实就是枚举。

若是没有更多的数字须要被输入,说明当前的组合已经产生。

若是还有数字须要被输入:

  • 遍历下一个数字所对应的全部映射的字母
  • 将当前的字母添加到组合最后,也就是 str + tmp[r]

关键点

在for循环中调用递归。

复杂度分析

N+M 是输入数字的总数

  • 时间复杂度:O(3^N * 4^M)
  • 空间复杂度:O(3^N * 4^M)
const letterCombinations = function (digits{
    if (!digits) {
        return [];
    }
    const len = digits.length;
    const map = new Map();
    map.set('2''abc');
    map.set('3''def');
    map.set('4''ghi');
    map.set('5''jkl');
    map.set('6''mno');
    map.set('7''pqrs');
    map.set('8''tuv');
    map.set('9''wxyz');
    const result = [];

    function generate(i, str{
        if (i == len) {
            result.push(str);
            return;
        }
        const tmp = map.get(digits[i]);
        for (let r = 0; r < tmp.length; r++) {
            generate(i + 1, str + tmp[r]);
        }
    }
    generate(0'');
    return result;
};

❤️爱心三连击

1.看到这里了就点个在看支持下吧,你的「在看」是我创做的动力。

2.关注公众号前端食堂,「你的前端食堂,记得按时吃饭」

3.本文已收录在前端食堂Github github.com/Geekhyt,求个小星星,感谢Star。

公众号:前端食堂


掘金:童欧巴


知乎:童欧巴


这是一个终身学习的男人,他在坚持本身热爱的事情,欢迎加入前端食堂,和这个男人一块儿开心的变胖~


推荐阅读:

一张图探秘淘宝直播背后的前端技术

解读新一代 Web 性能体验和质量指标

  在看和转发是莫大鼓励❤️


本文分享自微信公众号 - 前端食堂(webcanteen)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索