1. 程式人生 > >算法基礎 -- 簡介時間復雜度與空間復雜度

算法基礎 -- 簡介時間復雜度與空間復雜度

耗時 需要 語言 忽略 turn text 綜合 color 關心

  算法是為求解一個問題所需要遵循的、被清楚地指定的簡單指令的集合。對於一個問題,一旦給定某種算法並且確定其實正確的,那麽重要的一步就是確定該算法將需要多少諸如時間或空間等資源量的問題,這就是時間復雜度和空間復雜度存在的意義。常用時間復雜度和空間復雜度來衡量不同算法的優劣。

一、從數學的角度理解 O(n)、Ω(n)、o(n)和Θ(n)

  通常,我們以函數所處理的數據量來表示算法的性能,也就是說,對於大小為 n 的數據,我們用函數 f(n) 來表示它的算法性能。在很多情況下,我們可以完全確定 f 的具體值,但是這通常不是必要的,我們更關心的是當算法處理的數據量變得無窮大時,算法的性能將趨近於一個什麽樣的值,即一個算法的增長速率

(運行時間、占用空間的增長率)。要描述這種增長率,需要用到一系列新的定義,這些定義的目的是要在函數間建立一種相對的級別,反映出二者之間的相對增長率

1.1 Θ 記法:

  Θ 記法給出了一個函數的漸進上界和漸進下界。對於一個給定的函數 g(n),用 Θ(g(n)) 來表示以下函數的集合:

  Θ(g(n)) = { f(n):存在正常量c1、c2、n0,使得對所有 n >= n0,有 0 <= c1g(n) <= f(n) <= c2g(n) }

  即,f(n) = Θ(g(n)) 表示,對所有的 n>=n0,函數 f(n) 在一個常量因子內等於 g(n),稱 g(n) 是 f(n) 的一個漸進緊確界

。此外,有一個定理:對於任意兩個函數 f(n) 和 g(n),我們有 f(n) = Θ(g(n)),當且僅當 f(n) = O(g(n)) 且 f(n) = Ω(g(n))

1.2 大 O 記法:

  對於一個給定函數 g(n),用 O(g(n)) 來表示以下函數的集合:

  O(g(n)) = {f(n) :存在正常量 c 和 n0,使得對所有 n>=n0,有 0 <= f(n) <= cg(n)}

  用大 O 記法來給出函數的一個在常量因子內的漸進上界。即, f(n) = O(g(n)) 表示,函數 f(n) 的增長率小於等於 g(n) 的增長率

  大 O 記法是最常用的一種用來表示算法增長規律的方法,以大 O 記法來表示函數 f(n) 時,需要註意幾點(其他幾種記法也是):

1)可以忽略 f (n) 的常數項,因為隨著 n 的值變得越來越大,常數項最終變得可忽略不計;

2)可以忽略 f (n) 的常數因子,因為隨著 n 的值越來越大,常數因子也可以忽略不計;

3)只需要考慮 f (n) 的高階項的因子,因為隨著 n 的值越來越大,高階因子的值會迅速超過低階因子的值。 

1.3 大 Ω 記法:

  大 Ω 記法給出了一個函數的漸進下界。對於給定的函數 g(n),用 Ω(g(n)) 表示以下函數的集合:

  Ω(g(n)) = {f(n) :存在正常量 c 和 n0,使得對所有 n>=n0,有 0 <= cg(n) <= f(n)}

  即,f(n) = Ω(g(n)) 表示,函數 f(n) 的增長率小於等於 g(n) 的增長率

1.4 小 o 記法:

  大 O 記法提供的漸進上界可能是漸進緊確的也可能不是,因此,我們使用小 o 記號來表示一個非漸進緊確的上界。因此, o(g(n)) 表示這樣的集合:

  o(g(n)) = {f(n) :對任意正常量 c > 0,存在常量 n0 > 0,使得對所有 n>=n0,有 0 <= f(n) < cg(n)}

  即,f(n) = o(g(n)) 表示,函數 f(n) 的增長率小於 g(n) 的增長率

二、時間復雜度

  時間復雜度指的是執行一個算法所需要的時間。這不一定是一個確切的時間,通常,我們需要知道的是一個算法在最壞情況下執行(比如輸入規模無限大)所需要的時間,也就是尋找算法執行時間的一個漸進上界來作為算法的時間復雜度,通過比較多個算法的這個上界,可以知道哪個算法執行比較快,哪個比較慢。這裏就用到了上面所講的大 O 記法,我們通常使用大 O 記法來表示一個算法的時間復雜度。

