[數據結構] 複雜度分析---時間、空間複雜度

寫在前面,本章將瞭解到:
1.什麼是時間、空間複雜度?爲什麼要進行時間、空間複雜度分析?
2.大O表示法
3.分析時間、空間複雜度的方法
4.複雜度分析的四個方面:最好情況時間複雜度,最壞情況時間複雜度,平均情況時間複雜度,均攤時間複雜度


1.什麼是時間、空間複雜度分析?

數據結構和算法本身解決的是“快”和“省”的問題,如何讓代碼運行的更快,更省存儲空間?執行效率就是算法一個非常重要的考量指標,那麼如何來衡量算法的執行效率呢?就是用時間、空間複雜度分析。

關鍵點:

  • 1.數據結構和算法解決的是 “如何讓計算機更快時間、更省空間的運行” 的問題
  • 2.複雜度分析需要從 執行時間佔用空間 兩個維度去評估數據結構和算法的性能

2.爲什麼要進行時間、空間複雜度分析?

原因:

  • ①測試非常依賴環境,同一段代碼 在 Core I9 上運行肯定比 在 CoreI3上運行快,也就是說會受到硬件影響
  • ②測試依賴數據規模,同一段代碼處理 10條數據 和處理百萬條數據,肯定是數據少的快

所以,我們需要一個不用具體測數據,就可以粗略估計算法執行效率的方法


3.複雜度表示法–大O複雜度表示法

我們看一段代碼,求 1,2,3,4,…,n的累加和:

 int cal(int n) {
   int sum = 0;
   int i = 1;
   for (; i <= n; ++i) {
     sum = sum + i;
   }
   return sum;
 }

當我們粗略估計這段代碼的執行時間時,我們假設每行代碼的執行時間一樣,用 unit_time 來表示,
2,3行分別都需要 一個 unit_time,
4,5行在for 循環裏,也就是 2 * n * unit_time,
所以這段代碼的總執行時間爲 T(n) = (2 + 2n)* unit_time

由上述計算可以看出,所有代碼的執行時間T(n)與每行代碼的執行次數 n 成正比

我們再來看下面的代碼:

 int cal(int n) {
   int sum = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1;
     for (; j <= n; ++j) {
       sum = sum +  i * j;
     }
   }
 }

我們依然假設每行代碼的執行時間爲 unit_time
2,3,4行代碼總共是 3 * unit_time
5,6行代碼是 2 * n * unit_time
7,8行代碼是 2 * n2 * unit_time
所以,整段代碼執行時間爲T(n)=(2n2 + 2*n +3)*unit_time

依舊符合,我們之前發現的規律:所有代碼的執行時間T(n)與每行代碼的執行次數 n 成正比

我們把這個規律總結成一個公式:
T(n) = O(f(n))

T(n) 代碼代碼執行時間, n 表示數據規模的大小 ,**f(n)表示每行代碼執行的次數總和,O 表示代碼的執行時間T(n)f(n)**表達式成正比

第一個例子中T(n) = O(2n+2),
第二個例子中T(n) = O(2n2 + 2n +3)
這就是大O表示法,代表代碼執行時間隨數據規模增長的變化趨勢,也叫漸進時間複雜度,簡稱時間複雜度


4.分析時間複雜度的方法

①只關注循環執行次數最多的一段代碼

我們在分析一個算法,一段代碼的時間複雜度時候,只關注循環執行次數最多的那一段代碼就可以
我們拿第一個例子來說:

 int cal(int n) {
   int sum = 0;
   int i = 1;
   for (; i <= n; ++i) {
     sum = sum + i;
   }
   return sum;
 }

這個例子中,2、3行代碼都是常量執行時間,循環次數最多的是4、5行代碼,這兩行代碼被執行 n 次,總的時間複雜度就是O(n)

②加法法則:總複雜度等於量級最大的那段代碼的複雜度

來看一個複雜的例子:

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;
 }

