图解算法-读后感-二分法

170 阅读6分钟

headerImg.png

这我的的github地址:github,这是我的b站直播间每天都会直播写代码:前端自习室,期待关注!!!

背景

算法产生的背景个人感觉其实与西方经济学核心的理念是一致的。资源的稀缺性和人类无尽的欲望之间的矛盾。如果资源是无限供给的,也就不存在市场,价格,供求矛盾了。

回归到算法如果计算资源是无限性的,计算性能是无限的,那就不存在任何算法可言,没必要了。正因为计算资源的有限性,所以我们需要更合理的使用计算资源,更好的编程方式。

别以为这不重要,一个好的算法,一种好的设计模式,一个高效的语言场景,虽然在小公司没啥影响,一台服务器,一天也没多少人访问,但是在中型公司就能节省百万级别的资源费用。这是技术该考虑的场景。

算法简介

基础条件

  1. 基础的数学知识
  2. 基础的编程知识

二分法

找一本书的第多少页,最快的方式是什么?例如一本书是500页,我想最快找到第354页。 最快的方式就是,从中间把书撕开,然后在把撕开的下半本书从中间斯开,如此往复。 显然现实中我们只需要翻书就可以了,不需要撕书那么暴力。

book.png

整个执行的过程如下

1-500)(250-500) (250-375) (313-375)(343-375) (343-359500------->>250------>>375------>>313 ------>>343-------->>359
351-359) (351-355) (353-355354
 351------->>355------>>353 ------>>354 

以上是使用算法的方式,如果不使用算法,我们则需要从第一页开始,执行354次才可以找到那一页。

回归到js的代码实现

// 2的32次方是4294967296 
// 2的3次方是8 
// 2的8次方是256 
// 2的16次方是65536
<script>
  let array = [];
  const resCount = 500;
  // const target = parseInt(Math.random() * resCount);
  const target = 354;
  for (let i = 1; i <= resCount; i++) {
    array.push(i);
  }
  console.log("数组长度", resCount, "目标值", target);
  let count = 0;
  // 二分法函数
   function half(targetList, target) {
    const length = targetList.length;
    if (targetList[0] > target || targetList[length - 1] < target) {
      return -1;
    }
    const index =
      Math.floor(length / 2 - 1) <= 0 ? 0 : Math.floor(length / 2 - 1);
    const compareValue = targetList[index];
    console.log("中间值", compareValue);
    count++;
    console.log("执行次数", count);
    if (compareValue === target) {
      return compareValue;
    } else if (compareValue > target) {
      return half(targetList.slice(0, index + 1), target);
    } else {
      return half(targetList.slice(index + 1), target);
    }
  }
  half(array, target);
</script>

看完实例之后思考两个问题

数学概念

真正关心的其实有一点,就是我们是否知道一个算法的执行次数,一个问题抛出了,能否用数学的方式表现是解释这个问题的关键。 所有人文的话语都没有数学的表达力强。这一点一定要成为准则,例如我粉丝的增长分析,开源star数,网站的访问量。

计算次数

正如我们的问题一样,我们不同的实现方案,就要有不同的执行计算次数。我需要考虑的永远不是最巧实现,那没有意义。例如遍历数据方法,正好第一个数据就是目标数据,例如二分法第一次二分结果就是目标结果,这样的两个场景都是没有意义。我们先来观察最差情况这个场景,如果是目标值是最难找的那个数。应该怎么用数学表示。

如果使用遍历的方式,我们很快就发现,多大的数组就需要执行多少次,我们是不是可以计为x次。

如果使用二分法,执行不同的数据可结果,大家也很容易发现规律如果如果是找在4里面找3,需要执行2次,在8里面找7需要执行三次,在16里面找15需要找4次,明显这个查找的次数与2的次方数有关系。

其实就是对数关系,对数(英语:Logarithm)是幂运算的逆运算。 y=log2(x)y=\log_2(x) ,y是以2为底数,x的对数。

2的三次方是8,那么3就是以2为底数8的对数,以此类推。

增长速度

compare.png

因为我们问题的抽象其实是一个无上限的有序数组查找的问题,我们把这个两个算法抽象成函数,数学表达式分别是y=xy=x,y=log2(x)y=\log_2(x) 表达式写的有问题 看图可知我们x的值越大,y的变化率呈现不同的效果。 例如2的23次方是42亿多,42亿的数据用二分法查询只需要32次;而使用遍历的方式则需要42亿次。我们由此不难得出,从数学的角度看,二分法是更高效的查询方式。

大O表示法

概念

不同语言不同设备代码的执行效果是不一样的,例如同样的10000个数的遍历循环,js这样的脚本语言和c这样的语言执行的效率是不一样的。不同设备也是如果,一个10年前的mac电脑和今年新出的mac比计算速度显然那是不合适的。

我们需要对算法进行抽象,更好的评估一个算法正在的执行时间。不依赖于语言,设备,场景。

结合上文算法时最差选择情况下的表达力,而不是最优效果偶先优化。不能你查询1个数第一个数就是你想要的数字,这显然是不合理的。一定最差的情况有多差,才有算法的意义。

抽象运行时间,一个西瓜切几刀变成4块。

切4刀当然变成4块,交叉两刀也是4块,对于达成4块的这样的目的,显然两刀是更快的方式。

抽象的最好就是数学表现。

常用场景

我们已经知道了上面两种算法的基本表示方式

  • O(log2(n)\log_2(n))对数时间,二分法就是这样的场景
  • O(n) 线性时间,遍历查询就是这样的场景

复杂场景

有些时候算法并不能减少多少时间,这是大家要接受的现实。例如旅行商最短路线的问题。

多种实现

二分法使用js还有其他编码的形式,使用while循环也能实现,效率可能更高,但是人们往往会使用递归的方式去实现,就是因为递归的问题分解的方式,代码的阅读性更高。这就是我们抉择的过程,每天都是处于选择的场景下面,如何执行一直是程序员走向架构师的必经之路,架构师就是要做出更好的选择。

function half2(targetList, target) {
    let lowIndex = 0;
    let highIndex = array.length - 1;
    while (lowIndex <= highIndex) {
      let midIndex = Math.floor((lowIndex + highIndex) / 2);
      const compareValue = targetList[midIndex];
      console.log("中间值", compareValue);
      count++;
      console.log("执行次数", count);
      if (target === compareValue) {
        return compareValue;
      } else if (target > compareValue) {
        lowIndex = midIndex + 1;
      } else {
        highIndex = midIndex - 1;
      }
    }
    return undefined;
    }
half2(array, target);

算法优化小技巧

优化一些判断(自己的经验)

  • 例如添加边界值,有效更快速的结束,小场景的优化,不能质变但是有时候有意向不到的效果。
  if (targetList[0] == target || targetList[length - 1] == target) {
          return compareValue;
        }

总结

  1. 算法出现本身就是计算资源的稀缺性导致的。
  2. 算法时程序的逻辑基础,最小单位之一,任何框架和库的使用都是基于场景的扩展。
  3. 算法的抽象就是数学,一定要用数学来表达你的算法,你的任何一个观点。

dianzang.png