阅读 5636
「算法思想🌿」为了解决一个小问题,我写了 88 W 行代码! 🌧️

「算法思想🌿」为了解决一个小问题,我写了 88 W 行代码! 🌧️

大家好,我是寒草😈,一只草系码猿🐒。间歇性热血🔥,持续性沙雕🌟
如果喜欢我的文章,可以关注➕ 点赞,与我一同成长吧~
加我微信:hancao97,邀你进群,一起学习交流前端,成为更优秀的工程师~

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

大家好,好久没有给大家整活儿了🔥,今天中午在我的前端早早睡群里有人发了这样一张截图:

image.png

当时大家都笑喷了,我也感觉很有趣,这个截图也给了我很大的灵感,我要不也写一份这样的代码给大家整个活儿?

遇事不决,穷举搞定

但是这个代码那么长,我是这么懒的人,咋肯定写这样的代码呢,于是我就需要用最简单的方式给大家整活儿,动态生成这 88 W 行代码。

代码实现

说代码实现之前,我还是列一下题目要求:

给出一个不多于 5 位的正整数,要求:

  • 求出他是几位数
  • 分别输出每一位数
  • 按照逆序输出各位数字,比如原数字是 12345,新的数字是 54321

主要思路还是用 node 的 fs 相关 api,完成文件写入。

const { writeFileSync } = require('fs');
const { join } = require('path');

const generateFunction = () => {
  let currentNum = 0;
  let str = '';
  while( currentNum < 100000) {
    const stringCurrentNum = String(currentNum);
    const numArr = stringCurrentNum.split('').reverse();
    const unitList = ['个', '十', '百', '千', '万']
    str += `    case ${currentNum}: \n`
    str += `      console.log('是 ${stringCurrentNum.length} 位数');\n`;
    numArr.forEach((char, index) => {
      str += `      console.log('${unitList[index]}位数是:${char}')\n`;
    })
    str += `      console.log('倒过来是:${Number(numArr.join(''))}');\n`;
    str += `      break;\n`;
    currentNum ++;
  }
  writeFileSync(join(__dirname, 'func.js'), `
  function handleNumber(num) {
    switch (num) {
      ${str}
    }
  }`, {
    encoding: 'utf-8'
  })
}

generateFunction();
复制代码

注意处理反转之后开头的 0 哦,这里我直接转成了 Number~

之后大家可以看一下我生成的文件内容:

image.png

88 w 行代码,穷举法,永远的神!

准确的说是 888897 行代码。

之后我们看一下效果:

image.png

没有问题!

浅析算法思想

image.png

我对算法了解不足,如有错误,还请多多指教✨

那么写这个内容总不能就是博君一笑吧,我也想和大家说一说我的思考,并为大家延伸更多的东西。首先大家可能都觉得穷举法比较 LOW,那我先给大家讲个例子:

刘慈欣的《诗云》中通过人类的诗人与宇宙高级文明之间的对话,讨论了绝对的技术与人类的艺术之间的关系。高级文明想通过写出比李白还要好的诗证明技术胜过艺术,但无论他学李白喝酒还是穿李白的衣服,他都无法写出李白那样的诗。最后他想到了穷举的办法,通过量子储存,他将人类的汉字进行了所有的排列组合,包含着全部可能的诗词。

穷举法一直都是好用的手段(毕竟上文中的高级文明都要用),并且应用十分广泛(比如:计算机领域),对于很多问题穷举法都是极其好用或者最好用的手段,比如字符串匹配,就是从头开始遍历所有可能的结果,就算是大家耳熟能详的 KMP 算法也无非是通过之前的匹配情况去筛去不可能的结果,无非是经过处理更加高级的穷举。

既然说到穷举法,那本章寒草 🌿 就和大家简单聊聊几种算法思想,也算是对于这个问题的延伸。

本章内容参考:

穷举法

