目录
算法
算法(Algorithm) 是指用来操作数据、解决程序问题的一组方法。 —— 冰与火之歌:「时间」与「空间」复杂度
算法是一组完成任务的指令。 —— 《算法图解:像小说一样有趣的算法入门书》
算法在计算机领域中就是为了解决问题而指定的一系列简单的指令集合。 —— 算法时间复杂度、空间复杂度(大O表示法)
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。 —— 《大话数据结构》
综合以上四条定义可以看到,对于同一个概念,不同的人有不同的见解,而我也同样有自己的见解!
当然,我是站在巨人所有的前辈们(包括上述的几位前辈)的肩膀上,我的见解顶多算是换了一种说法,不过既然是学习笔记,自己理解了,那肯定是有收获的嘛!😁😁
先看个小故事:
一天下午,小红正在座位上写数学作业,但是她被一道复杂的几何题给难住了,已经苦苦思索了好久也没有找到一个好的计算方法。
这时,从旁边经过的小明发现了小红的苦恼,于是小明停了下来。
“小红,你是不是不会写这道题啊?”小明脸上带着坏坏的笑容。
“是啊,这道题我已经思考了有半个小时了,都不知道怎么算,小明你难道会这道题吗?”小红一脸期待地看着小明问道。
“那是,虽然我学习成绩一般,但是这道题的解法,我还真的会!嘿嘿!”小明一脸骄傲地说道。
“真的吗?可不可以教教我?”小红可怜巴巴地看着小明,一脸地期待。
“哇🤩🤩”,小明心里一颤,他何时被女孩子这样看过,顿时虚荣心得到了前所未有地满足,一口答应了小红。
于是,班上出现了奇怪的一幕:学渣小明在给学霸小红讲题!
…
小明:你看,这里有个角,我们在这里画条线,把这个图形切分开
小明:第二步,我们把切分后的这个图形平移一下,挪到这个地方,是不是就变成了一个规则的图形
小明:第三步,我们带入公式A,来计算这个图形的…
…
一番讲解,在小明的帮助下,小红成功地解出了这道题,而小红从此也对小明刮目相看。
不过奇怪的是,在之后的 N 多年里,小红每次一见小明,脸都会变得红通通的。😊😊
从上面这个小故事,我们可以提炼出几个关键字:算、解法、第二步、第三步、解出!
再类比到我们计算机领域的算法这个词,作为一个“法”,算法也是用来解题的,不过不仅是用来解数学题,还可以用来解决一些生活中、实际中碰到的问题。同时,算法也包括很多步骤,有第一步、第二步、第三步…不过,和解数学题不一样,算法的每个步骤中需要用到的不是公式,而是一个个程序指令!但是,本质上,程序指令也是由一个个的或简单、或复杂的公式构成的!
那么,总结一下,算法是用来解决某个问题(任务)的一组方法,在计算机领域中,它被描述为一系列的指令集合。
同时,还有个简单粗暴的理解,算法是由一组数据得到另一组数据的过程。
算法的特性
- 输入输出
- 有穷性:在执行有限步骤后自动结束而不会出现死循环。【虽然有限,但需要执行 20 年的算法也可以当作死循环了】
- 确定性:算法不会出现二义性
- 可行性
影响算法优劣的因素
我们说了算法是用来解决问题的一组方法,那么,既然是方法,那就肯定有难易之分!
就像解数学题一样,解法 A 只用了两个公式,两行就搞定了;但解法 B 却用了23个公式,写了足足一页纸才把计算步骤写完!
虽然同样解出了正确答案,但毫无疑问,解法 A 要比解法 B 简单得多,不仅是占的解题空间少,而且还能节省出更多的时间去继续做后面的题!
换句话说,这道袍一脱(解法 A 一用),小红可就高攀不起了!
而同样的,对于一个算法来说,同样有优劣(难易)之分,而影响算法优劣的因素,主要有两个:
- 时间复杂度:执行当前算法所消耗的时间
- 空间复杂度:执行当前算法需要占用的内存空间
时间复杂度
那么,既然说时间复杂度是指执行当前算法所消耗的时间,是不是说我们评判一个算法的优劣,只需要看需要用几秒钟、或者几分钟能跑完这个算法,看最终哪个算法用的时间短,就说明它更优秀?
当然不是!
设想一下以下几种情况:
- 对于同一个算法 A,小明用了一台30年前的老古董计算机去执行,花费了10分钟;小亮用了一台最新款的 Mac 计算机去执行,花费了10秒
- 同样使用最新款的Mac计算机,小明使用算法A花费了2分钟,处理了1000万条数据;小亮则使用算法B花费了1分钟,处理了200万条数据
- 有一个算法C,小亮用最新款的Mac计算机去跑它,但是已经跑了8个小时了还没完;小明瞅了一眼小亮,心里说道:“傻逼!”😂
由此可以看出,衡量一个算法的优劣,仅仅去看它运行的时间长短,是非常不合理的!
那,怎么搞?—— 当然是用大O表示法!
大 O 表示法
大 O 表示法的具体定义和数学概念我就不介绍了,太过复杂、抽象(感兴趣的可以自己去查),我主要用一个例子来简单说明怎么使用【大 O 表示法】!
先来看一段简单的代码:👇
int total = 0;
for (int i = 0; i < n; i++) {
total += i;
}
我们计算一下上面👆这段代码的执行时间:
- 第一行
int total = 0;
为赋值语句,记 1 个单位时间- 第二行 for 循环中:
赋值语句记 1 个单位时间
比较语句记 n+1 个单位时间【假设 n = 4,0<4、1<4、2<4、3<4、4==4 共 5 次操作】
自增语句记 n 个单位时间【最后一次比较不满足循环条件,不进行自增】- 循环体内,
total += i;
执行了 n 次,记 2n 个单位时间【total += i;
→ 先加 i,再赋值】
合计 4n + 3 个单位时间
而用【大O表示法】来表示以上代码的时间复杂度,就是 O(4n + 3)
,而当 n 趋向于无限大时,4n+3
≈ n
,也因此,以上代码的时间复杂度为 O(n)
。
常见的时间复杂度量级
常数阶 O(1)
无论代码执行多少行,只要代码中没有循环结构,那么时间复杂度就是 O(1),也就是常数阶。
int a = 1;
a = a + 2;
int b = a + 2;
线性阶 O(N)
如果代码中存在一个循环体,循环 N 次,则时间复杂度为 O(N),也就是线性阶。
int sum = 0;
for(int i = 0; i < N; i++) {
sum = sum + i;
}
对数阶 O(logN)
如果代码中存在一个循环体,循环 logN 次,则时间复杂度为 O(logN),也就是对数阶。
循环 N 次,直接看代码就可以很轻易地数出来,但循环 logN 次,这怎么数?看👇代码!
int i = 1;
while(i < N) {
i = i * 2;
}
假设,真正运行👆这段代码时循环了 X
次就退出了 while 循环,那么,也就是 i * 2X >= N
,我们取临界值,只需要 i == N
的时候就可以退出循环了,也就是求解 i * 2X=N
,其中 i = 1
,解得 X = log2N
。
而在大O表示法中,我们一般默认对数 logaN
的底数为 2,记做 logN
。
因此,以上代码的时间复杂度即为 O(logN),即对数阶。
线性对数阶 O(NlogN)
线性阶是将循环体循环了 N
次,对数阶是将循环体循环了 logN
次,那么,不妨大胆地猜一下,线性对数阶会不会是将循环体循环了 N * logN
次呢?
如果代码中存在一个双重循环,分别循环 N 次和 logN 次,则时间复杂度为 O(NlogN),也就是线性对数阶。
int sum = 0;
for(int i = 0; i < N; i++) {
int j = 1;
while(j < N) {
sum = sum + j;
j = j * 2;
}
}
平方阶 O(N2)
搞清楚上面两个,平方阶简直不要太简单!
如果代码中存在一个双重循环,每层循环都循环了 N 次,则时间复杂度为 O(N2),也就是平方阶。
int sum =0;
for(int i = 1; i <= N, i++) {
for(int j = 1; j <= N, j++) {
sum = i * j;
}
}
根据平方阶可以推出立方阶 O(N3),k 次方阶 O(Nk),或者 O(N * M)。
指数阶 O(2N)
指数阶的算法基本上已经很烂了,除非数据量很小,否则千万不要使用指数阶算法。
int fibRecur(N) {
if(N <= 1) { // 递归终止条件
return N;
}
return fibRecur(N - 1) + fibRecur(N - 2); // 向下递归
}
以上👆代码是一个递归算法的斐波那契数列解法,其时间复杂度为 O(2N),也就是指数阶。
怎么证明的我就不放了我数学一般😂,网上有很多大佬对其进行了证明,感兴趣的可以自行查找。
阶乘 O(N!)
这个比较具有代表性的代码我没找到😂,不过,无所谓了,估计大多数人也碰不到这种烂到极致的代码!
时间复杂度对比
O(1) < O(logN) < O(N) < O(NlogN) < O(N2) < O(N3) < O(2N) < O(N!)
对于时间复杂度的对比,我自己做了一个图,虽然比较真实,但是在复杂度走势上不是很清晰,因此我将网上流传的一个比较火的对比图也放了上来,可以作为参考!
最坏情况与平均情况
就像在家里找东西一样,有时运气好瞬间就找到了,有时候运气不好,找完客厅找卧室、找完卧室找厨房、找完厨房找卫生间,花费一番大力气才最终找到。
而算法也一样,有时运气好,花费的时间很少,有时候就需要花费很长时间,而这就是最坏情况运行时间。
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。一般在没有特殊说明的情况下,都是指最坏时间复杂度。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。可现实中,平均运行时间很难通过分析得到,一般都是通过运行一定数量的实验数据后估算出来的。
空间复杂度
前面说了时间复杂度,那么接下来来看看空间复杂度!
前面提到,空间复杂度是指执行当前算法需要占用的内存空间,也就是所需的内存的大小。
而和时间复杂度类似,空间复杂度也可以用大O表示法来表示。
空间复杂度 O(1)
一段代码,随着数据量的变化内存消耗不变化的时候,空间复杂度为常数,也就是 O(1)。
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
空间复杂度 O(N)
一段代码,随着数据量的变化内存消耗呈线性变化的时候,空间复杂度为O(N)。
int[] m = new int[n];
for(int i = 0; i < n; i++) {
m[i] = i;
}
空间复杂度 O(N2)
一段代码,随着数据量的变化内存消耗呈平方变化的时候,空间复杂度为O(N2)。
int[][] arr = new int[n][n];
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
arr[i][j] = new Random().nextInt();
}
}
总结
- 算法是用来解决某个问题(任务)的一组方法,在计算机领域中,它被描述为一系列的指令集合
- 时间复杂度:执行当前算法所消耗的时间
- 空间复杂度:执行当前算法需要占用的内存空间
- 无论是时间复杂度还是空间复杂度,考虑的都是问题规模 N 无限大的情况下,所需时间或所需空间变化的趋势
- 事实上,时间复杂度和空间复杂度描述的都是在问题规模无限大的情况下,所需时间或空间的增速
- 当问题规模 N 是有限的或很小的值,我们完全可以不考虑某个算法的时间复杂度或空间复杂度
参考资料
- 算法时间复杂度、空间复杂度(大O表示法)
- 看动画轻松理解时间复杂度(一)
- 看动画轻松理解时间复杂度(二)
- 冰与火之歌:「时间」与「空间」复杂度
- 《算法图解:像小说一样有趣的算法入门书》
- 《大话数据结构》