啥是复杂度?

169 阅读5分钟

前言

开篇两个问题。

问题A - 什么是算法?

回答:算法的本意是问题的解法。

问题B - 程序员为啥要学算法?

回答:因为程序员要解决问题。

 

再来两个问题。

问题C - 什么是数据结构?

回答:数据的结构。定义了结构,就能利用结构做一些理所应当的操作。

例如:队列的先进先出、栈的先进后出。

 

问题D - 什么是程序员平时口中说的算法?

回答:是用于处理数据的,配合数据结构使用的解决问题的经典套路和思想。

 

回到问题B, 程序员为啥要学算法?(针对问题D中狭义的算法定义)

回答:因为效率高。

 

算法的目的,就是写出高效的代码。

 

如何判断一个算法的好坏?

空间复杂度和时间复杂度

 

什么是高效?

用最少的资源来完成任务就是高效。

 

空间复杂度就是衡量程序使用内存空间的程度。

时间复杂度就是衡量程序使用时间的程度。

 

为啥不直接使用内存空间&执行时间来判断算法是否高效?

因为,影响因素太多,不能直接表现这段算法的性能。

影响算法执行时间的因素包括:

1、算法本身选用的策略;

2、问题的规模;

3、书写程序的语言;

4、编译产生的机器代码质量;

5、机器执行指令的速度等。

6、具体执行时的随机性。

 

也就是说,只有因素1和算法有关。另外因素6只要是玩过LeetCode 都会明白我在说什么。

(不过想了想,因素6和因素2好像是一回事。哈哈)

 

注意:数据结构和算法严格来说是和语言无关的。你去LeetCode 上面玩一下你就会知道,同样的算法,使用Java、C、Kotlin来写,执行时间天差地别。

 

算法复杂度才是真正衡量算法质量的标准。

 

大O表示法

我们通常使用大O表示法来表示算法的复杂度。

其实大O表示法的作用就是去除项

简单来说,

举例,

一个算法的时间复杂计算次数和规模n的函数关系是:

f(n) = 2n^2+4n+8.

去除项:

  1. 只保留最高阶。
  2. 去除和最高阶相乘的常数。

O(n) = n^2;

 

空间复杂度

空间复杂度就是衡量程序使用内存空间的程度。

使用内存空间好比花钱,花最少的钱做最多的事情,就是提升空间复杂度的目的。

不过随着硬件技术的发展,计算机的内存早就从128MB发展到了PC端最大内存是128G!

(装机猿给半佛装的那台主机就是这个内存配置。)

就是手机内存8GB也是标配。

 

内存空间的大小很多时候已经不再是问题。

甚至很多所谓程序员对于内存泄漏都完全不在意,甚至是不知道。

之后,我会单独用一篇文章讲讲内存的重要性。

现在,我们暂时可以认为,只要我们不写内存泄漏的代码,基本上可以不用在意内存空间。因为用不完。

 

甚至,很多算法就是用空间换取时间的(当然,也有时间换空间的)。

空间富余而时间有限,所以我们自然要换取更加珍贵的时间。

复习一下算法的目的是高效。

 

强调一下,时间复杂度不是用来计算程序具体耗时的,空间复杂度也不是用来计算程序实际占用的空间的。

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,也是问题规模的趋势描述。

 

空间复杂度常用为O(1)、O(n)、O(n^2)

 

举例:

O(1)

int i = 1
int j = 1
int k = 1
i = j + k 
j = k+1
k +=1 

上面的这段代码,空间复杂度就是O(1)。他代表他的内存空间的使用就是一个常量。不会随着规模加大而逐渐加大。

O(n)

val result = Array(2*n,{0})

上面这段代码,空间复杂度是O(n)。因为数据规模n越大,占用空间越多。基于大O表示法的规则,空间复杂度为O(n)

O(n^2)

val result = Array(2*n*n,{"A"})

不解释,懂的都懂。

时间复杂度

“等了好久终于等到今天~~”

上文说到,空间复杂度在内存空间充足的今天不是一个需要太过在意的东西。

所以,时间复杂度就成为了算法的最关键指标。

 

常见的时间复杂度量级有:

  • 常数阶O(1)
  • 对数阶O(logN)
  • 线性阶O(n)
  • 线性对数阶O(nlogN)
  • 平方阶O(n²)
  • 立方阶O(n³)
  • K次方阶O(n^k)
  • 指数阶(2^n)

从上至下依次的时间复杂度越来越大,执行的效率越来越低。

常数阶O(1)

var n = 2
var j = 0
n = n + j
j = j++

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,无论程序有多少行,都用O(1)来表示它的时间复杂度。

对数阶O(logN)

var i = 1
val n = 8
var j = 0
while(i<n){
   i = i*2
   j++
   println("$j")
}

N = a^x ; x就是以a为底,N的对数。a叫底数,N叫真数。

在上述例子中,a就是2,N就是8.x = log(2)8 , 基于大O表示法,a、N都是常数,不显示。

所以,表示为O(logN)。

线性阶O(n)

var n = 8
for (i in 0..n) {
   println("$i")
}

基于变量n的单循环就是典型的O(n)

线性对数阶O(nlogN)

var n = 8
for (m in 0..n) {
     var i = 1
     while (i<n){
          i = i*2
          println("$m:$i")
     }
        
}

将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。

平方阶O(n²)

var n = 3
for (m in 0..n) {
    for (i in 0..n) {
         println("$m - $i")
    }
        
}

双重循环。

 

立方阶O(n³)K次方阶O(n^k)

三重循环和K重循环。

 

指数阶(2^n)

 fun exponent(int n):Int {
    if (n == 0) return 1
    return exponent(n-1) + exponent(n-1)
}

后记

复杂度速查表 - liam.page/2016/06/20/…