首先最好懂的,最简单的思想就是穷举,也叫做枚举。 穷举穷举,顾名思义,就是穷尽列举,穷举思想的应用场景十分广泛,也非常容易理解。简单来说,穷举就是将问题的可能解依次列举出来,然后一一带入问题检验,从而从一系列可能解中获得。

大道至简,采用枚举法就能够很好的规避系统复杂性带来的冗余,同时或许在一定程度上还能够对空间进行缩减。

优化穷举法有两种方案:

  • 一是问题的简化,尽可能对需要处理的问题进行模型结构上的精简。这种精简具体可体现在问题中的变量数目,减少变量的数据,从而能够从根本上降低「可能解」的组合。
  • 二是对筛选「可能解」的范围和条件进行严格判断,尽可能的剔除大部分无效的「可能解」。

举一个例子,emmmm,算了,就比如上文中的例子吧,或者找出1到100之间的素数。

递推法

与枚举算法思想相比,递推算法能够通过已知的某个条件,利用特定的关系得出中间推论,然后逐步递推,直到得到结果为止。由此可见,递推算法要比枚举算法聪明,它不会尝试每种可能的方案。

递推算法可以不断利用已有的信息推导出新的东西,在日常应用中有如下两种递推 算法。

  • 顺推法:从已知条件出发,逐步推算出要解决问题的方法。例如斐波那契数列就可以通过顺推法不断递推算出新的数据。
  • 逆推法:从已知的结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程。

有一种很好的方法让大家理解递推法,就是被数学证明题:

  1. 因为三角形内角和是 180 度,且其中两个角分别为 65 度和 70 度。
  2. 所以第三个角是 45 度。
  3. 因为平角是 180 度。
  4. 所以该角的外角是 135 度。

最后举一个例子,一对大兔子每月能生一对小兔子,且每对新生的小兔子经过一个月可以长成一对大兔子,具备繁殖能力,如果不发生死亡,且每次均生下一雌一雄,问一年后共有多少对兔子?

递归法

递归算法是把问题转化成规模更小的同类子问题,先解决子问题,再通过相同的求解过程逐步解决更高层次的问题,最终获得最终的解。所以相较于递推而言,递归算法的范畴更小,要求子问题跟父问题的结构相同。而递推思想从概念上并没有这样的约束。

用一句话来形容递归算法的实现,就是在函数或者子过程的内部,直接或间接的调用自己算法。所以在实现的过程中,最重要的是确定递归过程终止的条件,也就是迭代过程跳出的条件判断。否则,程序会在自我调用中无限循环。

举一个例子就是经典的汉诺塔问题。源于印度传说中,大梵天创造世界时造了三根金钢石柱子,其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

分治法

分治算法将一个规模为 N 的问题分解为 K 个规模较小的子问题,这些子问题相互独立且与原问题性质相同。只要求出子问题的解,就可得到原问题的解。

在编程过程中,经常遇到处理数据相当多、求解过程比较复杂、直接求解法会比较耗时的问题。在求解这类问题时,可以采用各个击破的方法。

具体做法是:先把这个问题分解成几个较小的子问题,找到求出这几个子问题的解法后,再找到合适的方法,把它们组合成求整个大问题的解。如果这些子问题还是比较大,还可以继续再把它们分成几个更小的子问题,以此类推,直至可以直接求出解为止。这就是分治算法的基本思想。

使用分治算法解题的一般步骤如下。

  • 分解,将要解决的问题划分成若干个规模较小的同类问题。
  • 求解,当子问题划分得足够小时,用较简单的方法解决。
  • 合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。

举个例子,比如归并排序

贪心算法

贪心算法也被称为贪婪算法,它在求解问题时总想用在当前看来是最好方法来实现。这种算法思想不从整体最优上考虑问题,仅仅是在某种意义上的局部最优求解。

虽然贪心算法并不能得到所有问题的整体最优解,但是面对范围相当广泛的许多问题时,能产生整体最优解或者是整体最优解的近似解。由此可见,贪心算法只是追求某个范围内的最优,可以称之为“温柔的贪婪”。

