重要性
在日常生活中我们经常需要完成某些任务。比如出租车司机,假设出租车司机只按照运了多少单来挣钱,那么当你作为出租车司机时肯定希望每一单完成的时间越短越好。同样的,我们在执行\采用某些代码时,考虑到执行时间越长,花费的成本也就越高。或者我们希望有一个标准,能够证明自己所写的代码效率很高,而不是写出实现目标功能的所有可能的代码组合再依次去执行、尝试,统计每个代码所花费的时间并进行比较,才能选出效率最高的代码(给程序员减负)。
Table 1. 运行时间
数据量 | 10 | 10000 |
---|---|---|
算法1 | 0.00s | 1h |
算法2 | 0.05s | 15s |
实际上如果条件允许的话,如果我们采取前文所说的将不同的代码分别执行(无数次)并比较运行时间的方法,这也可以选出效率更高的代码(如上表,一般我们会认为算法2的效率更高,因为在数据量小的时候所有算法的执行效率差异不大,如上表的0.05s;而在数据量较大时,算法的效率差异极大,差了接近1h)。
我们会发现比较代码效率的一个条件是数据量的大小一致。并且越大越好,因为数据量越大算法效率的差异就体现的越明显。
举一个简单的例子:
for (int i = 0; i < N; i++)
for (int j = i + 1; j < N; j++)
if (A[i] > A[j])
swap(A[i], A[j]);
当我们给定一个数据集(总共有 N 个数据,并且每一个数据都已知),这时我们完全可以计算出每一行代码执行了多少次,然后我们让N增大(无穷),这样我们就有了比较不同算法之间效率差异的一个前提。
然后,让我们回到问题:如何评价代码的运行效率?在关注这个问题的时候我们实际是在关注:在同样数据量的情况下哪个代码的运行时间最短。
如上例,第1行代码需要运行 N 次,第2行代码需要运行 N(N-1)/2 次,第3行代码也需要运行 N(N-1)/2 次,对第4行代码来说:如果数据是升序排列,那就不运行;如果数据是降序排列,那么需要运行 N(N-1)/2 次。
有人可能会想那我们把第4行代码取一个平均,当做运行次数不就行了么。然而实际上我们关注的是最坏情况 (有时根据实际情况也可以选择关注平均情况或者最好情况,后面会提到最好情况)。很容易理解,因为最坏情况下的运行时间造成的负面影响很大。比如我们使用某软件时,最坏情况需要10年才能打开(极端),这时即便是一个小概率事件,但一旦被我们遇到,也大概率会直接卸载软件。
因此上例中第4行我们取运行次数为 N(N-1)/2 次的情况,那么实际的运行次数
f
(
N
)
=
3
N
(
N
–
1
)
/
2
+
N
=
1.5
N
2
–
0.5
N
f(N) = 3N(N – 1)/2 + N = 1.5N^2 – 0.5N
f(N)=3N(N–1)/2+N=1.5N2–0.5N ,这样我们就在没有讨论现实中诸多影响因素的情况下得到了我们想要的答案。
只需要比较在相同的给定数据集并选择最坏运行情况下,每种算法运行次数的多少,就可以比较出各种算法运行效率的差异。
而在实际工作中代码量很大,运行次数也很多,根据我们前面所说的计算每种算法运行次数的方法似乎也十分复杂,因此我们还需要对前面的方法再进行简化。
根据我们学过的极限知识,我们可以知道
N
2
>
>
N
N^2>>N
N2>>N ,并且1.5 也没有那么重要了,因此我们可以假设
g
(
N
)
=
N
2
g(N) = N^2
g(N)=N2 ,此时我们就可以用
g
(
N
)
g(N)
g(N) 近似的反映
f
(
N
)
f(N)
f(N) 的数量级。
下面我们给出课本上的定义,引入新的记号 big-oh(大O记号)
big-O notation
上界
∀ N > N 0 ; f ( N ) < c ⋅ g ( N ) \forall N>N_0;f(N)<c \cdot g(N) ∀N>N0;f(N)<c⋅g(N)
记为:
f ( N ) = O ( g ( N ) ) f(N) = O(g(N)) f(N)=O(g(N))
我们还用上面的例子,
f
(
N
)
=
3
N
(
N
−
1
)
/
2
+
N
=
1.5
N
2
−
0.5
N
f(N) = 3N(N-1)/2 + N = 1.5N^2 - 0.5N
f(N)=3N(N−1)/2+N=1.5N2−0.5N 而
g
(
N
)
=
N
2
g(N) = N^2
g(N)=N2 ,我们可以说
f
(
N
)
=
O
(
g
(
N
)
)
=
O
(
N
2
)
f(N) = O(g(N)) = O(N^2)
f(N)=O(g(N))=O(N2)
同样的我们可以定义最好情况
big-omega notation
下界
∀
N
>
N
0
;
f
(
N
)
>
c
⋅
g
(
N
)
\forall N>N_0;f(N)>c \cdot g(N)
∀N>N0;f(N)>c⋅g(N)
我们引入 big-omega notation (大
Ω
\Omega
Ω记号)
f
(
N
)
=
Ω
(
g
(
N
)
)
f(N) = \Omega (g(N))
f(N)=Ω(g(N))
big-theta notation
如果我们给出的上界和下界恰好是同一个函数,例如上例中
f
(
N
)
f(N)
f(N) 的上下界都是
N
2
N^2
N2 ,因此
f
(
N
)
=
Θ
(
N
2
)
f(N) = \Theta (N^2)
f(N)=Θ(N2)
即
∀
N
>
N
0
;
c
1
⋅
g
(
N
)
<
f
(
N
)
<
c
2
⋅
g
(
N
)
\forall N>N_0;c_1 \cdot g(N)<f(N)<c_2 \cdot g(N)
∀N>N0;c1⋅g(N)<f(N)<c2⋅g(N)
我们引入 big-theta notation(大
Θ
\Theta
Θ记号)
f
(
N
)
=
Θ
(
g
(
N
)
)
f(N) = \Theta(g(N))
f(N)=Θ(g(N))