新的一年,先給你們整理分享一個簡單而又重要的知識點:時間複雜度和空間複雜度。由於在前幾篇文章中,提到了時間複雜度,也許有些小夥伴還不清楚。(ps:但願在我上篇文章留言的那位小夥伴別失望哦,慢慢來。)算法
先給你們出個思考題,題目:sum = 1+2+3+...+n ,計算 sum 的值。數組
因此須要一個不用準確的測試結果來衡量,就能夠粗略地估計代碼執行時間的方法。這就是複雜度分析。bash
以一個例子開始,請估算下面代碼的執行時間數據結構
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
sum += i; // 4
} //5
} //6
複製代碼
咱們假設每行代碼執行的時間都同樣,記作 t,那麼上面的函數中的第 2 行須要 1 個 t 的時間,第 3 行 和 第 4 行分別須要 n 個 t 的時間,那麼這段代碼總的執行時間爲 (2n+1)*t。數據結構和算法
那麼按照上面的分析方法,請估算下面代碼的執行時間函數
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
for (var j = 0; j < n; j++) { // 4
sum = sum + i + j; // 5
}
}
}
複製代碼
第 2 行須要一個 t 的時間,第 3 行須要 n 個 t 的時間,第 4 行和第 5 行分別須要 n2 個的時間,那麼這段代碼總的執行時間爲 (2n2+n+1)*t 的時間。性能
從數學角度來看,咱們能夠得出個規律:代碼的總執行時間 T(n) 與每行代碼的執行次數成正比學習
T(n) = O(f(n))測試
在這個公式中,T(n) 表示代碼的執行時間;n 表示數據規模的大小;f(n) 表示每行代碼執行的次數總和;O 表示代碼的執行時間 T(n) 與 f(n) 表達式成正比。ui
因此上邊兩個函數的執行時間能夠標記爲 T(n) = O(2n+1) 和 T(n) = O(2n2+n+1)。這就是大 O 時間複雜度表示法,它不表明代碼真正的執行時間,而是表示代碼隨數據規模增加的變化趨勢,簡稱時間複雜度。
並且當 n 很大時,咱們能夠忽略常數項,只保留一個最大量級便可。因此上邊的代碼執行時間能夠簡單標記爲 T(n) = O(n) 和 T(n) = O(n2)。
那如何分析一段代碼的時間複雜度呢,能夠利用下面的幾個方法
咱們在分析一段代碼的時間複雜度時,咱們只要關注循環次數最多的那一段代碼就 ok 了。 好比說在第一段代碼中
function total(n) { // 1
var sum = 0; // 2
for (var i = 0; i < n; i++) { // 3
sum += i; // 4
} //5
} //6
複製代碼
只有第 3 行和第 4 行是執行次數最多的,分別執行了 n 次,那麼忽略常數項,因此此段代碼的時間複雜度就是 O(n)。
好比說,看下面這段代碼的時間複雜度。
function total(n) {
// 第一個 for 循環
var sum1 = 0;
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
sum1 = sum1 + i + j;
}
}
// 第二個 for 循環
var sum2 = 0;
for(var i=0;i<1000;i++) {
sum2 = sum2 + i;
}
// 第三個 for 循環
var sum3 = 0;
for (var i = 0; i < n; i++) {
sum3 = sum3 + i;
}
}
複製代碼
咱們先分別分析每段 for 循環的時間複雜度,再取他們中最大的量級來做爲整段代碼的時間複雜度。
第一段 for 循環的時間複雜度爲 O(n2)。
第二段 for 循環執行了 1000 次,是個常數量級,儘管對代碼的執行時間會有影響,可是當 n 無限大的時候,就能夠忽略。由於它自己對增加趨勢沒有影響,因此這段代碼的時間複雜度能夠忽略。
第三段 for 循環的時間複雜度爲 O(n)。
總上,取最大量級,因此整段代碼的時間複雜度爲 O(n2)。
好比說,看下面這段代碼的時間複雜度
function f(i) {
var sum = 0;
for (var j = 0; j < i; j++) {
sum += i;
}
return sum;
}
function total(n) {
var res = 0;
for (var i = 0; i < n; i++) {
res = res + f(i); // 調用 f 函數
}
}
複製代碼
單獨看 total 函數的時間複雜度就是爲 T1(n)=O(n),可是考慮到 f 函數的時間複雜度也爲 T2(n)=O(n)。 因此整段代碼的時間複雜度爲 T(n) = T1(n) * T2(n) = O(n*n)=O(n2)。
只看最高量級的複雜度,下圖中效率是遞減的
O(1) 只是常量級時間複雜度表示法,並非代碼只有一行,好比說下面這段代碼
function total() {
var sum = 0;
for(var i=0;i<100;i++) {
sum += i;
}
}
複製代碼
雖然有這麼多行,即便 for 循環執行了 100 次,可是代碼的執行時間不隨 n 的增大而增加,因此這樣的代碼複雜度就爲 O(1)。
對數階時間複雜度的常見代碼以下
function total1(n) {
var sum = 0;
var i = 1;
while (i <= n) {
sum += i;
i = i * 2;
}
}
function total2(n) {
var sum = 0;
for (var i = 1; i <= n; i = i * 2) {
sum += i;
}
}
複製代碼
上面兩個函數都有一個相同點,變量 i 從 1 開始取值,每循環一次乘以 2,當大於 n 時,循環結束。實際上,i 的取值就是一個等比數列,就像下面這樣
20 21 22 ... 2k... 2x =n;
因此只要知道 x 的值,就能夠知道這兩個函數的執行次數了。那由 2x = n 能夠得出 x = log2n,因此這兩個函數的時間複雜度爲 O(log2n)。
再看下面兩個函數的時間複雜度
function total1(n) {
var sum = 0;
var i = 1;
while (i <= n) {
sum += i;
i = i * 3;
}
}
function total2(n) {
var sum = 0;
for (var i = 1; i <= n; i = i * 3) {
sum += i;
}
}
複製代碼
由上能夠得知,這兩個函數的時間複雜度爲 O(log3n) 。
因爲咱們能夠忽略常數,也能夠忽略對數中的底數,因此在對數階複雜度中,統一表示爲 O(logn);那 O(nlogn) 的含義就很明確了,時間複雜度 爲O(logn) 的代碼執行了 n 次。
再來看一段特殊的代碼時間複雜度,好比說
function total(m,n) {
var sum1 = 0;
for (var i = 0; i < n; i++) {
sum1 += i;
}
var sum2 = 0;
for (var i = 0; i < m; i++) {
sum2 += i;
}
return sum1 + sum2;
}
複製代碼
由於咱們沒法評估 m 和 n 誰的量級比較大,因此就不能忽略掉其中一個,這個函數的複雜度是有兩個數據的量級來決定的,因此此函數的時間複雜度爲 O(m+n);那麼 O(m*n) 的時間複雜度相似。
空間複雜度的話和時間複雜度相似推算便可。 所謂空間複雜度就是表示算法的存儲空間和數據規模之間的關係。
好比說分析下面代碼的空間複雜度:
function initArr(n) {
var arr = [];
for (var i = 0; i < n; i++) {
arr[i] = i;
}
}
複製代碼
根據時間複雜度的推算,忽略掉常數量級,每次數組賦值都會申請一個空間存儲變量,因此此函數的空間複雜度爲 O(n)。
常見的空間複雜度只有 O(1)、O(n)、O(n2)。其餘的話不多會用到。
如今咱們回到開始的思考題上,代碼實現很簡單:
function total(n) {
var sum = 0;
for (var i = 1; i <= n; i++) {
sum += i;
}
return sum;
}
複製代碼
此函數的時間複雜度你如今應該很容易就能看出來了,爲 O(n)。
我以爲這個時間複雜度有點高了,我想要 O(1) 的時間複雜度函數來實現這個算法,能夠嗎?
能夠的,小數學神通高斯教會咱們一招,以下
function total(n) {
var sum = n*(n+1)/2
return sum;
}
複製代碼
此函數的時間複雜度僅僅爲 O(1),在數據規模比較龐大的時候,下面的函數是否是明顯比上面的函數運算效率更高呢。
複雜度也叫漸進複雜度,包括時間複雜度和空間複雜度,一個表示執行的快慢,一個表示內存的消耗,用來分析算法執行效率與數據規模之間的增加關係,能夠粗略的表示,越高階複雜度的算法,執行效率越低。
學習了複雜度分析後,是否是能避免寫出效率低的代碼呢?來給你的代碼作個分析吧。
若是有錯誤或者錯別字,還請給我留言指出,謝謝。
咱們下期見。