论思维的重要性

论思维的重要性

从3道面试题讲起:

1.求和

求1-100的数字的总和

光看题目很简单,最简单的方法暴力破解,从1累加到100不就行了么。

function calc(n){
    var sum = 0
    for(let i=1;i<n+1;i++){
        sum+=i
    }
    return sum
}

那如果将数字扩大到10000000001呢,结果发现计算机半天算不出来。我上学那会还有个算法,说是1+99,2+98,3+97…这样可以将复杂度缩减一半,可貌似计算量依然很大。如果数字更大呢,再乘以10呢,又会陷入半天算不出来的情况,这时候我想到了数学的解决方法:
数列的前n项和

你没看错,就是数列的前n项和公式,转化为代码:

function calc(n){
    var sum = n/2*(1+n)
    return sum
}
> calc(100)
> 5050
> calc(100000000000000000000001)
> 5.0000000000000006e+45
> calc(1000000000000000000000000000000000000000000000000001)
> 5e+101

这时候,你可以计算任意大的值,计算机都能给你瞬间算出来。

2.斐波那契数列

学计算机的应该都知道这东西,样子如下:

1 1 2 3 5 8 13 ...

求第n项的值

通过观察,很容易发现规律, 第n项的值=前一项+前前项的值:

f(n) = f(n-1) + f(n-2)

转为代码:

function getN(n){
    if(n==0){
        return 0
    }
    if(n==1){
        return 1
    }
    return getN(n-1) + getN(n-2)
}

该函数用了递归,我试着求前面7项,都得出正确结果,速度很快,但是,当我求99项的时候,计算机没有给出结果,直接卡住了。递归有其缺陷性,当递归调用次数大的时候,会跟循环一样,计算结果半天出不来,当我尝试着求第999999项的时候,直接给我报了个错:

> getN(999999)
> Uncaught RangeError: Maximum call stack size exceeded

这个错的意思是,超出了最大调用栈的限制,我们都知道,每个函数执行的时候都会推入执行栈中,而递归求值,每次都依赖前一个函数求值,前一个函数求值仍然依赖前一个函数求值,这样的结果就是,临时缓存所有函数调用,直到达到函数执行栈的最大尺寸限制,抛出错误。

这时候我想到,把递归改成循环把,循环的话就不会保留这么多函数调用了,也不会存在爆栈问题:

function getNByLoop(n){
    let n1 = 0
    let n2 = 1
    let count = 0
    if(n==0) return 0
    if(n==1) return 1
    let i = 2
    while(i <= n){
        count = n1+n2
        n1=n2
        n2=count
        i++
    }
    return count
 }

这时候我再尝试之前的计算:

> getNByLoop(99)
> 218922995834555200000
> getNByLoop(99999)
> Infinity

这些计算也是瞬间得出结果,尽管99999没有给出具体值,但不是因为算不出来,而是因为超过计算机能表示的最大数值,索性用趋近于无穷表示。

但是我们思考一个问题,如果这个数仍然扩大十倍百倍,甚至万倍,计算机还能算出结果吗。恐怕同样和之前示例一样卡住。

因此我去查了下数学的通项公式:
通项公式

转化为计算机语言:

function calc(n){
    return parseFloat(1/Math.sqrt(5)*(Math.pow((1+Math.sqrt(5))/2,n) - Math.pow((1-Math.sqrt(5))/2,n))).toFixed(0)
}

由于去掉了循环,计算速度大幅度提升,只要数值在计算机表示范围内都可以瞬间得出结果。


3.求最小硬币数

有面值1块、4块、5块的硬币,求组成n块的最小硬币数

拿到题目首先分析题目,怎样才能得到最小硬币数,经过一番思考不难想到,只要先加较大面值的硬币,自然就能尽可能减少总的硬币数,面值越大,加的硬币数越少。于是很容易得出:

function min(n){
    let count = 0  // 最小硬币数
    while(n>0){
        count++
        if(n-5>=0){     // 先求5的面值最大数量
           n-=5;
           continue 
        }
        if(n-4>=0){     // 剩下的再求4和1面值的数量
           n-=4;
           continue 
        }
        if(n-1>=0){
           n-=1;
           continue 
        }
    }
    return count
}

我网上搜索了下别人的解法,俗称暴力解法,遍历每一种可能性:

function getNum(N){
     let n1 = 1;
     let n2 = 4;
     let n3 = 5;
     let last=N;
     for(let i=0;i<=N;i++){                 // 面值1可能的硬币数
          for(let j=0;j<=N/4;j++){          // 面值4可能的硬币数
               for(let k=0;k<=N/5;k++){     // 面值5可能的硬币数
                    if(n1*i+n2*j+n3*k===N){ // 各种面值加起来等于n的所有可能
                       if(i+j+k<last){      // 小于上一次硬币数就覆盖
                            last = i+j+k;
                       }
                    }
               }
          }
     }
     return last;   // 最终算出的last就是最小的硬币数
}

这个方法的效率,我看到的时候是头皮发麻的,于是我实验了一下

> getNum(100)
> 20
> getNum(1000)
> 200
> getNum(10000)
> 2000 # 这里卡了半天才出来

暴力解法只适合解决数值较小的情况,根本无法推广到任意值解法。当然当面值足够大的时候,循环似乎也不是最佳解决思路,尽管,已经可以解决大部分问题了。

这时候我认真思考了下,从数学的角度来说,可以简化吗?

循环的时候先累加5的硬币数,再累加4的硬币数,最后累加1的硬币数,可我为什么要累加呢?为什么不能跳过这个过程,直接计算呢?于是我再次精简计算:

function min(n){
    let n1,n4,n5 // 面值1块,4块,5块的硬币数
    n1=n4=n5=0
    let rest = n%5 // 余数
    if(rest>=0){ // 对5求余有余数,证明数值大于等于5可以取得面值5的硬币数
        n5 = parseInt(n/5)
    }
    if(rest%4>=0){
        n4 = parseInt(rest/4)
        rest = rest%4
    }
    n1 = rest  // 剩下的余数就是面值1的硬币数
    return n1 + n4 + n5 // 所有面值最小硬币数 
}

这时候的算法,也把循环给去掉了,基本可以推导任意值的计算了,当然parseInt计算大数仍有问题,理论上这个思路是没问题,

> min(1000000000000000000000)
> 200000000000000000000 # 瞬间得出值

如果是循环版的算法,当计算到10000000000的时候,都很难得出答案了。


结论

通过上面的3道面试题,能够总结出的思维,也就是算法的重要性,而算法很多时候又跟数学有点关系,这告诉我们,有时候,思考算法的时候,其实也可以从数学的角度去思考,而不要过于依赖计算机的算力,如果能通过动手计算得出的算法,远好过依赖计算机算力的暴力解法,枚举所有可能性永远是下下策。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值