学会对复杂度的计算与优化,提高程序执行效率

复杂度一词在程序的世界里经常听到,最常见的就是对于一个排序算法,我们都会去评估它的时间复杂度和空间复杂度。今天我们就来谈谈对复杂度的理解,这也是作为开发人员必备的技能之一。

什么是复杂度

复杂度是代码运行效率的重要度量因素,它直接影响了我们所开发出来的程序的运行效率,如果一个程序运行卡的要命,甚至出现死机的情况,我相信没有愿意去体验。所以说降低程序的复杂度是非常有必要的。

复杂度的分类

复杂度在程序中分为时间复杂度和空间复杂度两种。

  • 时间复杂度:顾名思义就是程序锁运行的时间。
  • 空间复杂度:程序运行所需要占用存储空间大小的度量。

对于复杂度的表示通常使用大写的O表示,例如:O(1),O(n),O(n²)…

复杂度的运算规则

  • 复杂度于具体的常系数无关
    例如 O(n) 与 O(2n) 他们表示相同的复杂度,O(2n) = O(n + n) = O(n) + O(n),一段O(n)复杂度的代码如果连续执行两遍,其复杂度是一样的。

  • 多项式的复杂度相加时,选择高的作为结果
    例如 O(n) + O(n²) 和O(n²) 表示同样的复杂度

  • O(1)复杂度
    O(1)是一个特殊的复杂度,它与输入量n无关,例如处理5条数据需要消耗 5个资源,处理100条数据还是只需要消耗5个资源。

复杂度的计算

这里我将通过一个例子来更好的理解计算的过程。

假如有一个数组 a = [1, 2, 3, 4, 5] ,现在我想得到它逆序后的结果。

我的思路是新建一个等长度的数组 b ,然后通过 for 循环,将 a 中的每个元素逐个逆序放入b中,就像下面这样。

代码如下:


function fun() {
  let a = [1, 2, 3, 4 , 5]
  let b = new Array(a.length).fill(0)
  for (let i = 0; i < a.length; i++) {
    b[a.length - i - 1] = a[i];
  }
  return b
}

这个过程中经历了一次 for 循环,循环次数为 5 ,这里可以看做是一个变量 n ,那么该过程的时间复杂度就为 O(n)。

对于空间复杂度的计算,该过程中定义了一个新的数组 b ,长度等同于输入数组的长度,也就是 n ,那么在空间资源上需要分配 n 个空间,可以得到空间复杂度也是 O(n)。

降低复杂度

还是刚才这个例子,有没有其他的算法可以降低复杂度呢,当然有更佳的方案。

对于这个逆序问题,还可以采用二分的思路,定义一个缓存变量 tmp ,将第一个元素与最后一个元素进行交换,然后将第二个元素与倒数第二个元素交换,直到进行到数组长度一半的时候结束。

代码如下:

function fun() {
  let a = [1, 2, 3, 4 , 5]
  for (let i = 0; i < a.length/2; i++) {
    let tmp = a[i]
    a[i] = a[a.length - 1 - i]
    a[a.length - 1 - i] = tep
  }
  return a
}

这种算法同样也是一个for循环,但是循环次数减少了一半,时间复杂度变成了 O(n/2)。但是根据复杂度与具体的常系数无关的性质,这段代码的时间复杂度还是 O(n)。

对于空间复杂度呢,我们只额外定义了一个tmp变量,无论我们输入的数组长度是多少,永远都只需要这一个额外的变量,与输入内容无关,那么空间复杂度即为O(1)。

通过这个问题可以看出,对于同一个问题,如果采用不同的算法结构,它的时间和空间的消耗是不同的,当数据量比较庞大的时候,降低它的空间复杂度和时间复杂度,在效率上会得到很明显的的提升效果。

分析冒泡排序

冒泡排序是最常见的排序算法之一,这里直接上代码


var arr = [3, 5, 6, 7, 1, 9, 2]function bubbleSort(arr) {
    var i = arr.length, j;
    var tempExchangVal;
    while (i > 0) {
        for (j = 0; j < i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                tempExchangVal = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tempExchangVal;
            }
        }
        i--;
    }
    return arr;
}

