Algorithm Master Road:算法的时间/空间复杂度
很多同學(xué)都覺得算法很難,難以入門,難以理解,更難以掌握和運用,其實歸根溯源,我們可以把所有的問題都通過枚舉法來解決,但是受困于「時間」和「空間」的因素,有的時候并不能枚舉所有的情況,所以需要通過精妙的算法設(shè)計避免枚舉一些顯而易見錯誤的情況。
那么既然說到了「時間」和「空間」,這篇文章就跟大家聊一下算法設(shè)計過程中必須要考慮的時間復(fù)雜度和空間復(fù)雜度。
什么是時間復(fù)雜度和空間復(fù)雜度?
算法的時間復(fù)雜度和空間復(fù)雜度是對算法執(zhí)行效率的分析,也是一個對算法的度量單位,目的是看算法實際是否可行,并且當同一個問題有多種解法時,可以進行時間和空間性能上的比較,以便從中挑選出較優(yōu)算法。
衡量算法執(zhí)行效率的方法有兩種:
事后統(tǒng)計法需要先將算法實現(xiàn),然后測算其真正的時間和空間開銷。這種方法的缺陷很顯然,一是必須把算法轉(zhuǎn)換成可執(zhí)行的程序,二是時空開銷的測算結(jié)果依賴于計算機的軟硬件等環(huán)境因素。
雖然我們都希望自己的算法更高效,但是每設(shè)計出來一個算法都寫出程序來測算時空開銷顯然是不現(xiàn)實的,所有我們通常采用事前分析法,在編程前就盡量準確地估計程序的時空開銷,通過數(shù)學(xué)分析和計算來估計算法的復(fù)雜度。
時間復(fù)雜度
算法的執(zhí)行時間
先來看幾個定義:
語句頻度:一條語句的重復(fù)執(zhí)行次數(shù)稱為語句頻度。
單位時間:由于語句的執(zhí)行要由源程序經(jīng)編譯程序翻譯成目標代碼,目標代碼經(jīng)裝配再執(zhí)行,因此語句執(zhí)行一次實際所需的具體時間跟機器軟、硬件環(huán)境相關(guān),所以設(shè)每條語句執(zhí)行一次所需的時間均為單位時間。
一個算法的執(zhí)行時間大致上等于其所有語句執(zhí)行時間的總和,而語句的執(zhí)行時間=語句頻度×執(zhí)行一次所需時間。
如果不考慮計算機的軟硬件等環(huán)境因素,影響算法時間代價的最主要因素是問題規(guī)模,問題規(guī)模是算法求解問題輸入量的多少,是問題大小的本質(zhì)表示,一般用整數(shù)n表示。
問題規(guī)模n對不同的問題含義不同,例如:
- 排序算法:n為參加排序的記錄數(shù)
- 矩陣運算:n為矩陣的階數(shù)
- 多項式運算:n為多項式的項數(shù)
- 集合運算:n為集合中元素的個數(shù)
- 樹有關(guān)運算:n為樹的結(jié)點個數(shù)
- 圖有關(guān)運算:n為圖的頂點數(shù)或邊數(shù)
顯然n越大算法的執(zhí)行時間越長。
例如:求兩個n階矩陣的乘積算法
矩陣乘法定義:
for(int i = 1; i < n + 1; i++) { // 頻度: n + 1 <思考:為什么是 n+1 而不是 n ?> for(int j = 1; j < n + 1; j++) { // 頻度: n * (n + 1) c[i][j] = 0; // 頻度: n^2 for(int k = 1; k < n + 1; k++) { // 頻度: n^2 * (n + 1)c[i][j] = c[i][j] + a[i][k] * b[k][j]; // 頻度: n^3}} }該算法中所有語句頻度之和,是矩陣階數(shù)n的函數(shù),用f(n)表示:f(n)=2n3+3n2+2n+1f(n)=2n^3+3n^2+2n+1f(n)=2n3+3n2+2n+1。
算法的時間復(fù)雜度定義
通常,算法的執(zhí)行時間是隨問題規(guī)模增長而增長的,因此對算法的評價通常只需考慮其隨問題規(guī)模增長的趨勢。這種情況下,我們只需要考慮當問題規(guī)模充分大時,算法中基本語句的執(zhí)行次數(shù)在漸進意義下的階。
比如矩陣的乘積算法中,當n趨向于無窮大時,顯然有:lim?n→∞f(n)n3=lim?n→∞2n3+3n2+2n+1n3=2\lim_{n\to \infty}\frac{f(n)}{n^{3}}=\lim_{n\to \infty}\frac{2n^3+3n^2+2n+1}{n^3}=2limn→∞?n3f(n)?=limn→∞?n32n3+3n2+2n+1?=2。
也就是說,當n充分大時,f(n)和n3之比是一個不等于零的常數(shù),即f(n)和n3是同階的,或者說f(n)和n3的數(shù)量級相同。
在這里,我們用“O”來表示數(shù)量級,記作T(n)=O(f(n))=O(n3),其中T(n)=O(f(n))就是算法的時間復(fù)雜度,它表示隨問題規(guī)模n的增大,算法執(zhí)行時間的增長率和f(n)的增長率相同,也叫算法的漸進時間復(fù)雜度,簡稱時間復(fù)雜度。
最好、最壞和平均時間復(fù)雜度
對于某些問題的算法,其基本語句的頻度不僅僅與問題的規(guī)模相關(guān),還依賴于其它因素。
例如:在一維數(shù)組a中順序查找某個值等于e的元素,并返回其所在位置:
int find(int e) {for(int i = 0; i < n; i++) {if(a[i] == e) {return i + 1;}}return -1; }容易看出,此算法中基本語句的頻度不僅與問題規(guī)模n有關(guān),還與輸入實例中數(shù)組a[i]的各元素值及e的取值有關(guān)。
假如運氣爆棚,每次要找的元素e正好是數(shù)組中的第一個元素,那么不論數(shù)組的規(guī)模多大,基本語句的頻度f(n)=1;假如運氣極差,每次要找的元素e是數(shù)組的最后一個元素,則基本語句的頻度f(n)=n。
對于一個算法來說,需要考慮各種可能出現(xiàn)的情況,以及每一種情況出現(xiàn)的概率,一般情況下,可假設(shè)待查找的元素在數(shù)組中所有位置上出現(xiàn)的可能性相同,則可取基本語句的頻度在最好情況和最壞情況的平均值,即f(n)=n/2。
最壞時間復(fù)雜度:在最壞情況下,算法的時間復(fù)雜度
最好時間復(fù)雜度:在最好情況下,算法的時間復(fù)雜度
平均時間復(fù)雜度:所有可能輸入實例在等概率出現(xiàn)的情況下,算法的期望運行時間
通常只討論算法在最壞情況下的時間復(fù)雜度,確定算法執(zhí)行時間的上界,以保證算法的運行時間不會比它更長。
時間復(fù)雜度分析舉例
分析算法時間復(fù)雜度的基本方法為:找出所有語句中語句頻度最大的那條語句作為基本語句,計算基本語句的頻度得到問題規(guī)模n的某個函數(shù)f(n),取其數(shù)量級用符號“O”表示即可。
定理 1.1:若f(n)=amnm+am?1nm?1+...+a1n+a0f(n)=a_{m}n^{m}+a_{m-1}n^{m-1}+...+a_{1}n+a_{0}f(n)=am?nm+am?1?nm?1+...+a1?n+a0?是一個m次多項式,則T(n)=O(nm)。
定理 1.1 說明,在計算算法時間復(fù)雜度時,可以忽略所有低次冪和最高次冪的系數(shù),這樣可以簡化算法分析,也體現(xiàn)出了增長率的含義。
下面舉例說明一些常見的時間復(fù)雜度。
常數(shù)階
例:獲取程序支持的最大值。
const int MAXN = 1024; int get_max() {return MAXN; }這個比較好理解,一共就一句話,沒有循環(huán),是常數(shù)時間,表示為 O(1)。
實際上,如果算法的執(zhí)行時間是一個與問題規(guī)模n無關(guān)的常數(shù),那么算法的時間復(fù)雜度為T(n)=O(1),稱為常數(shù)階。
對數(shù)階
例:給定n(n<1000)個元素的有序數(shù)組a和整數(shù)v,求v在數(shù)組中對的下標,若不存在則返回-1。
這是一個常見的查找問題,我們可以用O(n)的算法遍歷整個數(shù)組,然后去找v的值。當然,也有更快的辦法,注意到題目中的條件,數(shù)組a是有序的,所以我們可以利用二分查找來實現(xiàn)。
int binary_search(int n, int a[], int v) {int l = 0, r = n - 1;while(l <= r) {mid = (l + r) >> 1;if(a[mid] == v) return mid;else if(a[mid] < v)r = mid + 1;elsel = mid + 1;}return -1; }由于我們每次都可以把搜索范圍縮小一半,假設(shè)基本語句的頻度為f(n),則有2f(n)<n,f(n)<log2n,所以算法的時間復(fù)雜度為T(n)=O(log2n),稱為對數(shù)階。
線性階
例:給定n(n<1000)個元素ai,求其中奇數(shù)有多少個?
判斷一個數(shù)是偶數(shù)還是奇數(shù),只需要求它除上 2 的余數(shù)是 0 還是 1,那么我們把所有數(shù)都判斷一遍,并且對符合條件的情況進行計數(shù),最后返回這個計數(shù)器就是答案。
int count_odd(int n, int a[]) {int cnt = 0;for(int i = 0; i < n; ++i) {if(a[i] & 1)++cnt;}return cnt; }其中a & 1等價于a % 2
這個就是經(jīng)典的線性時間復(fù)雜度O(n),稱為線性階。
線性對數(shù)階
例:給定n(n<1000)個元素的有序數(shù)組a,求有多少個二元組(i, j),滿足ai+aj=1024?其中(i<j)
枚舉ai,然后在[i+1, n)范圍內(nèi)查找是否存在aj=1024-ai,存在則計數(shù)器+1,而這個查找的過程可以采用二分查找。
int count_odd(int n, int a[]) {int cnt = 0;for (int i = 0; i < n; ++i) {int l = i + 1, r = n - 1;while (l <= r) {mid = (l + r) >> 1;if(a[mid] == 1024 - a[i]) ++cnt;else if(a[mid] < v)r = mid + 1;elsel = mid + 1;}}return cnt; }該算法的時間復(fù)雜度為T(n)=O(nlog2n),稱為線性對數(shù)階。
平方階
例:給定n(n<1000)個元素ai,求有多少個二元組(i, j),滿足ai+aj是奇數(shù)?其中(i<j)
還是秉承枚舉法的思想,需要兩個變量i和j,枚舉ai和aj,再對ai+aj進行奇偶性判斷。
int count_odd_pair(int n, int a[]) {int cnt = 0;for(i = 0; i < n; ++i) {for(j = i+1; j < n; ++j) {if( (a[i] + a[j]) & 1) ++cnt;}}return cnt; }對循環(huán)語句只需要考慮循環(huán)體中語句的執(zhí)行次數(shù),以上程序段中頻度最大的語句是if( (a[i] + a[j]) & 1) ++cnt;,其頻度為f(n)=n(n?1)2f(n)=\frac{n(n-1)}{2}f(n)=2n(n?1)?,所以該算法的時間復(fù)雜度為T(n)=O(n2),稱為平方階。
多數(shù)情況下,當有若干個循環(huán)語句時,算法的時間復(fù)雜度是由最深層循環(huán)內(nèi)的基本語句的頻度f(n)決定的。
立方階
例:給定n(n<1000)個元素ai,求有多少個三元組(i, j, k),滿足ai+aj+ak是奇數(shù)?其中(i<j<k)
相信通過前面兩個例子的分析,可以直接給出代碼了。
int count_odd_triple(int n, int a[]) {int cnt = 0;for(i = 0; i < n; ++i) {for(j = i+1; j < n; ++j) {for(int k = j+1; k < n; ++k) {if( (a[i] + a[j] + a[k]) & 1 )++cnt;}}}return cnt; }該程序段中頻度最大的語句是:if( (a[i] + a[j] + a[k]) & 1 ) ++cnt;,這條最深層循環(huán)內(nèi)的基本語句的頻度依賴于各層循環(huán)變量的取值,算法的時間復(fù)雜度為T(n)=O(n3),稱為立方階。
常見的時間復(fù)雜度按數(shù)量級遞增排序:常數(shù)階O(1)<對數(shù)階O(log2n)<線性階O(n)<線性對數(shù)階O(nlog2n)<平方階O(n2)<立方階O(n3)<…<k次方階O(nk)<指數(shù)階O(2n)<階乘階O(n!)。
時間復(fù)雜度的計算
對于很多時間復(fù)雜度的題目,很多都是在做題時一眼就能看出程序的時間復(fù)雜度,但是無法規(guī)范地表述其推導(dǎo)過程。在這里總結(jié)此類題型的兩種形式以及做題技巧。
此類題應(yīng)該找出主體語句中與T(n)成正比的循環(huán)變量,將之代入條件中進行計算。
m++的次數(shù)恰好與T(n)成正比,記t為該程序的執(zhí)行次數(shù)并令t=m-5,有m=t+5,則(t+5+1)(t+5+1)<n,得t<n?6t<\sqrt{n}-6t<n??6,即T(n)=O(n)T(n)=O(\sqrt{n})T(n)=O(n?)。
循環(huán)主體中的變量與循環(huán)條件無關(guān)
此類題可采用數(shù)學(xué)歸納法或直接累計循環(huán)次數(shù)。多層循環(huán)時從內(nèi)到外分析,忽略單步語句、條件判斷語句,只關(guān)注主體語句的執(zhí)行次數(shù)。此類問題又可分為遞歸程序和非遞歸程序。
-
遞歸程序:使用公式進行遞推
-
非遞歸程序:直接累計次數(shù)
空間復(fù)雜度
算法的空間復(fù)雜度S(n)定義為該算法所需要的存儲空間,它也是問題規(guī)模n的函數(shù):S(n)=O(f(n))S(n)=O(f(n))S(n)=O(f(n))。
一個程序在機器上執(zhí)行時,除了需要寄存本身所用的指令、常數(shù)、變量和輸入數(shù)據(jù)外,還需要一些對數(shù)據(jù)進行操作和存儲一些為實現(xiàn)計算所需信息的輔助空間。
其中,對于輸入數(shù)據(jù)所占的具體存儲量取決于問題本身,與算法無關(guān),這樣只需分析該算法在實現(xiàn)時所需的輔助空格鍵就可以了。
例:數(shù)組逆序,將一維數(shù)組a中的n個數(shù)逆序存放到原數(shù)組中。
算法1:
for(int i = 0; i < n; i++) {b[i] = a[n - i - 1]; } for(int i = 0; i < n; i++) {a[i] = b[i]; }算法1需要另外借助一個大小為n的輔助數(shù)組b,所以其空間復(fù)雜度為O(n)。
算法2:
for(int i = 0; i < n / 2; i++) {t = a[i];a[i] = a[n - i - 1];a[n - i - 1] = t; }算法2僅需要另外借助一個變量t,與問題規(guī)模n大小無關(guān),所以其空間復(fù)雜度為O(1)。
算法原地工作是指算法所需的輔助空間為常量,即S(n)=O(1)。
關(guān)于「 算法時間/空間復(fù)雜度 」 的內(nèi)容到這里就結(jié)束了。
總結(jié)
以上是生活随笔為你收集整理的Algorithm Master Road:算法的时间/空间复杂度的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: unable to access ‘ht
- 下一篇: 计算机网络可被理解为( )