2.1 計算時間復雜度的方法

  一個程序運行的總時間主要與以下兩點有關:

1)執行每條語句所耗的時間;

2)執行每條語句的頻率或者說次數。

  其中,第一條取決於計算機、編譯器和操作系統,第二條取決於程序本身和輸入規模。如果對於一個程序的所有部分,我們都知道了這些性質,則將它們相乘並將所有指令的成本相加即可得到總運行時間,這是一個確切的時間。用大 O 記法對這個確切的時間表達式( f(n) )進行處理:

1)執行每條語句所耗的時間為常數,常數項忽略,因此只需要考慮每條語句的執行頻率;

2)忽略執行頻率函數的常數因子;

3)只保留執行頻率函數的最高階項。

  即可得到一個大 O 表達式,這個大 O 表達式就是該程序的時間復雜度。舉一個例子:

1 int Sum(int n)
2 {
3     int i,sum;
4 
5     sum = 0;
6     for(i = 0;i < n;i++)
7         sum += i*i*i;
8     return num;
9 }

  對這段代碼進行分析:首先,聲明不計入時間;第 5 行和第 8 行各占一個時間單元;第 7 行每執行一次占用四個時間單元(兩次乘法、一次加法、一次賦值),執行 n 次,共占用 4n 個時間單元;第 6 行在初始化 i、測試 i < n 和對 i 進行自增操作中隱含著開銷,所有這些開銷總共占用 2n+2 個時間單元(i 初始化占用一個時間單元、測試 i < n 要執行 n+1 次,占用 n+1 個時間單元、自增操作執行 n 次,占用 n 個時間單元)。因此,上述代碼所耗的時間總量為 2 + 4n +(2n+2) = 6n + 4。根據前面所提到的方法,忽略常數項、忽略常數因子,可以得到這段代碼的時間復雜度為 O(n)。

  當然,如果每次都要像這樣逐行地對代碼進行分析來計算時間復雜度顯然是不可行的,通常,許多代碼的大 O 表達式是已知的,直接根據已知的大 O 結果即可得到代碼最後的結果。因此,關於上述代碼的時間復雜度,還可以這樣分析: for 循環是 O(n) 語句,其他語句的執行時間為常數項,忽略掉,即可得到整段代碼的時間復雜度為 O(n).

2.2 一些常見的時間復雜度

大O表達式 描述
O(1)

常數級,表明算法的執行時間不隨問題規模 n 的增大而增大;

另外,對於常數 c,有 O(c) = O(1)

普通語句,如 a = b+c
O(logn)

對數級,表明算法的執行時間隨問題規模 n 的增大而呈對數增長;

對數的底數與增長的數量級無關(不同的底數相當於常數因子),

因此在說明對數級時一般使用 logn 來表示。

二分查找
O(n) 線性級,表明算法的執行時間隨問題規模 n 的增大而呈線性增長 單個for循環
O(nlogn) 線性對數級,表明算法的執行時間與問題規模 n 的關系為 nlogn 歸並排序、快速排序
O(n^2) 平方級,表明算法的執行時間隨問題規模 n 的增大而呈平方級增長 二層 for 循環、選擇排序
O(n^3) 立方級,表明算法的執行時間隨問題規模 n 的增大而呈立方級增長 三層 for 循環
O(2^n) 指數級,表明算法的執行時間隨問題規模 n 的增大而呈指數增長 窮舉查找

  按照所消耗時間從小到大排序:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n).

三、空間復雜度

  算法的空間復雜度用來描述算法在運行時臨時占用存儲空間的大小,記作 S(n) = O(f(n)) ,表示算法所占用的存儲空間與問題規模 n 的關系,其分析計算方式也與時間復雜度類似。

  對於一個算法,其時間復雜度和空間復雜度往往是相互影響的。當追求一個較好的時間復雜度時,可能會使得空間復雜度的性能變差,即占用更多的存儲空間;相反,當追求更好的空間復雜度時,可能會使得時間復雜度變差,消耗更多的運行時間。在設計一個程序(尤其是大型程序)時,需要綜合考慮算法的各項性能,以在二者之間尋求一個平衡點,達到最大收益。

參考資料:

《算法導論 第三版》

《數據結構與算法分析--C語言描述》

算法基礎 -- 簡介時間復雜度與空間復雜度