数学篇:聊聊迭代,如何不用语言提供的函数实现平方根?

454 阅读3分钟

什么是迭代?

迭代其实就是不断的用旧的变量的值,递归的计算出新的变量值的过程。迭代的思想比较容易通过计算机语言中的循环来实现,通过循环语句,让计算机重复的执行迭代中的递推步骤,直到推导出变量的最终值。

举个例子:如果现在我们有30个格子,我们需要向第一个格子里面房 1 颗糖,向第二个格子放 12=2 颗糖,向第三个格子放 22 颗糖,以此类推,每格子都比前一小格放多一倍的糖,那么我们需要多少颗糖能放满这 30个格子呢?

要解决上面的问题,就需要用到迭代法了。如果将前一个格子的糖数记作 Xn−1,当前格子的个数记作 Xn,那么这两个变量之间有什么关系呢? 截屏2021-04-06 下午7.26.16.png

哈哈😜,又发现啥规律没?通过循环语句就可以很好的解决递归问题,计算机可以很好的帮我们解决递归问题。下面是我用js实现的计算代码:

function getSugarCount(grid){
  let count = 1;
  let curCount = 1;

  for(let i = 0; i <= grid; i++){
    curCount *=2;
    count += curCount;
  }
  return count;
}
getSugarCount(64) // 73786976294838210000

执行上面的程序我们会得到最终的结果 73786976294838210000 是不是超乎你的想象,64个格子竟然需要那么多颗糖!!!

迭代法有那么应用呢?

其实迭代法不论实在计算机领域还是在数学领域的应用都是十分广泛的。那么主要有那几方面的应用呢?

  • 在一定范围内查找目标值,这方面的应用比如我们非常熟悉的 二分查找,不知道你还有没有印象。
  • 求数值的精确或者近似解,典型的方法包括 二分法牛顿迭代法
  • 机器学习算法中的迭代,相关的算法或者模型有很多,比如 K- 均值算法(K-means clustering)、PageRank 的马尔科夫链(Markov chain)、梯度下降法(Gradient descent)等等。迭代法之所以在机器学习中有广泛的应用,是因为很多时候机器学习的过程,就是根据已知的数据和一定的假设,求一个局部最优解。而迭代法可以帮助学习算法逐步搜索,直至发现这种解。

接下来我们再来举两个例子:

二分查找

首先举个我们熟知的 二分查找 吧,比如现在我们们有一组有序的数组(这里注意啦,二分法中很关键的前提条件是,所查找的区间是有序的。这样才能在每次折半的时候,确定被查找的对象属于左半边还是右半边),比如,[1,2,5,7,8,11,15,18,21],现在我们要查找数字 2 所在的位置,我们应该怎么做呢?

你可能会想,我可以把数组遍历一遍不就找到了吗?是的当然可以,但是这样做的时间复杂度是多少呢?很明显是 如果数组中第一个元素正好是要查找的数字 2,那就不需要继续遍历剩下的 n-1 个数据了,那时间复杂度就是 O(1)。但如果数组中不存在数字 2,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。所以,不同的情况下,这段代码的时间复杂度是不一样的。平均情况下就是 O(n)。这里的复杂分析过程可以参考什么是最好、最坏、平均、均摊时间复杂度?(算法)

那如果我们使用二分查找法呢,时间复杂度就变成了 O(logn), 具体进行二分查找的过程如下:

将数组折半,得到数组中间的数值于 2 比较,比 2 大则继续在数组的右半边查找,比 2 小则继续在数组的左半边查找,不断的迭代式地查找,直到找到我们要查找的数字 2。

查找过程参考下图 截屏2021-04-06 下午8.13.23.png

 function binarySearch(arr, target) {
    let leftIndex = 0;
    let rightIndex = nums.length - 1;
    let ret = -1;//定义返回结果
    while (leftIndex !== rightIndex) {
      let mid = Math.round((leftIndex + rightIndex) / 2)
      if (nums[mid] === que) {
        ret = mid
        break;
      } else if (nums[mid] > que) {
        rightIndex = mid
      } else {
        leftIndex = mid
      }
    }
    return ret
  }
  
  binarySearch([1,2,5,7,8,11,15,18,21], 2) // 1

求方程的精确或者近似解 之 求某个给定正整数 n 的平方根

假设有正整数 num,这个平方根一定小于 num 本身,并且大于 1。那么这个问题就转换成,在 1 到 num 之间,找一个数字等于 num 的平方根。采用迭代中常见的二分法。每次查看区间内的中间值,检验它是否符合。

举个例子,假如我们要找到 10 的平方根。我们需要先看 1 到 10 的中间数值,也就是 11/2=5.5。5.5 的平方是大于 10 的,所以我们要一个更小的数值,就看 5.5 和 1 之间的 3.25。由于 3.25 的平方也是大于 10 的,继续查看 3.25 和 1 之间的数值,也就是 2.125。这时,2.125 的平方小于 10 了,所以看 2.125 和 3.25 之间的值,一直继续下去,直到发现某个数的平方正好是 10。

具体步骤可以参考下图

截屏2021-04-07 下午7.32.03.png

下面我们用js来实现它:

/**
* @Description: 计算大于 1 的正整数之平方根
* @param num- 待求的数, deltaThreshold- 误差的阈值, maxTry- 二分查找的最大次数
* @return 平方根的解
*/
function getSqureRoot(num, deltaThreshold, maxTry){
  if (num <= 1) {
    return -1;
  }
  let min = 1.0;
  let max = num;
  for(let i = 0; i < maxTry; i++){
    let middle = (min + max)/2;
    let square = middle * middle;
    let delta = Math.abs(square / num - 1);
    if(delta <= deltaThreshold) {
      return middle;
    } else {
      if (square > num) {
        max = middle;
      } else{
        min = middle;
      }
    }
  }
  return -2;
}
getSqureRoot(10, 0.000001, 10000) // 3.1622767448425293

第一,使用了 deltaThreshold 来控制解的精度。虽然理论上来说,可以通过二分的无限次迭代求得精确解,但是考虑到实际应用中耗费的大量时间和计算资源,绝大部分情况下,并不需要完全精确的数据。

第二,使用了 maxTry 来控制循环的次数。之所以没有使用 while(true) 循环,是为了避免死循环。虽然,在这里使用 deltaThreshold,理论上是不会陷入死循环的,但是出于良好的编程习惯,还是尽量避免产生的可能性。

下面总结下迭代法的步骤

迭代的基本步骤

  • 确定用于迭代的变量
  • 建立迭代变量之间的递推关系
  • 控制迭代的过程