時間複雜度
請原諒我也是一個標題黨!node
關於時間複雜度和空間複雜度分析的文章其實很多,但大多數都充斥着複雜的數學計算,讓不少讀者感到困惑,我就不跟你們扯皮了,關於什麼是漸近分析、最壞時間複雜度、平均時間複雜度和最好的時間複雜度,以及大 記法等等,你們好好花點兒時間看看嚴老師的書就會了。web
咱們從代碼和實現的層面講講,如何計算你寫的代碼的時間複雜度?算法
任何一門語言的邏輯結構無非三種:順序結構、循環結構和分支結構,可是真正影響到時間複雜度只有循環結構,若是分支結構影響複雜度,也是由於分支內部包含了循環。數組
循環的實現有 for
和 while
兩種形式,可是本質都是同樣的,咱們接下來均以 for
循環進行說明。微信
若是一個函數(語句)不包含循環、迭代或很是數時間的函數,則能夠認爲函數爲 的時間。app
// 非循環或者遞歸語句或函數
int love = 520;
好比交換兩個數的 swap()
函數的時間複雜度就是
:編輯器
void swap(int *xp, int *yp)
{
int temp = *xp;
*xp = *yp;
*yp = temp;
}
一個循環若是僅僅運行常數次,那麼時間複雜度也能夠看成 來處理:svg
// c 表示一個常量
for(int i = 0; i < c; i++) {
// O(1) 表達式
}
若是循環控制變量 i
遞增/遞減的步長爲一個常數
,則認爲循環的時間複雜度爲
。例如,如下函數的時間複雜度爲
:函數
// c 是一個正整數常量
for(int i = 0; i < n; i += c) {
// O(1) 表達式
}
-------------------------------
for(int i = n; i > 0; i -= c) {
// O(1) 表達式
}
,嵌套循環的時間複雜度等於最內層語句的執行次數。例如,下面示例循環的時間複雜度爲 :ui
for (int i = 1; i <= n; i += c) {
for (int j = 1; j <= n; j += c) {
// O(1) 表達式
}
}
for (int i = n; i > 0; i -= c) {
for (int j = i+1; j <= n; j += c) {
// O(1) 表達式
}
}
若是循環控制變量 i
乘以或除以一個常數
,則循環的時間複雜度爲
量級:
// c 是一個正整數常量
for(int i = 1; i < n; i *= c) {
// O(1) 表達式
}
-------------------------------
for(int i = n; i > 0; i /= c) {
// O(1) 表達式
}
簡單地分析一下,循環控制變量到達 的順序爲 ,若是咱們令 ,那麼 ,也就是循環到達 n 僅僅會執行 次,複雜度天然爲 量級。
好比二分查找(binary search)的迭代實現的時間複雜度就是 :
int binarySearch(int arr[], int l, int r, int x)
{
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x)
return m;
if (arr[m] < x)
l = m + 1;
else
r = m - 1;
}
return -1;
}
若是循環控制變量 i
以指數級別增長或者減小,則時間複雜度爲
量級。
狀況一:
for(int i = 2; i <= n; i = pow(i, k)) {
// O(1) 表達式或語句
}
這種狀況下,i
的取值爲 ,而最後一項必定小於等於
,即 ,也就是說循環執行了
次,每一次迭代的時間爲常數時間,則上面的迭代總的時間複雜度爲
量級。
狀況二:
// fun(i) 表示爲 i 開方或者開立方
for(int i = n; i > 0; i = fun(i)){
// O(1) 表達式或語句
}
i
的取值爲, ,因此迭代總共執行
次,時間複雜度爲
量級。
若是程序中包含多個循環,又該如什麼時候間複雜性?
若是程序中存在多個連續循環時,時間複雜度爲多個單循環的時間複雜度之和。
for (int i = 1; i <= m; i += c) {
// O(1) 表達式或語句
}
for (int i = 1; i <= n; i += c) {
// O(1) 表達式或語句
}
上面程序的時間複雜度爲 ,當 時,則 ,依舊是 量級。
若是循環內部包含大量的 if...else...
語句時,又該如何計算時間複雜度?
在最好、平均和最差時間複雜度中,咱們通常只關注最壞時間複雜度。考慮最壞的狀況,當if...else...
條件中的語句致使執行時間複雜度的增長,就須要將其計算到最壞時間複雜度中。
int search(int arr[], int n, int x)
{
int i;
for (i = 0; i < n; i++) {
if (arr[i] == x)
return i;
}
return -1;
}
例如,考慮上面的線性搜索函數,其中咱們考慮元素出如今數組 arr[]
的末尾或根本不存在的狀況,分別對應
和
的時間複雜度,可是咱們僅關注最壞的時間複雜度
。
當代碼太複雜而沒法考慮全部 if...else
狀況時,咱們能夠忽略 if...else
和其餘複雜的控制語句來計算最壞狀況下的時間複雜度。
遞歸算法的時間複雜度又該如何計算?
不少算法都是基於遞歸思想的,咱們分析這些遞歸算法,能夠獲得關於時間複雜度的遞歸關係式。好比 「歸併排序」 的時間複雜度通常表示爲 ,還有二分查找,漢諾塔問題等等,可是關於遞歸的時間複雜度並不簡單。
對於遞歸的時間複雜度的計算主要有三種方式:
1、代入法:先對解進行猜測,而後用數學概括法證實猜測的正確性。
已知 ,注意 前面的係數 ;
又很容易獲得 和 之間的關係式,即 .
將 與 之間的關係式帶入 當中,就是將全部的 替換爲 :
則, ,注意
就獲得了 與 之間的關係式,
同理, 與 之間的關係爲: ,
帶入 ,獲得 和 之間的關係式;
,注意
以此類推...
能夠推知 與 之間的關係:
∴ 歸併排序的時間複雜度爲 量級。
2、主定理
令 和 是常數, 是一個函數, 是定義在非負整數上的遞歸式:
其中咱們將 解釋爲 或 ,那麼 有以下漸近界:
-
若對某個常數 有 ,則 . -
若 ,則 . -
若對某個常數 有 ,且對某個常數 和全部足夠大的 有 ,則 .
主定理對遞歸式 所提供的一種 「菜譜式」 的求解方法,關於主定理的證實就不在這裏解釋了,感興趣能夠看一下 《算法導論》4.6 節的主定理的證實。
咱們這裏直接 「下菜「 便可。
已知 ;
首先和 對應起來
、 、
則
∴ .
即歸併排序的時間複雜度爲 .
3、遞歸樹
在該方法中,咱們繪製了一棵遞歸樹,並計算了樹的每一層所花費的時間。最後,咱們總結了各級所作的工做。爲了繪製遞歸樹,咱們從給定的遞歸開始,不斷繪製,直到在級別之間找到一個模式。一般是算術或幾何級數。
以遞歸關係 爲例,其遞歸樹能夠表示爲:
咱們進一步打開表達式 ,以及表達式 :
繼續把 , 拆分。
而爲了計算 ,則須要一層一層地將樹中的全部結點累加起來,即:
上面序列的幾何級數的係數爲 5/16,爲了得到上限,咱們能夠無限趨近於無窮大,得到 ,時間複雜度爲 量級。
是否是被這種方法搞得暈暈乎乎,不要緊,這屬於瞭解內容,固然想深究的能夠看看算法導論。
你該不會到這裏就結束了吧!
其實計算算法的時間複雜度還有一種方法,就是根據題型去判斷計算:
題型 | 時間複雜度 |
---|---|
數組、動態規劃 | for 循環執行次數 |
鏈表 | while 循環執行次數 |
棧、隊列 | 棧或者隊列的大小 |
二叉樹 | 結點個數 |
回溯法、BFS/DFS | 元素個數 O(n) |
二分查找 | log(n) |
分治法 | log n (歸併排序、快速排序) |
固然這也只是一個比較粗略的歸納,時間複雜度的分析無定法,僅僅是你們的一個共識而已,最好的方法是,將你的代碼結合數據跑起來,計算它的運行時間,可是這也須要你們規定設備的型號和運行內存等等硬件。
誰說深究不是一種品質呢?若是你真的對時間複雜度和空間複雜度感興趣,那就研究下去!!!
推薦閱讀:
本文分享自微信公衆號 - 五分鐘學算法(CXYxiaowu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。