不论是 Android 代码仍是数据结构的设计,都涉及到算法的问题,其中时间复杂度是一个Core,这篇文章咱们就一块儿聊聊时间复杂度的原理!
虽然随着计算机硬件的迭代更新,运算处理的性能愈来愈强,但实际上,它也须要根据输入数据的大小和算法效率来消耗必定的处理器资源。要想编写出能高效运行的程序,咱们就须要考虑到 “算法的效率”。java
衡量算法的“好坏”和“效率”主要由如下两个指标(复杂度)来评估:算法
✨ 时间复杂度(运行时间):评估执行程序所需的时间,能够估算出程序对处理器的使用程度。(本篇博文咱们重点探讨时间复杂度)
✨ 空间复杂度(占用空间):评估执行程序所需的存储空间,能够估算出程序对计算机内存的使用程度。编程
咱们经过几个场景引出时间复杂度的概念,以及常见的几种时间复杂度,最后再总结比较它们的优劣!数据结构
场景一
生活场景: 你买了一箱“牛栏山二锅头”(16瓶),2天喝一瓶,所有喝完须要几天?函数
很简单的算术问题,2 ✖ 16 = 32 天,那若是一箱有 n 瓶,则须要 2 ✖ n = 2n 天,若是咱们用一个函数来表达这个相对时间,能够记做 T(n) = 2n
。性能
代码场景:.net
for(int i = 0; i < n; i++){ // 执行次数是线性的 System.out.println("喝一瓶酒"); System.out.println("等待一天"); }
场景二
生活场景: 你又买了一箱“牛栏山二锅头”(16瓶),决定换个法子喝,5天为一个周期,喝剩下酒的一半,因而第一次喝 8 瓶,第二次喝 4 瓶,那么喝到最后一瓶须要几天?设计
这个问题其实也很简单,16/2 = 8,8/2 = 4,4/2 = 2,2/2 = 1(还剩一瓶),这不就是对数函数吗?以 2 为底数,16为真数,获得的对数就是咱们须要的答案!咱们能够简写为:5log16,若是一箱有 n 瓶,则须要 5logn 天,若是咱们用一个函数来表达这个相对时间,能够记做 T(n) = 5logn
。code
代码场景:blog
for(int i = 1; i < n; i *= 2){ System.out.println("喝一瓶酒"); System.out.println("等待一天"); System.out.println("等待一天"); System.out.println("等待一天"); System.out.println("等待一天");
场景三
生活场景: 酒喝多了,买了一瓶枸杞,3天喝一瓶,请问喝完枸杞要几天?
是的,你没听错,我只是问你喝完枸杞要多久?答案很简单:3天!若是咱们用一个函数来表达这个相对时间,能够记做:T(n) = 3
。
代码场景:
void drink(int n){ System.out.println("喝一瓶枸杞"); System.out.println("等待一天"); System.out.println("等待一天");
场景四
生活场景: 酒瘾难戒,又买了一箱好酒(6瓶),可是又不能多喝,因而第一瓶喝了1天,第二瓶喝了2天,第三瓶喝了3天,这样下去所有喝完须要几天?
不用我说,其实这就是一个 1 + 2 + 3 ... + 6 的算术问题,咱们知道有个公式:6(6+1)/2 = 21 天,那若是有 n 瓶,就须要 n(n+1)/2 天,若是咱们用一个函数来表达这个相对时间,能够记做 T(n) = n²/2 + n/2
。
代码场景:
void drink(int n){ for(int i = 0; i < n; i++){ for(int j = 0; j < i; j++){ System.out.println("等待一天"); } System.out.println("喝一瓶酒"); } }
有了基本操做执行次数的函数 T(n),是否就能够分析和比较一段代码的运行时间了呢?仍是有必定的困难。好比算法 A 的相对时间是 T(n) = 100n
,算法 B 的相对时间是 T(n) = 5n²
,这两个到底谁的运行时间更长一些?这就要看 n 的取值了!
因此,这时候有了 “渐进时间复杂度”(asymptotic time complectiy)的概念。
咱们看看官方的定义:
若存在函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同数量级函数。记做 T(n)= O(f(n)),称 O(f(n)) 为算法的 “渐进时间复杂度”,简称 “时间复杂度”。渐进时间复杂度用大写 O 来表示,因此也被称为 “大O表示法”。
如何推导出时间复杂度呢?有以下几个原则:
✨ 一、若是运行时间是常数量级,用常数 1 表示;
✨ 二、只保留时间函数中的最高阶项;
✨ 三、若是最高阶项存在,则省去最高阶项前面的系数。
场景一
相对时间:T(n) = 2n
,根据推导原则三:最高阶数为 2n ,省去系数 2 ,转换后的时间复杂度为:T(n) = O(n)
。
场景二
相对时间:T(n) = 5logn
,根据推导原则三:最高阶数为 5logn ,省去系数 5 ,转换后的时间复杂度为:T(n) = O(logn)
。
场景三
相对时间:T(n) = 3
,根据推导原则一:只有常数量级 ,用常数 1 表示 ,转换后的时间复杂度为:T(n) = O(1)
。
场景四
相对时间:T(n) = n²/2 + n/2
,根据推导原则二:最高阶数为 n²/2 ,省去系数 0.5 ,转换后的时间复杂度为:T(n) = O(n²)
。
这四种时间复杂度究竟谁用时更长,谁节省时间呢?O(1) < O(logn) < O(n) < O(n²)
除了常数阶、线性阶、平方阶、对数阶,还有以下时间复杂度:
f(n) | 时间复杂度 | 阶 |
---|---|---|
nlogn | O(nlogn) | nlogn 阶 |
n³ | O(n³) | 立方阶 |
2ⁿ | O(2ⁿ) | 指数阶 |
n! | O(n!) | 阶乘阶 |
(√n) | O(√n) | 平方根阶 |
n | logn | √n | nlogn | n² | 2ⁿ | n! |
---|---|---|---|---|---|---|
5 | 2 | 2 | 10 | 25 | 32 | 120 |
10 | 3 | 3 | 30 | 100 | 1024 | 3628800 |
50 | 5 | 7 | 250 | 2500 | 约10^15 | 约3.0*10^64 |
100 | 6 | 10 | 600 | 10000 | 约10^30 | 约9.3*10^157 |
1000 | 9 | 31 | 9000 | 1000 000 | 约10^300 | 约4.0*10^2567 |
从上表能够看出,O(n)、O(logn)、O(√n)、O(nlogn) 随着 n 的增长,复杂度提高不大,所以这些复杂度属于效率比较高的算法,反观 O(2ⁿ) 和 O(n!) 当 n 增长到 50 时,复杂度就突破十位数了,这种效率极差的复杂度最好不要出如今程序中,所以在动手编程时要评估所写算法的最坏状况的复杂度。
这些时间复杂度究竟谁用时更长,谁节省时间呢?O(1) < O(logn) < O(√n) < O(n) < O(nlogn) < O(n²) < O(n³) < O(2ⁿ) < O(n!)
😕【疑问】😕:如今计算机硬件性能愈来愈强,算法真的体验那么明显吗?算法时间复杂度真的须要那么重视吗?
我相信你确定存在这样的疑问,虽然咱们知道算法这个东西是很重要的,可是咱们日常可能接触很少,不少时候计算机的性能已经能知足咱们的需求,可是我仍是要举个例子让你更直观的看到不一样算法之间的巨大差别!
💥 算法 A 的相对时间规模是 T(n) = 100n,时间复杂度是 O(n),算法 A 运行在老旧电脑上。
💥 算法 B 的相对时间规模是 T(n) = 5n²,时间复杂度是 O(n²),算法 B 运行在某台超级计算机上,运行速度是老旧电脑的 100 倍。
当随着 n 的增大,咱们经过表格看看 T(n) 的变化:
n | T(n) = 100n ✖ 100 | T(n) = 5n² |
---|---|---|
1 | 10000 | 5 |
5 | 50000 | 125 |
10 | 10 0000 | 500 |
100 | 100 0000 | 50000 |
1000 | 1000 0000 | 500 0000 |
2000 | 2000 0000 | 2000 0000 |
10000 | 1 0000 0000 | 5 0000 0000 |
100000 | 10 0000 0000 | 500 0000 0000 |
1000000 | 100 0000 0000 | 50000 0000 0000 |
从表格中能够看出,当 n 的值很小的时候,算法 A 的运行用时要远大于算法 B;当 n 的值达到 1000 左右,算法 A 和算法 B 的运行时间已经接近;当 n 的值达到 2000 左右,算法 A 和 算法 B 的运行时间一致;当 n 的值愈来愈大,达到十万、百万时,算法 A 的优点开始显现,算法 B 则愈来愈慢,差距愈来愈明显。这就是不一样时间复杂度带来的差距,即使你的计算机很牛X!