一、時間復雜度計算
1、 時間復雜度的意義
復雜度分析是整個算法學習的精髓,只要掌握了它,數據結構和算法的內容基本上就掌握了一半
- 測試結果非常依賴測試環境
- 測試結果受數據規模的影響很大
所以,我們需要一個不用具體的測試數據來測試,就可以粗略地估計算法的執行效率的方法,即時間、空間復雜度分析方法。
2、大 O 復雜度表示法
1)、 可以將計算時間復雜度的方式和計算代碼執行次數來進行類別
int cal(int n) { int sum = 0; int i = 1; for (; i <= n; ++i) { sum = sum + i; } return sum; }
第 2、3 行代碼分別需要 1 個 unit_time 的執行時間,第 4、5 行都運行了 n 遍,所以需要 2n * unit_time 的執行時間,所以這段代碼總的執行時間就是 (2n+2) * unit_time。可以看出來,所有代碼的執行時間 T(n) 與每行代碼的執行次數成正比。
2)、 復雜一點的計算
int cal(int n) { ----1 int sum = 0; ----2 int i = 1; ----3 int j = 1; ----4 for (; i <= n; ++i) { ----5 j = 1; ----6 for (; j <= n; ++j) { ----7 sum = sum + i * j; ----8 } ----9 } ----10 } ----11
T(n) = (2n^2+2n+3)unit_time
大 O 時間復雜度實際上並不具體表示代碼真正的執行時間,而是表示代碼執行時間隨數據規模增長的變化趨勢,所以,也叫作漸進時間復雜度(asymptotic time complexity),簡稱時間復雜度 T(n)=O(f(n))
2、 時間復雜度計算法則
只關注循環執行次數最多的一段代碼
加法法則:總復雜度等於量級最大的那段代碼的復雜度
- 如果 T1(n)=O(f(n)),T2(n)=O(g(n));那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n))).
乘法法則:嵌套代碼的復雜度等於嵌套內外代碼復雜度的乘積
T(n) = T1(n) * T2(n) = O(n*n) = O(n2)
3、 常見的是時間復雜度
復雜度量級(遞增)排列 公式 常量階 O(1) 對數階 O(logn) 線性階 O(n) 線性對數階 O(nlogn) 平方階、立方階...K次方階 O(n2),O(n3),O(n^k) 指數階 O(2^n) 階乘階 O(n!) ①. O(1):代碼的執行時間和n沒有關系,一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬行的代碼,其時間復雜度也是Ο(1);
②. O(logn)、O(nlogn)
i=1; while (i <= n) { i = i * 2; }
通過 2x=n 求解 x 這個問題我們想高中應該就學過了,我就不多說了。x=log2n,所以,這段代碼的時間復雜度就是 O(log2n)。
當底數為其他方式的時候,可以根據換底公式對去掉底數通過前面的學習,我們知道常量系數對於時間復雜度是沒有影響的,所以可以統一換算為以10位底的對數,及來O(logn);
根據前面的乘法法則,執行n遍時間復雜度為O(logn)的代碼段,即可以得到時間整個總的時間復雜度為O(nlogn);
③. 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; }
針對這種情況,原來的加法法則就不正確了,我們需要將加法規則改為:
,但是乘法法則繼續有效:T1(m)*T2(n) = O(f(m) * f(n)) T1(m) + T2(n) = O(f(m) + g(n))
二、空間復雜度分析
1、定義:時間復雜度的全稱是漸進時間復雜度,表示算法的執行時間與數據規模之間的增長關系。類比一下,空間復雜度全稱就是漸進空間復雜度,表示算法的存儲空間與數據規模之間的增長關系。
2、 例子:
void print(int n) { int i = 0; int[] a = new int[n]; for (i; i <n; ++i) { a[i] = i * i; } for (i = n-1; i >= 0; --i) { print out a[i] } }
第 2 行代碼中,我們申請了一個空間存儲變量 i,但是它是常量階的,跟數據規模 n 沒有關系,所以我們可以忽略。第 3 行申請了一個大小為 n 的 int 類型數組,除此之外,剩下的代碼都沒有占用更多的空間,所以整段代碼的空間復雜度就是 O(n)
3、 常見空間復雜度:O(1)、O(n)、O(n2), O(logn)、O(nlogn) 這樣的對數階復雜度平時都用不到;
4、 課后習題有人說,我們項目之前都會進行性能測試,再做代碼的時間復雜度、空間復雜度分析,是不是多此一舉呢?而且,每段代碼都分析一下時間復雜度、空間復雜度,是不是很浪費時間呢?你怎么看待這個問題呢?
答案肯定是否定的,性能測試很依賴於測試環境,環境的優劣影響着測試結果,另外,數據的測試規模也對結果有很大的影響,利用時間復雜度和空間復雜度的分析,可以排除環境原因,在理論上對程序的運行時間和占用空間做分析,均衡這兩個點進行算法的選擇
精選留言:
我不認為是多此一舉,漸進時間,空間復雜度分析為我們提供了一個很好的理論分析的方向,並且它是宿主平台無關的,能夠讓我們對我們的程序或算法有一個大致的認識,讓我們知道,比如在最壞的情況下程序的執行效率如何,同時也為我們交流提供了一個不錯的橋梁,我們可以說,算法1的時間復雜度是O(n),算法2的時間復雜度是O(logN),這樣我們立刻就對不同的算法有了一個“效率”上的感性認識。 當然,漸進式時間,空間復雜度分析只是一個理論模型,只能提供給粗略的估計分析,我們不能直接斷定就覺得O(logN)的算法一定優於O(n), 針對不同的宿主環境,不同的數據集,不同的數據量的大小,在實際應用上面可能真正的性能會不同,個人覺得,針對不同的實際情況,進而進行一定的性能基准測試是很有必要的,比如在統一一批手機上(同樣的硬件,系統等等)進行橫向基准測試,進而選擇適合特定應用場景下的最有算法。 綜上所述,漸進式時間,空間復雜度分析與性能基准測試並不沖突,而是相輔相成的,但是一個低階的時間復雜度程序有極大的可能性會優於一個高階的時間復雜度程序,所以在實際編程中,時刻關心理論時間,空間度模型是有助於產出效率高的程序的,同時,因為漸進式時間,空間復雜度分析只是提供一個粗略的分析模型,因此也不會浪費太多時間,重點在於在編程時,要具有這種復雜度分析的思維
一、什么是復雜度分析? 1.數據結構和算法解決是“如何讓計算機更快時間、更省空間的解決問題”。 2.因此需從執行時間和占用空間兩個維度來評估數據結構和算法的性能。 3.分別用時間復雜度和空間復雜度兩個概念來描述性能問題,二者統稱為復雜度。 4.復雜度描述的是算法執行時間(或占用空間)與數據規模的增長關系。 二、為什么要進行復雜度分析? 1.和性能測試相比,復雜度分析有不依賴執行環境、成本低、效率高、易操作、指導性強的特點。 2.掌握復雜度分析,將能編寫出性能更優的代碼,有利於降低系統開發和維護成本。 三、如何進行復雜度分析? 1.大O表示法 1)來源 算法的執行時間與每行代碼的執行次數成正比,用T(n) = O(f(n))表示,其中T(n)表示算法執行總時間,f(n)表示每行代碼執行總次數,而n往往表示數據的規模。 2)特點 以時間復雜度為例,由於時間復雜度描述的是算法執行時間與數據規模的增長變化趨勢,所以常量階、低階以及系數實際上對這種增長趨勢不產決定性影響,所以在做時間復雜度分析時忽略這些項。 2.復雜度分析法則 1)單段代碼看高頻:比如循環。 2)多段代碼取最大:比如一段代碼中有單循環和多重循環,那么取多重循環的復雜度。 3)嵌套代碼求乘積:比如遞歸、多重循環等 4)多個規模求加法:比如方法有兩個參數控制兩個循環的次數,那么這時就取二者復雜度相加。 四、常用的復雜度級別? 多項式階:隨着數據規模的增長,算法的執行時間和空間占用,按照多項式的比例增長。包括, O(1)(常數階)、O(logn)(對數階)、O(n)(線性階)、O(nlogn)(線性對數階)、O(n^2)(平方階)、O(n^3)(立方階) 非多項式階:隨着數據規模的增長,算法的執行時間和空間占用暴增,這類算法性能極差。包括, O(2^n)(指數階)、O(n!)(階乘階) 五、如何掌握好復雜度分析方法? 復雜度分析關鍵在於多練,所謂孰能生巧。
二、最好、最壞、平均、均攤時間復雜度
- 理解:最好情況時間復雜度、最壞情況時間復雜度、平均情況時間復雜度、均攤時間復雜度 相應的概念和區別
- 學習四種時間復雜度的出現情況以及對應情況下時間復雜度的簡單計算方法
- 區別平均時間復雜度和均攤時間復雜度的相同點和不同點(了解)
1、最好、最壞情況時間復雜度
顧明思意,及在最好情況下和最差的情況下,程序的時間復雜度
先看一段程序
// n表示數組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; } } return pos; }
上面這段程序是查找數據所在數組的下標,假設數組中不存在重復元素,當所要查找的元素在第一個位置時,第一次執行for循環當中的語句即退出,程序的時間復雜度為O(1),當數組中不存在索要查找的元素時,則時間復雜度上升到O(n);所以:
最好情況時間復雜度就是,在最理想的情況下,執行這段代碼的時間復雜度
最壞情況時間復雜度就是,在最糟糕的情況下,執行這段代碼的時間復雜度
2、 平均情況時間復雜度
- 仍然是上面那段程序,元素可以出現在任何位置,且出現幾率相同,要查找的變量 x 在數組中的位置,有 n+1 種情況:在數組的 0~n-1 位置中和不在數組中,我們把每種情況下,查找遍歷元素的次數加起來,除以n+1,得到平均每次需要遍歷的個數
化簡后,去掉系數,則為O(n);
上面的這種計算方法是有一定問題的,及:我們假設每個位置出現的查找元素的概率是相同的,實際上,是不同的
我們知道,要查找的變量 x,要么在數組里,要么就不在數組里。這兩種情況對應的概率統計起來很麻煩,為了方便你理解,我們假設在數組中與不在數組中的概率都為 1/2。另外,要查找的數據出現在 0~n-1 這 n 個位置的概率也是一樣的,為 1/n。所以,根據概率乘法法則,要查找的數據出現在 0~n-1 中任意位置的概率就是 1/(2n),
因此,前面的推導過程中存在的最大問題就是,沒有將各種情況發生的概率考慮進去。如果我們把每種情況發生的概率也考慮進去,那平均時間復雜度的計算過程就變成了這樣:
這個計算出來的值,叫做加權平均值,所以平均時間復雜度的全稱應該叫加權平均時間復雜度或者期望時間復雜度;
3、均攤時間復雜度(了解:可看作為一種特殊的平均時間復雜度)
實際上,在大多數情況下,我們並不需要區分最好、最壞、平均情況時間復雜度三種情況。像我們上一節課舉的那些例子那樣,很多時候,我們使用一個復雜度就可以滿足需求了。只有同一塊代碼在不同的情況下,時間復雜度有量級的差距,我們才會使用這三種復雜度表示法來區分。
4、課后習題
// 全局變量,大小為10的數組array,長度len,下標i。 int array[] = new int[10]; int len = 10; int i = 0; // 往數組中添加一個元素 void add(int element) { if (i >= len) { // 數組空間不夠了 // 重新申請一個2倍大小的數組空間 int new_array[] = new int[len*2]; // 把原來array數組中的數據依次copy到new_array for (int j = 0; j < len; ++j) { new_array[j] = array[j]; } // new_array復制給array,array現在大小就是2倍len了 array = new_array; len = 2 * len; } // 將element放到下標為i的位置,下標i加一 array[i] = element; ++i; }
答案:最好:O(1);最壞:O(n);平均O(1);
當i < len時, 即 i = 0,1,2,...,n-1的時候,for循環不走,所以這n次的時間復雜度都是O(1); 當i >= len時, 即 i = n的時候,for循環進行數組的copy,所以只有這1次的時間復雜度是O(n); 由此可知: 該算法的最好情況時間復雜度(best case time complexity)為O(1); 最壞情況時間復雜度(worst case time complexity)為O(n); 平均情況時間復雜度(average case time complexity), 第一種計算方式: (1+1+...+1+n)/(n+1) = 2n/(n+1) 【注: 式子中1+1+...+1中有n個1】,所以平均復雜度為O(1); 第二種計算方式(加權平均法,又稱期望): 1*(1/n+1)+1*(1/n+1)+...+1*(1/n+1)+n*(1/(n+1))=1,所以加權平均時間復雜度為O(1); 第三種計算方式(均攤時間復雜度): 前n個操作復雜度都是O(1),第n+1次操作的復雜度是O(n),所以把最后一次的復雜度分攤到前n次上,那么均攤下來每次操作的復雜度為O(1)