復(fù)雜度分析:
數(shù)據(jù)結(jié)構(gòu)和算法解決的兩大問題:快和省摊阀。運行快胞此,儲存省漱牵。
復(fù)雜度分析是算法學(xué)習(xí)的精髓:時間復(fù)雜度疚漆,空間復(fù)雜度娶聘。
事后統(tǒng)計法有很大局限性:
- 測試結(jié)果依賴測試環(huán)境
- 測試結(jié)果受數(shù)據(jù)規(guī)模影響很大丸升。
大O復(fù)雜度表示法:
例1:
1 int cal(int n) {
2 int sum = 0;
3 int i = 1;
4 for (; i <= n; ++i) {
5 sum = sum + i;
6 }
7 return sum;
8 }
從 CPU 的角度來看狡耻,這段代碼的每一行都執(zhí)行著類似的操作:讀數(shù)據(jù)-運算-寫數(shù)據(jù)夷狰。盡管每行代碼對應(yīng)的 CPU 執(zhí)行的個數(shù)孵淘、執(zhí)行的時間都不一樣瘫证,但是背捌,我們這里只是粗略估計毡庆,所以可以假設(shè)每行代碼執(zhí)行的時間都一樣烙如,為 unit_time亚铁。在這個假設(shè)的基礎(chǔ)之上徘溢,這段代碼的總執(zhí)行時間是多少呢然爆?
第 2黍图、3 行代碼分別需要 1 個 unit_time 的執(zhí)行時間助被,第 4恰起、5 行都運行了 n 遍检盼,所以需要 2n*
unit_time 的執(zhí)行時間吨枉,所以這段代碼總的執(zhí)行時間就是 (2n+2)*
unit_time貌亭∑酝ィ可以看出來剧腻,所有代碼的執(zhí)行時間 T(n) 與每行代碼的執(zhí)行次數(shù)成正比书在。
例2:
1 int cal(int n) {
2 int sum = 0;
3 int i = 1;
4 int j = 1;
5 for (; i <= n; ++i) {
6 j = 1;
7 for (; j <= n; ++j) {
8 sum = sum + i * j;
9 }
10 }
11 }
第 2儒旬、3、4 行代碼竖般,每行都需要 1 個 unit_time 的執(zhí)行時間,第 5前计、6 行代碼循環(huán)執(zhí)行了 n 遍男杈,需要 2n * unit_time 的執(zhí)行時間伶棒,第 7肤无、8 行代碼循環(huán)執(zhí)行了 n^2遍宛渐,所以需要 2n^2 * unit_time 的執(zhí)行時間窥翩。所以寇蚊,整段代碼總的執(zhí)行時間 仗岸。
盡管我們不知道 unit_time 的具體值爹梁,但是通過這兩段代碼執(zhí)行時間的推導(dǎo)過程姚垃,我們可以得到一個非常重要的規(guī)律积糯,那就是,所有代碼的執(zhí)行時間 T(n) 與每行代碼的執(zhí)行次數(shù) n 成正比君编。
公式:
其中,
- T(n) 表示代碼執(zhí)行的時間兑燥;
- n 表示數(shù)據(jù)規(guī)模的大薪低挣饥;
- f(n) 表示每行代碼執(zhí)行的次數(shù)總和扔枫。
- O表示代碼的執(zhí)行時間 T(n) 與 f(n) 表達(dá)式成正比茧吊。
例1即T(n) = O(2n+2),例2即 T(n) = O(2n^2+2n+3)八毯。這就是大 O 時間復(fù)雜度表示法搓侄。其并不具體表示代碼真正的執(zhí)行時間,而是表示代碼執(zhí)行時間隨數(shù)據(jù)規(guī)模增長的變化趨勢话速,所以讶踪,也叫作漸進(jìn)時間復(fù)雜度(asymptotic time complexity),簡稱時間復(fù)雜度泊交。
當(dāng) n --> +∞乳讥,公式中的低階廓俭、常量云石、系數(shù)三部分并不左右增長趨勢,所以都可以忽略研乒。我們只需要記錄一個最大量級就可以了汹忠,就可以記為:T(n) = O(n); T(n) = O(n^2)。
時間復(fù)雜度分析的三個方法:
1.只關(guān)注循環(huán)執(zhí)行次數(shù)最多的一段代碼:
大 O 這種復(fù)雜度表示方法只是表示一種變化趨勢宽菜。忽略公式中的常量谣膳、低階、系數(shù)铅乡,只需要記錄一個最大階的量級就可以了继谚。所以,分析算法時間復(fù)雜度的時候阵幸,也只關(guān)注循環(huán)執(zhí)行次數(shù)最多的那一段代碼就可以了花履。這段核心代碼執(zhí)行次數(shù)的 n 的量級,就是整段要分析代碼的時間復(fù)雜度挚赊。
2.加法法則:總復(fù)雜度等于量級最大的那段代碼的復(fù)雜度
int cal(int n) {
int sum_1 = 0;
int p = 1;
for (; p < 100; ++p) {
sum_1 = sum_1 + p;
}
int sum_2 = 0;
int q = 1;
for (; q < n; ++q) {
sum_2 = sum_2 + q;
}
int sum_3 = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum_3 = sum_3 + i * j;
}
}
return sum_1 + sum_2 + sum_3;
}
sum_1時間復(fù)雜度是個常量诡壁,理應(yīng)被忽略,與規(guī)模n無關(guān)咬腕。強(qiáng)調(diào)一下欢峰,即便這段代碼循環(huán) 10000 次葬荷、100000 次涨共,<font color=red>只要是一個已知的數(shù),跟 n 無關(guān)宠漩,照樣也是常量級的執(zhí)行時間举反。</font>當(dāng) n 無限大的時候,就可以忽略扒吁。盡管對代碼的執(zhí)行時間會有很大影響火鼻,但是回到時間復(fù)雜度的概念來說,它表示的是一個算法執(zhí)行效率與數(shù)據(jù)規(guī)模增長的變化趨勢雕崩,所以不管常量的執(zhí)行時間多大魁索,我們都可以忽略掉。因為它本身對增長趨勢并沒有影響盼铁。
sum_2是O(n)粗蔚,sum_3是O(n2)。綜合這三段代碼的時間復(fù)雜度饶火,我們?nèi)∑渲凶畲蟮牧考壟艨兀碠(n2)。
3. 乘法法則:嵌套代碼的復(fù)雜度等于嵌套內(nèi)外代碼復(fù)雜度的乘積
也就是說肤寝,假設(shè) T1(n) = O(n)当辐,T2(n) = O(n^2),則 T1(n) * T2(n) = O(n^3)鲤看。
int cal(int n) {
int ret = 0;
int i = 1;
for (; i < n; ++i) {
ret = ret + f(i);
}
}
int fun(int n) {
int sum = 0;
int i = 1;
for (; i < n; ++i) {
sum = sum + i;
}
return sum;
}
fun()為T1(n) = O(n)缘揪,而cal()本身有for循環(huán)即T2(n) = O(n)),for循環(huán)中又調(diào)用了fun(),即T(n) = T1(n) * T2(n) = O(n*n) = O(n^2)。
復(fù)雜度分析這個東西關(guān)鍵在于“熟練”寺晌。
常見時間復(fù)雜度:
遞增順序依次是:
多項式量級:常量階O(1)世吨,對數(shù)階O(logn),線性階O(n)呻征,線性對數(shù)階O(nlogn)耘婚,K方階O(n^k)
非多項式量級:指數(shù)階O(2^n),階乘階O(n!)
注:非多項式量級的算法問題叫作NP(Non-Deterministic Polynomial陆赋,非確定多項式)問題沐祷。當(dāng)數(shù)據(jù)規(guī)模 n 越來越大時,其執(zhí)行時間會急劇增加攒岛,求解問題的執(zhí)行時間會無限增長赖临。所以,非多項式時間復(fù)雜度的算法其實是非常低效的算法灾锯,理應(yīng)避免兢榨。
O(1)
常量級時間復(fù)雜度的表示方法,并不是指只執(zhí)行了一行代碼顺饮,即便執(zhí)行了三行吵聪,也只能是O(1);只要代碼的執(zhí)行時間不隨 n 的增大而增長兼雄,這樣代碼的時間復(fù)雜度我們都記作 O(1)吟逝。或者說赦肋,一般情況下块攒,只要算法中不存在循環(huán)語句、遞歸語句佃乘,即使有成千上萬行的代碼囱井,其時間復(fù)雜度也是Ο(1)。
O(logn)趣避、O(nlogn)
對數(shù)階 最常見庞呕,也是最難分析的一種。
i=1;
while (i <= n) {
i = i * 2;
}
代碼執(zhí)行次數(shù) 即i<=n截止鹅巍,i=1千扶,2,4骆捧,8澎羞,16,32……2k……2x=n;
求解執(zhí)行次數(shù)就是 求解2^x=n中的x敛苇;x=log?n妆绞,T(n)=O(log?n)顺呕;
實際上,不管是以 2 為底括饶、以 3 為底株茶,還是以 10 為底,我們可以把所有對數(shù)階的時間復(fù)雜度都記為 O(logn)图焰。為什么呢启盛?
我們知道,對數(shù)之間是可以互相轉(zhuǎn)換的技羔,log?n 就等于 log?2 * log?n僵闯,所以 O(log?n) = O(C * log?n),其中 C=log?2 是一個常量藤滥”钏冢基于我們前面的一個理論:在采用大 O 標(biāo)記復(fù)雜度的時候,可以忽略系數(shù)拙绊,即 O(Cf(n)) = O(f(n))向图。所以,O(log?n) 就等于 O(log?n)标沪。因此榄攀,在對數(shù)階時間復(fù)雜度的表示方法里,我們忽略對數(shù)的“底”谨娜,統(tǒng)一表示為 O(logn)航攒。
O(m+n)磺陡、O(m*n)
int cal(int m, int n) {
int sum_1 = 0;
int i = 1;
for (; i < m; ++i) {
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for (; j < n; ++j) {
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
m 和 n 是表示兩個數(shù)據(jù)規(guī)模趴梢。我們無法事先評估 m 和 n 誰的量級大,所以我們在表示復(fù)雜度的時候币他,就不能簡單地利用加法法則坞靶,省略掉其中一個。所以蝴悉,上面代碼的時間復(fù)雜度就是 O(m+n)彰阴。
針對這種情況,原來的加法法則就不正確了拍冠,我們需要將加法規(guī)則改為:T1(m) + T2(n) = O(f(m) + g(n))尿这。但是乘法法則繼續(xù)有效:T1(m)*T2(n) = O(f(m) * f(n))。
數(shù)據(jù)結(jié)構(gòu)操作的復(fù)雜性
數(shù)據(jù)結(jié)構(gòu) | 連接 | 查找 | 插入 | 刪除 |
---|---|---|---|---|
數(shù)組 | 1 | n | n | n |
棧 | n | n | 1 | 1 |
隊列 | n | n | 1 | 1 |
鏈表 | n | n | 1 | 1 |
哈希表 | - | n | n | n |
二分查找樹 | n | n | n | n |
B樹 | log(n) | log(n) | log(n) | log(n) |
紅黑樹 | log(n) | log(n) | log(n) | log(n) |
AVL樹 | log(n) | log(n) | log(n) | log(n) |
數(shù)組排序算法復(fù)雜性:
名稱 | 最優(yōu) | 平均 | 最壞 | 內(nèi)存 | 穩(wěn)定 |
---|---|---|---|---|---|
冒泡排序 | n | n^2 | n^2 | 1 | Yes |
插入排序 | n | n^2 | n^2 | 1 | Yes |
選擇排序 | n^2 | n^2 | n^2 | 1 | No |
堆排序 | n log(n) | n log(n) | n log(n) | 1 | No |
歸并排序 | n log(n) | n log(n) | n log(n) | n | Yes |
快速排序 | n log(n) | n log(n) | n^2 | log(n) | No |
希爾排序 | n log(n) | 取決于差距序列 | n (log(n))^2 | 1 | No |
空間復(fù)雜度
時間復(fù)雜度的全稱是漸進(jìn)時間復(fù)雜度庆杜,表示算法的執(zhí)行時間與數(shù)據(jù)規(guī)模之間的增長關(guān)系射众。類比一下,空間復(fù)雜度全稱就是漸進(jìn)空間復(fù)雜度(asymptotic space complexity)晃财,表示算法的存儲空間與數(shù)據(jù)規(guī)模之間的增長關(guān)系叨橱。1
1 void print(int n) {
2 int i = 0;
3 int[] a = new int[n];
4 for (i; i <n; ++i) {
5 a[i] = i * i;
6 }
for (i = n-1; i >= 0; --i) {
print out a[i]
}
}
第 2 行代碼中,我們申請了一個空間存儲變量 i,但是它是常量階的罗洗,跟數(shù)據(jù)規(guī)模 n 沒有關(guān)系愉舔,所以我們可以忽略。第 3 行申請了一個大小為 n 的 int 類型數(shù)組伙菜,除此之外绩鸣,剩下的代碼都沒有占用更多的空間,所以整段代碼的空間復(fù)雜度就是 O(n)飒焦。
我們常見的空間復(fù)雜度就是 O(1)酝掩、O(n)、O(n2 )丧叽,像 O(logn)卫玖、O(nlogn) 這樣的對數(shù)階復(fù)雜度平時都用不到。而且踊淳,空間復(fù)雜度分析比時間復(fù)雜度分析要簡單很多假瞬。所以,對于空間復(fù)雜度迂尝,掌握這些內(nèi)容已經(jīng)足夠了脱茉。
復(fù)雜度分析法則
1)單段代碼看高頻:比如循環(huán)。
2)多段代碼取最大:比如一段代碼中有單循環(huán)和多重循環(huán)垄开,那么取多重循環(huán)的復(fù)雜度琴许。
3)嵌套代碼求乘積:比如遞歸、多重循環(huán)等
4)多個規(guī)模求加法:比如方法有兩個參數(shù)控制兩個循環(huán)的次數(shù)溉躲,那么這時就取二者復(fù)雜度相加榜田。
一、復(fù)雜度分析的4個概念:
1.最壞情況時間復(fù)雜度:代碼在最理想情況下執(zhí)行的時間復(fù)雜度锻梳。
2.最好情況時間復(fù)雜度:代碼在最壞情況下執(zhí)行的時間復(fù)雜度箭券。
3.平均時間復(fù)雜度:用代碼在所有情況下執(zhí)行的次數(shù)的加權(quán)平均值表示。
-
4.均攤時間復(fù)雜度:在代碼執(zhí)行的所有復(fù)雜度情況中絕大部分是低級別的復(fù)雜度疑枯,個別情況是高級別復(fù)雜度且發(fā)生具有時序關(guān)系時辩块,可以將個別高級別復(fù)雜度均攤到低級別復(fù)雜度上【S溃基本上均攤結(jié)果就等于低級別復(fù)雜度废亭。
分別解釋四種情況:
長度為n的數(shù)組中查找x的下標(biāo),來返回具钥,找不到則返回-1豆村;
// n 表示數(shù)組 array 的長度 int find(int[] array, int n, int x) { int i = 0; int pos = -1; for (; i < n; ++i) { if (array[i] == x) { pos = i; //break;//TODO:找到即跳出 } } return pos; }
1.如果不加break的話,O(n)
2.加上break后氓拼,或許array[0]就是x你画,復(fù)雜度O(1)【最好情況】抵碟,只有數(shù)組不包含x時,才循環(huán)到n次【最差情況】坏匪。
最好情況 和 最壞情況 都是極端情況拟逮。引入一個概念:平均時間復(fù)雜度。
要查找的變量 x 在數(shù)組中的位置适滓,有 n+1 種情況:在數(shù)組的 0~n-1 位置中和不在數(shù)組中敦迄。我們把每種情況下,查找需要遍歷的元素個數(shù)累加起來凭迹,然后再除以 n+1罚屋,就可以得到需要遍歷的元素個數(shù)的平均值,即:
省略掉系數(shù)嗅绸、低階脾猛、常量,簡化之后鱼鸠,得到的平均時間復(fù)雜度就是 O(n)猛拴。這個結(jié)論雖然是正確的,但是計算過程稍微有點兒問題蚀狰。究竟是什么問題呢愉昆?我們剛講的這 n+1 種情況,出現(xiàn)的概率并不是一樣的麻蹋。
我們知道跛溉,要查找的變量 x,要么在數(shù)組里扮授,要么就不在數(shù)組里芳室。這兩種情況對應(yīng)的概率統(tǒng)計起來很麻煩,為了方便你理解糙箍,我們假設(shè)在數(shù)組中與不在數(shù)組中的概率都為 1/2渤愁。另外牵祟,要查找的數(shù)據(jù)出現(xiàn)在 0~n-1 這 n 個位置的概率也是一樣的深夯,為 1/n。所以诺苹,根據(jù)概率乘法法則咕晋,要查找的數(shù)據(jù)出現(xiàn)在 0~n-1 中任意位置的概率就是 1/(2n)。
因此收奔,前面的推導(dǎo)過程中存在的最大問題就是掌呜,沒有將各種情況發(fā)生的概率考慮進(jìn)去。如果我們把每種情況發(fā)生的概率也考慮進(jìn)去坪哄,那平均時間復(fù)雜度的計算過程就變成了這樣:
這個值就是概率論中的加權(quán)平均值质蕉,也叫作期望值势篡,所以平均時間復(fù)雜度的全稱應(yīng)該叫加權(quán)平均時間復(fù)雜度或者期望時間復(fù)雜度。去掉系數(shù)和常量模暗,這段代碼的加權(quán)平均時間復(fù)雜度仍然是 O(n)禁悠。
在大多數(shù)情況下,我們并不需要區(qū)分最好兑宇、最壞碍侦、平均情況時間復(fù)雜度三種情況。很多時候隶糕,我們使用一個復(fù)雜度就可以滿足需求了瓷产。只有同一塊代碼在不同的情況下,時間復(fù)雜度有量級的差距枚驻,我們才會使用這三種復(fù)雜度表示法來區(qū)分濒旦。
均攤時間復(fù)雜度
平均復(fù)雜度只在某些特殊情況下才會用到,而均攤時間復(fù)雜度應(yīng)用的場景比它更加特殊再登、更加有限疤估。
// array 表示一個長度為 n 的數(shù)組 // 代碼中的 array.length 就等于 n int[] array = new int[n]; int count = 0; void insert(int val) { if (count == array.length) { int sum = 0; for (int i = 0; i < array.length; ++i) { sum = sum + array[i]; } array[0] = sum; count = 1; } array[count] = val; ++count; }
這段代碼實現(xiàn)了一個往數(shù)組中插入數(shù)據(jù)的功能。當(dāng)數(shù)組滿了之后霎冯,也就是代碼中的 count == array.length 時铃拇,我們用 for 循環(huán)遍歷數(shù)組求和,并清空數(shù)組沈撞,將求和之后的 sum 值放到數(shù)組的第一個位置慷荔,然后再將新的數(shù)據(jù)插入。但如果數(shù)組一開始就有空閑空間缠俺,則直接將數(shù)據(jù)插入數(shù)組显晶。
最好為O(1),最差為O(n)壹士。
那平均時間復(fù)雜度是多少呢磷雇?答案是 O(1)。我們還是可以通過前面講的概率論的方法來分析躏救。
假設(shè)數(shù)組的長度是 n唯笙,根據(jù)數(shù)據(jù)插入的位置的不同,我們可以分為 n 種情況盒使,每種情況的時間復(fù)雜度是 O(1)崩掘。除此之外,還有一種“額外”的情況少办,就是在數(shù)組沒有空閑空間時插入一個數(shù)據(jù)苞慢,這個時候的時間復(fù)雜度是 O(n)。而且英妓,這 n+1 種情況發(fā)生的概率一樣挽放,都是 1/(n+1)绍赛。所以,根據(jù)加權(quán)平均的計算方法辑畦,我們求得的平均時間復(fù)雜度就是:
其實理解平均復(fù)雜度不需這么復(fù)雜惹资,我們可以對比 insert() 和find()的區(qū)別:
1.find() 只有在極端情況下,才為 O(1)航闺。但 insert() 通常為 O(1)褪测,極端情況下O(n)。
2.insert() 中O(1) 和 O(n) 出現(xiàn)的頻率有規(guī)律潦刃,且有一定的前后時序關(guān)系侮措,一個 O(n)之后,緊跟著 n-1 個 O(1) 乖杠,周期循環(huán)分扎。
針對這種特殊的場景,完全不需要平均復(fù)雜度那樣用概率加權(quán)平均胧洒,我們引入了一種更加簡單的分析方法:攤還分析法畏吓,對應(yīng)均攤時間復(fù)雜度。
如何用卫漫?每一個O(n) 緊跟 n-1 個 O(1) 菲饼,把耗時多的那次操作均攤到接下來的 n-1 次耗時少的操作上,均攤下來列赎,這一組連續(xù)的操作的均攤時間復(fù)雜度就是 O(1)宏悦。這就是均攤分析的大致思路。該方法不常用包吝,點到為止饼煞。
簡單總結(jié)下:對一個數(shù)據(jù)結(jié)構(gòu)進(jìn)行一組連續(xù)操作中,大部分情況下時間復(fù)雜度都很低诗越,只有個別情況下時間復(fù)雜度比較高砖瞧,而且這些操作之間存在前后連貫的時序關(guān)系,這個時候嚷狞,我們就可以將這一組操作放在一塊兒分析块促,看是否能將較高時間復(fù)雜度那次操作的耗時,平攤到其他那些時間復(fù)雜度比較低的操作上感耙。而且褂乍,在能夠應(yīng)用均攤時間復(fù)雜度分析的場合,一般均攤時間復(fù)雜度就等于最好情況時間復(fù)雜度即硼。
二、為什么要引入這4個概念屡拨?
1.同一段代碼在不同情況下時間復(fù)雜度會出現(xiàn)量級差異只酥,為了更全面褥实,更準(zhǔn)確的描述代碼的時間復(fù)雜度,所以引入這4個概念裂允。
2.代碼復(fù)雜度在不同情況下出現(xiàn)量級差別時才需要區(qū)別這四種復(fù)雜度损离。大多數(shù)情況下,是不需要區(qū)別分析它們的绝编。
三僻澎、如何分析平均、均攤時間復(fù)雜度十饥?
1.平均時間復(fù)雜度
代碼在不同情況下復(fù)雜度出現(xiàn)量級差別窟勃,則用代碼所有可能情況下執(zhí)行次數(shù)的加權(quán)平均值表示。
2.均攤時間復(fù)雜度
兩個條件滿足時使用:1)代碼在絕大多數(shù)情況下是低級別復(fù)雜度逗堵,只有極少數(shù)情況是高級別復(fù)雜度秉氧;2)低級別和高級別復(fù)雜度出現(xiàn)具有時序規(guī)律。均攤結(jié)果一般都等于低級別復(fù)雜度蜒秤。