該例子,總共可以分爲3個部分,sum_1,sum_2,sum_3,我們可以分析每一部分的時間複雜度,然後放到一起,取一個量級最大的作爲整段代碼的複雜度

第一段,sum_1的被循環執行了100次,是一個常量級的執行時間,跟 n 無關
第二段是O(n),第三段是O(n2)

根據我們之前說的,總的時間複雜度就等於量級最大的那段時間複雜度,也就是這個例子整段代碼的時間複雜度就爲O(n2)

3.乘法法則:嵌套代碼的複雜度等於嵌套內外代碼複雜度的乘積

先來看一個例子:

int cal(int n) {
   int ret = 0; 
   int i = 1;
   for (; i < n; ++i) {
     ret = ret + f(i);
   } 
 } 
 
 int f(int n) {
  int sum = 0;
  int i = 1;
  for (; i < n; ++i) {
    sum = sum + i;
  } 
  return sum;
 }

我們先看單獨的函數 cal(),假設 f() 只是一個普通的操作,那第 4-6 行的時間複雜度就是 T1(n) = O(n),但是 f() 函數本身就有時間複雜度 T2(n) = O(n),所以整個 cal() 的時間複雜度就是
T(n) = T1(n) * T2(n) = O(n2)


4.常見時間複雜度實例分析

我們先了解下,複雜度量級可以粗略的分爲兩類:多項式量級非多項式量級
在這裏插入圖片描述
非多項式量級只有兩個 O(2n) O(n!),非多項式量級的算法問題叫做NP(Non-Deterministic Polynomial,非確定多項式)問題

當數據規模 n 越來越大的時候,非多項式量級算法的執行時間會急劇增加,所以效率低下

多項式時間複雜度

①O(1)

O(1)是常量級時間複雜度的一種表示方法,並不是只執行了一行代碼只要代碼的執行時間不隨 n 的增大而增長,這樣的代碼都可以是O(1),一般情況下,只要算法中不存在循環語句、遞歸語句,即使有成千上萬的代碼,其時間複雜度也是O(1)

②O(logn) O(nlogn) 對數時間複雜度

我們先看一段代碼,

 i=1;
 while (i <= n)  {
   i = i * 2;
 }

根據我們之間所說的,第三行代碼執行次數最多,我們只要計算這個行代碼執行了多少次就行
根據代碼,我們可以看到,變量 i 從1開始取值,每次循環就乘以 2,大於 n 時就循環結束。實際上就是一個等比數列
在這裏插入圖片描述
所以我們只要知道 x 是多少,就知道執行了多少次
所以 x = log2n,時間複雜度就是 O(log2n)

當我們一段代碼的時間複雜度是O(logn),那麼當這段代碼被執行了 n 次,那時間複雜度就是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;
}

從代碼中可以看出, m 和 n表示的是兩個數據規模,但是無法評估是哪個數據規模較大,所以我們就乾脆把兩個加起來 O(m + n)


5.空間複雜度分析

時間複雜度的全稱是漸進時間複雜度,表示算法的執行時間與數據規模之間的增長關係,
空間複雜度就是漸進空間複雜度,表示算法的存儲空間與數據規模之間的增長關係

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)

總結:
複雜度也叫漸進複雜度,包括時間複雜度和空間複雜度,用來分析算法執行效率與數據規模之間的增長關係,越高階複雜度的算法,執行效率越低
在這裏插入圖片描述


6.複雜度分析的四個方面

最好情況時間複雜度最壞情況時間複雜度平均情況時間複雜度均攤時間複雜度

①最好、最壞情況時間複雜度

我們看一段代碼:

// 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;
  }
  return pos;
}

這段代碼功能就是在數組中,查找變量 x 出現的位置,如果沒有出現就返回 -1,這段代碼的時間複雜度就是O(n),執行了 n 次

但是並不是 x都在數組最後,也許 x 出現在了中間呢,假如我們在找到 x 以後,break 跳出了循環呢?那意味着這段代碼的時間複雜度就不是 O(n) 了

// 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;
}