冒泡排序外层需要进行 n - 1 次循环,内层每次需要进行 n - i 次比较,每次比较需要移动记录三次来达到交换记录位置。当初始数据全部为逆序时,这是最坏的情况,比较次数和移动次数均达到最大值:

因此冒泡排序的最坏情况下的时间复杂度为O(n²)。

若初始数据是正序,只需要一趟就可以完成排序,这是最好的情况,比较次数和移动次数均达到最小值:

因此冒泡排序的最坏情况下的时间复杂度为O(n)。综上可得,冒泡排序的平均时间复杂度为 O(n²)。

在空间上只用到了一个额外的中间变量,与输入量无关,则空间复杂度为O(1)。

复杂度的优化与转换

通过前面的一些列的讲解,我们知道了复杂度分为时间复杂度和空间复杂度,对于同一个问题采用不同的编码方式可以改变复杂度。

时间复杂度消耗的是时间,空间复杂度消耗的资源。对于资源来说我们可以通过提高计算机性能,提高存储空间来满足需求;但是时间的话是固定的,无法使用金钱买到。如果你开发了一个软件,每次操作都需要等待大量的时间,估计没有用户愿意去接受,从此可以看出时间复杂度更为重要。

对复杂度的优化可分为3个阶段

  • 暴力解法:不考虑时间和空间的约束,只要完成目的就行
  • 处理无效操作:将代码中的无效计算、无效存储等内容删除
  • 复杂度转换:设计合理的数据结构,实现时间复杂度与空间复杂度之间的相互转换

来看个例子,在一个数组中找出出现次数最多的数字。

首先来看看暴力解法,内外两层循环,外层循环遍历每个数字,内层循环统计每个数字出现的次数,并记录下已统计过的数字中出现次数最多的数字。

function nummax() {
  var a = [ 1, 2, 3, 4, 5, 5, 6 ];
  var val_max = -1;
  var time_max = 0;
  var time_tmp = 0;
  for (var i = 0; i < a.length; i++) {
    time_tmp = 0;
    for (var j = 0; j < a.length; j++) {
      if (a[i] == a[j]) {
        time_tmp += 1;
      }
      if (time_tmp > time_max) {
        time_max = time_tmp;
        val_max = a[i];
      }
    }
  }
  return val_max
}```
这种暴力解法的时间复杂度为O(n²)。

我们能否只通过一次循环就可以找到答案呢?在ES6中,我们可以先声明一个Map对象 map,然后遍历数组,判断在map中是否有该属性,若无,则把该值作为Key存入map中,并赋值为1;若有,则将它的值加一。通过一次循环就可以统计出每个数字出现的次数,然后再通过一次循环找到map中的最大值。
```javascript
function nummax() {
  var a = [ 1, 2, 3, 4, 5, 5, 6 ];
  var map = new Map()
  for(var i = 0; i < a.length; i++){
    if(map.has(a[i]))
      map.set(a[i], map.get(a[i]) + 1)
    else
      map.set(a[i], 1)
  }
  var time_max = 0;
  var time_tmp = 0;
  for(key in map){
    if(map.get(key) > time_tmp){
      time_tmp = key
      time_tmp = map.get(key)
    }
  }
  return time_tmp
}

虽然这种方式也要经历两次循环,但是他们不是嵌套关系,在最坏的情况下,假如每个元素只出现了1次,那么map的长度为n,两次循环得到的时间复杂度为O(n + n) = O(n),空间复杂度为O(n)。

两种算法的时间复杂度从O(n²)到O(n)得到了很大的提升,但是牺牲了空间复杂度,综合来看这种交易得到的最终效果还是很划算的。

通过这篇文章的介绍,希望可以让你对编码结构有了更加深入的了解,提高执行效率。
如果觉得我的文章还不错的话,可以点个关注,或者关注我的个人公众号【前端筱园
我的个人网站:www.dengzhanyong.com
所发布的内容CSDN、公众号、个人网站同步更新

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端筱园

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值