贪心算法从问题的某一个初始解出发,逐步逼近给定的目标,以便尽快求出更好的解。当达到算法中的某一步不能再继续前进时,就停止算法,给出一个近似解。由贪心算法的特点和思路可看出,贪心算法存在以下3个问题。

  • 不能保证最后的解是最优的。
  • 不能用来求最大或最小解问题。
  • 只能求满足某些约束条件的可行解的范围。

贪心算法的基本思路如下。

  • 建立数学模型来描述问题。
  • 把求解的问题分成若干个子问题。
  • 对每一子问题求解,得到子问题的局部最优解。
  • 把子问题的局部最优解合并成原来解问题的一个解。

贪心算法的经典例子很多,比如背包问题,推销问题。

试探法

试探法也叫回溯法,它将问题的候选解按某种顺序逐一进行枚举和检验。当发现当前候选解不可能是正确的解时,就选择下一个候选解。

如果当前候选解除了不满足问题规模要求外能够满足所有其他要求时,则继续扩大当前候选解的规模,并继续试探。如果当前候选解满足包括问题规模在内的所有要求时,该候选解就是问题的一个解。在试探算法中,放弃当前候选解,并继续寻找下一个候选解的过程称为回溯。扩大当前候选解的规模,并继续试探的过程称为向前试探。

使用试探算法解题的基本步骤如下所示。

  • 针对所给问题,定义问题的解空间。
  • 确定易于搜索的解空间结构。
  • 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

试探法为了求得问题的正确解,会先委婉地试探某一种可能的情况。在进行试探的过程中,一旦发现原来选择的假设情况是不正确的,立即会自觉地退回一步重新选择,然后继续向前试探,如此这般反复进行,直至得到解或证明无解时才死心。

举个经典的例子,八皇后问题。在 8 × 8 格的国际象棋上摆放 8 个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,求摆法。

image.png

动态规划

可以去看看这个问答:www.zhihu.com/question/23…

这一节想加入一些自己的心得与编码实践,过一阵补上~

模拟法

许多真实场景下,由于问题规模过大,变量过多等因素,很难将具体的问题抽象出来,也就无法针对抽象问题的特征来进行算法的设计。这个时候,模拟思想或许是最佳的解题策略。

模拟的过程就是对真实场景尽可能的模拟,然后通过计算机强大的计算能力对结果进行预测。这相较于上述的算法是一种更为宏大的思想。在进行现实场景的模拟中,可能系统部件的实现都需要上述几个算法思想的参与。

模拟说起来是一种很玄幻的思想,没有具体的实现思路,也没有具体的优化策略。只能说,具体问题具体分析。

模拟法实践 ——— 求圆周率

比如我求圆周率,就可以转换为概率问题:随机打点落在扇形区域的概率

image.png

于是,我开始编码:

let i = 0;
let num = 0;
const sum = 100000000;
while(i < sum) {
    i ++;
    const x = Math.random();
    const y = Math.random();
    if(x * x + y * y < 1) {
        num++;
    }
}
console.log(4 * num / sum);
复制代码

我模拟了 100000000 次,结果如下:

image.png

结束语

image.png

参考:

这篇文章灵感来自于那张图片,哈哈哈,整活儿很开心✨。

还有最近打算不整活儿了,打算重新学习前端,文章的内容和顺序按照之前的「 30天整理 |2W字长篇」用一篇文章明确前端学习路线并构筑知识体系🌿,可能我最开始复习的不会是 HTML 和 CSS ,而是一些通用技能,请大家保持期待。

喜欢我的文章,点赞和关注便是最大的鼓励,也可以加我好友:hancao97,与我深入交流

写在最后

我也想
能够把你照亮
在你的生命中留下阳光
陪你走过那山高水长
陪你一起生长☀️

文章分类
前端
文章标签