前言
最近打算好好刷刷算法題,然鵝發(fā)現(xiàn)自己對(duì)這個(gè)算法復(fù)雜度的知識(shí)記憶已全部返還給數(shù)據(jù)結(jié)構(gòu)老師了
一、算法
算法(Algorithm)是指解題方案的準(zhǔn)確而完整的描述,是一系列解決問(wèn)題的清晰指令,算法代表著用系統(tǒng)的方法描述解決問(wèn)題的策略機(jī)制。通俗地說(shuō),數(shù)據(jù)結(jié)構(gòu)就是指存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu)。算法就是操作數(shù)據(jù)的方法。
二、算法的特征
- 有窮性(Finiteness):算法的有窮性是指算法必須能在執(zhí)行有限個(gè)步驟之后終止
- 確切性(Definiteness):算法的每一步驟必須有確切的定義
- 輸入項(xiàng)(Input):一個(gè)算法有0個(gè)或多個(gè)輸入,以刻畫運(yùn)算對(duì)象的初始情況,所謂0個(gè)輸入是指算法本身定出了初始條件
- 輸出項(xiàng)(Output):一個(gè)算法有一個(gè)或多個(gè)輸出,以反映對(duì)輸入數(shù)據(jù)加工后的結(jié)果,沒(méi)有輸出的算法是毫無(wú)意義的
- 可行性(Effectiveness):算法中執(zhí)行的任何計(jì)算步驟都是可以被分解為基本的可執(zhí)行的操作步,即每個(gè)計(jì)算步都可以在有限時(shí)間內(nèi)完成(也稱之為有效性)
三、算法效率的度量
對(duì)于同一個(gè)問(wèn)題,使用不同的算法,也許最終得到的結(jié)果是一樣的,但在過(guò)程中消耗的資源和時(shí)間卻會(huì)有很大的區(qū)別。那么我們應(yīng)該如何去衡量不同算法之間的優(yōu)劣呢?主要還是從算法所占用的「時(shí)間」和「空間」兩個(gè)維度去考量。
時(shí)間維度:是指執(zhí)行當(dāng)前算法所消耗的時(shí)間,我們通常用「時(shí)間復(fù)雜度」來(lái)描述。
空間維度:是指執(zhí)行當(dāng)前算法需要占用多少內(nèi)存空間,我們通常用「空間復(fù)雜度」來(lái)描述。
評(píng)價(jià)一個(gè)算法的效率主要是看它的時(shí)間復(fù)雜度和空間復(fù)雜度情況。有的時(shí)候時(shí)間和空間卻又是「魚和熊掌」不可兼得,那么我們就需要從中去取一個(gè)平衡點(diǎn)。
四、時(shí)間復(fù)雜度
【4.1】時(shí)間頻度 :一個(gè)算法執(zhí)行所耗費(fèi)的時(shí)間,從理論上是不能算出來(lái)的,必須上機(jī)運(yùn)行測(cè)試才能知道。但我們不可能也沒(méi)有必要對(duì)每個(gè)算法都上機(jī)測(cè)試,只需知道哪個(gè)算法花費(fèi)的時(shí)間多,哪個(gè)算法花費(fèi)的時(shí)間少就可以了。并且一個(gè)算法花費(fèi)的時(shí)間與算法中語(yǔ)句的執(zhí)行次數(shù)成正比例,哪個(gè)算法中語(yǔ)句執(zhí)行次數(shù)多,它花費(fèi)時(shí)間就多。一個(gè)算法中的語(yǔ)句執(zhí)行次數(shù)稱為語(yǔ)句頻度或時(shí)間頻度,記為T(n)。
【4.2】時(shí)間復(fù)雜度: 在剛才提到的時(shí)間頻度T(n)中,n稱為問(wèn)題的規(guī)模,當(dāng)n不斷變化時(shí),時(shí)間頻度T(n)也會(huì)不斷變化。但有時(shí)我們想知道它變化時(shí)呈現(xiàn)什么規(guī)律。為此,我們引入時(shí)間復(fù)雜度概念。 算法的時(shí)間復(fù)雜度也就是算法的時(shí)間度量,記作:T(n) = O(f(n))。它表示隨問(wèn)題規(guī)模n的增大,算法執(zhí)行時(shí)間的增長(zhǎng)率和f(n)的增長(zhǎng)率相同,稱作算法的漸進(jìn)時(shí)間復(fù)雜度,簡(jiǎn)稱時(shí)間復(fù)雜度
【4.3】大O表示法:像前面用O( )來(lái)體現(xiàn)算法時(shí)間復(fù)雜度的記法,我們稱之為大O表示法。 算法復(fù)雜度可以從最理想情況、平均情況和最壞情況三個(gè)角度來(lái)評(píng)估,由于平均情況大多和最壞情況持平,而且評(píng)估最壞情況也可以避免后顧之憂,因此一般情況下,我們?cè)O(shè)計(jì)算法時(shí)都要直接估算最壞情況的復(fù)雜度。
【4.4】常見的時(shí)間復(fù)雜度量級(jí):
- 常數(shù)階O(1)
- 線性階O(n)
- 平方階O(n2)
- 立方階O(n3)
- 對(duì)數(shù)階O(logn)
- 線性對(duì)數(shù)階O(nlogn)
- 指數(shù)階O(2?)
【4.5】計(jì)算時(shí)間復(fù)雜度時(shí)的程序分析法則:
⑴. 對(duì)于一些簡(jiǎn)單的輸入輸出語(yǔ)句或賦值語(yǔ)句,近似認(rèn)為需要O(1)時(shí)間
⑵. 對(duì)于順序結(jié)構(gòu),需要依次執(zhí)行一系列語(yǔ)句所用的時(shí)間可采用大O求和法則
- 例一:算法的2個(gè)部分時(shí)間復(fù)雜度分別為 T1(n)=O(f(n)) 和 T2(n)=O(g(n)),則時(shí)間復(fù)雜度 T1(n)+T2(n)=O(max(f(n), g(n)))
- 例二:算法的2個(gè)部分時(shí)間復(fù)雜度分別為T1(m)=O(f(m)) 和 T2(n)=O(g(n)),則時(shí)間復(fù)雜度為 T1(m)+T2(n)=O(f(m) + g(n))
⑶. 對(duì)于選擇結(jié)構(gòu),如if語(yǔ)句,它的主要時(shí)間耗費(fèi)是在執(zhí)行then字句或else字句所用的時(shí)間,需注意的是檢驗(yàn)條件也需要O(1)時(shí)間
⑷. 對(duì)于循環(huán)結(jié)構(gòu),循環(huán)語(yǔ)句的運(yùn)行時(shí)間主要體現(xiàn)在多次迭代中執(zhí)行循環(huán)體以及檢驗(yàn)循環(huán)條件的時(shí)間耗費(fèi),一般可用大O乘法法則
- 例一:算法的2個(gè)部分時(shí)間復(fù)雜度分別為 T1(n)=O(f(n)) 和 T2(n)=O(g(n)),則時(shí)間復(fù)雜度為 T1T2=O(f(n)g(n))
⑸. 對(duì)于復(fù)雜的算法,可以將它分成幾個(gè)容易估算的部分,然后利用求和法則和乘法法則技術(shù)求出整個(gè)算法的時(shí)間復(fù)雜度
⑹. 另外還有以下2個(gè)運(yùn)算法則
- 若 g(n)=O(f(n)),則O(f(n))+ O(g(n))= O(f(n))
- O(Cf(n)) = O(f(n)),其中C是一個(gè)正常數(shù)
【4.6】常見的時(shí)間復(fù)雜度示例:
常數(shù)階 O(1)
let j = temp;
let i = j;
let Temp = i;
解:以上三條單個(gè)語(yǔ)句的頻度均為1,該程序段的執(zhí)行時(shí)間是一個(gè)與問(wèn)題規(guī)模n無(wú)關(guān)的常數(shù)。算法的時(shí)間復(fù)雜度為常數(shù)階,記作T(n)=O(1)。注意:如果算法的執(zhí)行時(shí)間不隨著問(wèn)題規(guī)模n的增加而增長(zhǎng),即使算法中有上千條語(yǔ)句,其執(zhí)行時(shí)間也不過(guò)是一個(gè)較大的常數(shù)。此類算法的時(shí)間復(fù)雜度是O(1)。
線性階 **O(n) **
let a = 0;
let b = 1; // 語(yǔ)句1
for (let i = 1; i <= n; i++) { // 語(yǔ)句2
let s = a + b; // 語(yǔ)句3
let b = a; // 語(yǔ)句4
let a = s; // 語(yǔ)句5
}
解: 語(yǔ)句1的頻度為2;語(yǔ)句2的頻度為n;語(yǔ)句3的頻度為 n-1;語(yǔ)句4的頻度為n-1;語(yǔ)句5的頻度為n-1;T(n)=2+n+3(n-1)=4n-1=O(n)
平方階 O(n2)
let sum = 0; // 1次
for(let i = 1; i <= n; i++) { // n+1次
for (j = 1; j <= n; j++) { // n2次
sum ++ ; // n2次
}
}
解:因?yàn)镺(2n2+n+1)=n2(即:去低階項(xiàng),去掉常數(shù)項(xiàng),去掉高階項(xiàng)的常參得到),所以T(n)=O(n2);
for (let i = 1; i < n; i++) {
y = y + 1; // 語(yǔ)句1
for (let j = 0; j <= (2 * n); j++)
x++; // 語(yǔ)句2
}
解: 語(yǔ)句1的頻度是n-1, 語(yǔ)句2的頻度是(n-1)*(2n+1)=2n2-n-1,即 f(n)=2n2-n-1+(n-1)=2n2-2;又O(2n2-2)=n2,該程序的時(shí)間復(fù)雜度T(n)=O(n2)。一般情況下,對(duì)步進(jìn)循環(huán)語(yǔ)句只需考慮循環(huán)體中語(yǔ)句的執(zhí)行次數(shù),忽略該語(yǔ)句中步長(zhǎng)加1、終值判別、控制轉(zhuǎn)移等成分,當(dāng)有若干個(gè)循環(huán)語(yǔ)句時(shí),算法的時(shí)間復(fù)雜度是由嵌套層數(shù)最多的循環(huán)語(yǔ)句中最內(nèi)層語(yǔ)句的頻度f(wàn)(n)決定的。
對(duì)數(shù)階 O(log2?)
let i = 1; //語(yǔ)句1
while (i <= n) {
i = i * 2; // 語(yǔ)句2
}
解: 上面的代碼,在while循環(huán)里面,每次都將 i 乘以 2,乘完之后,i 距離 n 就越來(lái)越近了,直到i不小于n退出。我們?cè)囍蠼庖幌拢僭O(shè)循環(huán)次數(shù)為x,也就是說(shuō) 2 的 x 次方等于 n,則由2^x=n得出x=log2n。因此這個(gè)代碼的時(shí)間復(fù)雜度為T(n)=O(log2? )
【4.7】復(fù)雜度的比較
其中x軸代表n值,y軸代表T(n)值(時(shí)間復(fù)雜度)。T(n)值隨著n的值的變化而變化,其中可以看出O(n!)和O(2?)隨著n值的增大,它們的T(n)值上升幅度非常大,而O(logn)、O(n)、O(1)隨著n值的增大,T(n)值上升幅度則很小。
常用的時(shí)間復(fù)雜度按照耗費(fèi)的時(shí)間從小到大依次是:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2?)<O(n!)
五、空間復(fù)雜度
時(shí)間復(fù)雜度,換句話說(shuō),就是它們運(yùn)行得有多快。但有些時(shí)候,我們還得以另一種名為空間復(fù)雜度的度量方式,去估計(jì)它們會(huì)消耗多少內(nèi)存。當(dāng)內(nèi)存有限時(shí),空間復(fù)雜度便會(huì)成為選擇算法的一個(gè)重要的參考因素。比如說(shuō),在給小內(nèi)存的小型設(shè)備寫程序時(shí),或是處理一些會(huì)迅速占滿大內(nèi)存的大數(shù)據(jù)時(shí)都會(huì)考慮空間復(fù)雜度。
【5.1】空間復(fù)雜度:空間復(fù)雜度是執(zhí)行算法的空間成本,是對(duì)一個(gè)算法在運(yùn)行過(guò)程中臨時(shí)占用存儲(chǔ)空間大小的量度,它同樣適用了大O表示法。程序占用空間大小的計(jì)算公式記作S(n)=O(f(n)),其中n為問(wèn)題的規(guī)模,f(n)為算法所占儲(chǔ)存空間的函數(shù)
【5.2】算法存儲(chǔ)量包括:
- 程序本身所占空間
- 輸入數(shù)據(jù)所占空間
- 輔助變量所占空間
輸入數(shù)據(jù)所占空間只取決于問(wèn)題本身,和算法無(wú)關(guān),空間復(fù)雜度只需分析除了輸入數(shù)據(jù)所占空間和程序本身所占空間之外的輔助變量所占空間。
【5.3】常見空間復(fù)雜度:
O(1):算法執(zhí)行所需要的臨時(shí)空間不隨著某個(gè)變量n的大小而變化,即此算法空間復(fù)雜度為一個(gè)常量
O(n):當(dāng)一個(gè)算法的空間復(fù)雜度與n成線性比例關(guān)系時(shí),可表示為O(n)。若形參為數(shù)組,則只需要為它分配一個(gè)存儲(chǔ)由實(shí)參傳送來(lái)的一個(gè)地址指針的空間,即一個(gè)機(jī)器字長(zhǎng)空間;若形參為引用方式,則也只需要為其分配存儲(chǔ)一個(gè)地址的空間,用它來(lái)存儲(chǔ)對(duì)應(yīng)實(shí)參變量的地址,以便由系統(tǒng)自動(dòng)引用實(shí)參變量。
O(log2n):當(dāng)一個(gè)算法的空間復(fù)雜度與以2為底的n的對(duì)數(shù)成正比時(shí)
【5.3】空間復(fù)雜度示例:
空間復(fù)雜度 O(1)
function makeUpperCase(arr) {
for (let i = 0; i < arr.length; i++) {
arr[i] = arr[i].toUpperCase();
}
return arr;
}
因?yàn)樵摵瘮?shù)并不消耗額外的內(nèi)存空間,所以我們把它的空間復(fù)雜度描述為O(1)
空間復(fù)雜度 O(n)
function makeUpperCase(arr) { // makeUpperCase函數(shù)接收一個(gè)數(shù)組作為參數(shù)arr。
let newArr = []; // 然后它創(chuàng)建了一個(gè)全新的數(shù)組,名為newArr
for (let i = 0; i < arr.length; i++) {
newArr[i] = arr[i].toUpperCase(); // 并將原數(shù)組arr里的字符串的大寫形式填進(jìn)去。
}
return newArr;
}
分析該函數(shù)的話,你會(huì)發(fā)現(xiàn)它接收一個(gè)n元素的數(shù)組,就會(huì)產(chǎn)生另一個(gè)新的n元素?cái)?shù)組。因此,我們會(huì)說(shuō)這個(gè)makeUpperCase函數(shù)的空間復(fù)雜度是O(n)
**注意 **
① 空間復(fù)雜度相比時(shí)間復(fù)雜度分析要少
② 對(duì)于遞歸算法來(lái)說(shuō),代碼一般都比較簡(jiǎn)短,算法本身所占用的存儲(chǔ)空間較少,但運(yùn)行時(shí)需要占用較多的臨時(shí)工作單元;若寫成非遞歸算法,代碼一般可能比較長(zhǎng),算法本身占用的存儲(chǔ)空間較多,但運(yùn)行時(shí)將可能需要較少的存儲(chǔ)單元
六、常用的時(shí)間復(fù)雜度和空間復(fù)雜度
一個(gè)經(jīng)驗(yàn)規(guī)則:其中c是一個(gè)常量,如果一個(gè)算法的復(fù)雜度為c 、 log2? 、n 、 n*log2? ,那么這個(gè)算法時(shí)間效率比較高 ,如果是2? 、3? 、n!,那么稍微大一些的n就會(huì)令這個(gè)算法不能動(dòng)了,居于中間的幾個(gè)則差強(qiáng)人意。
| 排序法| 最差時(shí)間分析 | 平均時(shí)間復(fù)雜度 | 穩(wěn)定度 | 空間復(fù)雜度 |
| 冒泡排序 | O(n2) | O(n2) | 穩(wěn)定 | O(1) |
| 快速排序 | O(n2) | O(nlog2?) | 不穩(wěn)定 | O(log2?)~O(n) |
| 選擇排序 | O(n2) | O(n2) | 穩(wěn)定 | O(1) |
| 二叉樹排序 | O(n2) | O(nlog2?) | 不一定 | O(n) |
| 插入排序| O(n2) | O(n2) | 穩(wěn)定 | O(1) |
| 堆排序 | O(nlog2?) | O(nlog2?) | 不穩(wěn)定 | O(1) |
| 希爾排序 | O | O | 不穩(wěn)定 | O(1) |
七、時(shí)間復(fù)雜度和空間復(fù)雜度的關(guān)系
對(duì)于一個(gè)算法,其時(shí)間復(fù)雜度和空間復(fù)雜度往往是相互影響的。當(dāng)追求一個(gè)較好的時(shí)間復(fù)雜度時(shí),可能會(huì)使空間復(fù)雜度的性能變差,即可能導(dǎo)致占用較多的存儲(chǔ)空間;反之,當(dāng)追求一個(gè)較好的空間復(fù)雜度時(shí),可能會(huì)使時(shí)間復(fù)雜度的性能變差,即可能導(dǎo)致占用較長(zhǎng)的運(yùn)行時(shí)間。