不論是 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!