时间复杂度与空间复杂度的简单认识

543 阅读6分钟

算法初级 我们在写一段程序的时候,不仅仅需要实现它的需求,还需要去优化这段代码,其中包括时间复杂度和空间复杂度两个方面去考虑。包括在刷力扣的题目时,最后提交返回的数据也是根据时间复杂度和空间复杂度得到的结果。

复杂度

什么是时间复杂度和空间复杂度

复杂度是衡量代码运行效率的重要的度量因素。

算法复杂度分为时间复杂度空间复杂度

其作用:

  • 时间复杂度是指执行算法所需要的计算工作量;
  • 而空间复杂度是指执行这个算法所需要的内存空间。

(算法的复杂性体运行该算法时的计算机所需资源的多少上,计算机资源最重要的是时间和空间(即寄存器)资源,因此复杂度分为时间和空间复杂度。)

实际生活中的例子。某个十字路口没有建立立交桥时,所有车辆通过红绿灯分批次行驶通过。当大量汽车同时过路口的时候,就会分别消耗大家的时间。但建了立交桥之后,所有车辆都可以同时通过了,因为立交桥的存在,等于是消耗了空间资源,来换取了时间资源。

复杂度遵循的原则

通常,复杂度的计算方法遵循以下几个原则:

  • 首先,复杂度与具体的常系数无关,例如 O(n) 和 O(2n) 表示的是同样的复杂度。我们详细分析下,O(2n) 等于 O(n+n),也等于 O(n) + O(n)。也就是说,一段 O(n) 复杂度的代码只是先后执行两遍 O(n),其复杂度是一致的。
  • 其次,多项式级的复杂度相加的时候,选择高者作为结果,例如 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。具体分析一下就是,O(n²)+O(n) = O(n²+n)。随着 n 越来越大,二阶多项式的变化率是要比一阶多项式更大的。因此,只需要通过更大变化率的二阶多项式来表征复杂度就可以了。

值得一提的是,O(1) 也是表示一个特殊复杂度,含义为某个任务通过有限可数的资源即可完成。此处有限可数的具体意义是,与输入数据量 n 无关。

例如:

  • 你的代码处理 10 条数据需要消耗 5 个单位的时间资源,3 个单位的空间资源。
  • 处理 1000 条数据,还是只需要消耗 5 个单位的时间资源,3 个单位的空间资源。

那么就能发现资源消耗与输入数据量无关,就是 O(1) 的复杂度。

一段代码的复杂度

数组去重案例1

//时间复杂度为 n^2
let arr = [3, 1, 2, 5, 4, 9, 7, 2, 1, 5, 3];
function check(arr) {
   for (let i = 0; i < arr.length; i++) {
         let item = arr[i];
         for (let j = i + 1; j < arr.length; j++) {
             if (item === arr[j]) {
                arr[j] = arr[arr.length - 1];
                arr.length--;
                j--;
             }
         }
    }
    return arr;
}
console.log(check(arr));

这段代码中,我们采用了双层循环的方式计算:第一层循环,我们对数组中的每个元素进行遍历;第二层循环,讲拿出来的当前元素与后面的元素依次比较。由于是双层循环,这段代码在时间方面的消耗就是 n*n 的复杂度,也就是 O(n²)。

数组去重案例2

// 时间复杂度为n
let arr = [3, 1, 2, 5, 8,4, 9, 7, 2, 1, 5, 3,8];
function check(arr) {
    let obj = {};
    for (let i = 0; i < arr.length; i++) {
        //是否存在这一项
       let item = arr[i];
       if (obj[item] != undefined) {
             arr[i] = arr[arr.length - 1];
             arr.length--;
             i--;
       }
       obj[item] = item;
   }
   return arr;
}
console.log(check(arr));

这段代码中,定义了新的存储空间用来接收,只通过一次for循环来判断对象中是否已经存在该值,这段代码在时间方面的消耗就是 n 的复杂度,也就是 O(n),但是他有定义了一个新的存储空间,所以把减少的时间复杂度转移到了空间上

结论:

  • 一个顺序结构的代码,时间复杂度是 O(1)。
  • 二分查找,或者更通用地说是采用分而治之的二分策略,时间复杂度都是 O(logn)。这个我们会在后续课程讲到。
  • 一个简单的 for 循环,时间复杂度是 O(n)。
  • 两个顺序执行的 for 循环,时间复杂度是 O(n)+O(n)=O(2n),其实也是 O(n)。
  • 两个嵌套的 for 循环,时间复杂度是 O(n²)。

复杂度总结

复杂度通常包括时间复杂度和空间复杂度。在具体计算复杂度时需要注意以下几点。

  • 它与具体的常系数无关,O(n) 和 O(2n) 表示的是同样的复杂度。
  • 复杂度相加的时候,选择高者作为结果,也就是说 O(n²)+O(n) 和 O(n²) 表示的是同样的复杂度。
  • O(1) 也是表示一个特殊复杂度,即任务与算例个数 n 无关。

复杂度细分为时间复杂度和空间复杂度,其中时间复杂度与代码的结构设计高度相关;空间复杂度与代码中数据结构的选择高度相关。

如何降低复杂度

应该降低空间复杂度还是时间复杂度

代码效率的瓶颈可能发生在时间或者空间两个方面。

  • 如果是缺少计算空间,花钱买服务器就可以了。这是个花钱就能解决的问题。
  • 相反,如果是缺少计算时间,只能投入宝贵的人生去跑程序。即使你有再多的钱、再多的服务器,也是毫无用处。

相比于空间复杂度,时间复杂度的降低就显得更加重要了。=> 因此,你会发现这样的结论:空间是廉价的,而时间是昂贵的。

优化过程

  • 按照需求写出代码,不考虑复杂度的问题
  • 查看写好的代码中,删除冗余的、无效计算、无校存储的代码,降低复杂度
  • 设计合理数据结构,完成时间复杂度向空间复杂度的转移。

回顾上面的数组去重,首先根据需求实现功能,再去看里面冗余的代码以及看是否可以把时间复杂度转为空间复杂度

参考

通过学习拉勾 => 重学数据结构与算法 一课而做的笔记