假如,正好第一個元素就是我們要找的 x ,那就不需要遍歷剩下的 n - 1個數據了,時間複雜度就是O(1)
假如,數組中不存在 x ,那我們就遍歷了整個數組,時間複雜度就是 O(n)

鑑於上面的這種情況,我們纔有了最好情況時間複雜度最壞情況時間複雜度平均情況時間複雜度
最好情況時間複雜度:在最理想的情況下,執行這段代碼的時間複雜度,就比如我們剛剛說的,第一個元素就是我們要找的 x

最壞情況時間複雜度:在最糟糕的情況下,執行這段代碼的時間複雜度,我們數組中沒有要查找的 x,遍歷了整個數組

平均情況時間複雜度:最好情況時間複雜度和最壞情況時間複雜度都是很極端的情況,爲了更好的表示時間複雜度,就有了平均情況時間複雜度

如何分析平均情況時間複雜度呢?
還是剛剛那個例子,要查找 x 在數組中的位置, 有 n+1 中情況,即在數組的 0 到 n-1 位置中不在數組中,我們把每種情況需要遍歷的元素個數相加,除以 n+1 ,
在這裏插入圖片描述
當我們省略掉低階,常量和係數的時候,得到的平均時間複雜度就是O(n)

但是,這 n + 1 種情況的概率並不是一樣的,在數組中和不在數組中的概率是 1/2,在數組中的情況下,每種情況的概率又是1/n,當我們考慮概率的情況之後,
在這裏插入圖片描述

這個值去掉係數和常量以後時間複雜度還是 O(n),
其實這個值就是概率中的加權平均值,也叫作期望,所以平均時間複雜度的全稱應該叫做加權平均時間複雜度或者期望時間複雜度

②均攤時間複雜度

先看一段代碼:

 // array 表示一個長度爲 n 的數組
 // 代碼中的 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;
 }

這段代碼的功能就是,在一個數組中插入了數據,當數組滿了以後,就用for循環去遍歷數組求和,並且清空數組,將求和之後的 sum 放到數組的第一個位置,再將新的數據插入

我們來分析下這段代碼的時間複雜度,
最好的情況:數組中有空閒的空間,每次只需要插入就行,時間複雜度爲O(1),
最壞的情況:數組中沒有空閒的空間,需要遍歷整個數組求和,再將數據插入,時間複雜度爲O(n)

那麼此時,平均時間複雜度是多少呢?
假設數組長度是 n,根據數據插入的位置不同,
我們分爲 n 種情況每種情況的時間複雜度都是O(1)
還有一種額外的情況,就是沒有空閒位置的時候,時間複雜度是O(n),但是概率也是一樣的,都是1/(n+1),按照我們加權平均的算法,平均時間複雜度就是

在這裏插入圖片描述
我們想想看是不是有更好的辦法呢????
先對比一下,這個insert() 的例子和前面的那個 find() 的例子,
find()在極端情況下才會出現 O(1)的情況,但是inser()在大部分情況下都是O(1),只有個別情況是O(n),這是第一個區別

第二個區別,對於insert()函數來說,O(1)和O(n)出現的頻率是非常有規律的,一般都是O(n)之後緊跟着 n - 1 個O(1)

我們引入了均攤時間複雜度,通過均攤分析得到的時間複雜度。。。

那究竟如果使用均攤分析來分析均攤時間複雜度呢?

例如,我們拿插入數據的這個例子,每一次O(n)的插入操作,都會跟着 n - 1 次O(1) 的插入操作,所以把耗時多的那次操作均攤到接下來的 n-1 次耗時少的操作上,那麼這一組連續的操作的均攤時間複雜度就是 O(1)

個人觀點,均攤時間複雜度是一種特殊的平均時間複雜度,主要是針對一些很特殊的情況,例如,剛剛的那個例子,大部分情況都是低複雜度,只有個別情況出現了高複雜度的,而且還有一定的規律,我們就可以試試用均攤複雜度的分析方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章