複雜度是什麼
複雜度是衡量代碼運行效率的重要的度量因素
如何衡量複雜度
- 這段代碼消耗的資源是什麼
一般而言,代碼執行過程中會消耗計算時間和計算空間,那需要衡量的就是時間複雜度和空間複雜度
- 這段代碼對於資源的消耗是多少
我們不會關注這段代碼對於資源消耗的絕對量,因爲不管是時間還是空間,它們的消耗程度都與輸入的數據量高度相關,輸入數據少時消耗自然就少。爲了更客觀地衡量消耗程度,我們通常會關注時間或者空間消耗量與輸入數據量之間的關係
如何計算複雜度
複雜度是一個關於輸入數據量 n 的函數
通常,複雜度的計算方法遵循以下幾個原則:
- 首先,複雜度與具體的常係數無關,例如 O(n) 和 O(2n) 表示的是同樣的複雜度。我們詳細分析下,O(2n) 等於 O(n+n),也等於 O(n) + O(n)。也就是說,一段 O(n) 複雜度的代碼只是先後執行兩遍 O(n),其複雜度是一致的
- 其次,多項式級的複雜度相加的時候,選擇高者作爲結果,例如 O(n²)+O(n) 和 O(n²) 表示的是同樣的複雜度。具體分析一下就是,O(n²)+O(n) = O(n²+n)。隨着 n 越來越大,二階多項式的變化率是要比一階多項式更大的。因此,只需要通過更大變化率的二階多項式來表徵複雜度就可以了
時間複雜度與代碼結構的關係
代碼的時間複雜度,與代碼的結構有非常強的關係,我們一起來看一些具體的例子
例 1,定義了一個數組 a = [1, 4, 3],查找數組 a 中的最大值,代碼如下:
public void s1() {
int a[] = { 1, 4, 3 };
int max_val = -1;
for (int i = 0; i < a.length; i++) {
if (a[i] > max_val) {
max_val = a[i];
}
}
System.out.println(max_val);
}
這個例子比較簡單,實現方法就是,暫存當前最大值並把所有元素遍歷一遍即可。因爲代碼的結構上需要使用一個 for 循環,對數組所有元素處理一遍,所以時間複雜度爲 O(n)。
例2,下面的代碼定義了一個數組 a = [1, 3, 4, 3, 4, 1, 3],並會在這個數組中查找出現次數最多的那個數字
public void s1() {
int a[] = { 1, 3, 4, 3, 4, 1, 3 };
int val_max = -1;
int time_max = 0;
int time_tmp = 0;
for (int i = 0; i < a.length; i++) {
time_tmp = 0;
for (int j = 0; j < a.length; j++) {
if (a[i] == a[j]) {
time_tmp += 1;
}
if (time_tmp > time_max) {
time_max = time_tmp;
val_max = a[i];
}
}
}
System.out.println(val_max);
}
這段代碼中,我們採用了雙層循環的方式計算:第一層循環,我們對數組中的每個元素進行遍歷;第二層循環,對於每個元素計算出現的次數,並且通過當前元素次數 time_tmp 和全局最大次數變量 time_max 的大小關係,持續保存出現次數最多的那個元素及其出現次數。由於是雙層循環,這段代碼在時間方面的消耗就是 n*n 的複雜度,也就是 O(n²)
在這裏,我們給出一些經驗性的結論:
- 一個順序結構的代碼,時間複雜度是 O(1)
- 二分查找,或者更通用地說是採用分而治之的二分策略,時間複雜度都是 O(logn)
- 一個簡單的 for 循環,時間複雜度是 O(n)
- 兩個順序執行的 for 循環,時間複雜度是 O(n)+O(n)=O(2n),其實也是 O(n)
- 兩個嵌套的 for 循環,時間複雜度是 O(n²)
降低時間複雜度的必要性
假設某個計算任務需要處理 10萬 條數據。你編寫的代碼:
- 如果是 O(n²) 的時間複雜度,那麼計算的次數就大概是 100 億次左右
- 如果是 O(n),那麼計算的次數就是 10萬 次左右
- 如果這個工程師再厲害一些,能在 O(log n) 的複雜度下完成任務,那麼計算的次數就是 17 次左右
總結
複雜度通常包括時間複雜度和空間複雜度。在具體計算複雜度時需要注意以下幾點。
- 它與具體的常係數無關,O(n) 和 O(2n) 表示的是同樣的複雜度
- 複雜度相加的時候,選擇高者作爲結果,也就是說 O(n²)+O(n) 和 O(n²) 表示的是同樣的複雜度
- O(1) 也是表示一個特殊複雜度,即任務與算例個數 n 無關
- 複雜度細分爲時間複雜度和空間複雜度,其中時間複雜度與代碼的結構設計高度相關;空間複雜度與代碼中數據結構的選擇高度相關
拓展:降低複雜度的案例
假設有任意多張面額爲 2 元、3 元、7 元的貨幣,現要用它們湊出 100 元,求總共有多少種可能性
- 暴力解法
public void s2_1() {
int count = 0;
for (int i = 0; i <= (100 / 7); i++) {
for (int j = 0; j <= (100 / 3); j++) {
for (int k = 0; k <= (100 / 2); k++) {
if (i * 7 + j * 3 + k * 2 == 100) {
count += 1;
}
}
}
}
System.out.println(count);
}
使用了 3 層的 for 循環。從結構上來看,是很顯然的 O( n³ ) 的時間複雜度
- 無效操作處理
代碼中最內層的 for 循環是多餘的,主要確定了其餘兩個第三個也就能算出來
public void s2_2() {
int count = 0;
for (int i = 0; i <= (100 / 7); i++) {
for (int j = 0; j <= (100 / 3); j++) {
if ((100-i*7-j*3 >= 0)&&((100-i*7-j*3) % 2 == 0)) {
count += 1;
}
}
}
System.out.println(count);
}
代碼的結構由 3 層 for 循環,變成了 2 層 for 循環。很顯然,時間複雜度就變成了O(n²)
查找出一個數組中,出現次數最多的那個元素的數值。例如,輸入數組 a = [1,2,3,4,5,5,6 ] 中,查找出現次數最多的數值
- 暴力解法
public void s2_3() {
int a[] = { 1, 2, 3, 4, 5, 5, 6 };
int val_max = -1;
int time_max = 0;
int time_tmp = 0;
for (int i = 0; i < a.length; i++) {
time_tmp = 0;
for (int j = 0; j < a.length; j++) {
if (a[i] == a[j]) {
time_tmp += 1;
}
if (time_tmp > time_max) {
time_max = time_tmp;
val_max = a[i];
}
}
}
System.out.println(val_max);
}
程序採用了兩層的 for 循環,很顯然時間複雜度就是 O(n²)。而且代碼中,幾乎沒有冗餘的無效計算。如果還需要再去優化,就要考慮採用一些數據結構方面的手段,來把時間複雜度轉移到空間複雜度了
- 時空轉換,空間換時間
public void s2_4() {
int a[] = { 1, 2, 3, 4, 5, 5, 6 };
Map<Integer, Integer> d = new HashMap<>();
for (int i = 0; i < a.length; i++) {
if (d.containsKey(a[i])) {
d.put(a[i], d.get(a[i]) + 1);
} else {
d.put(a[i], 1);
}
}
int val_max = -1;
int time_max = 0;
for (Integer key : d.keySet()) {
if (d.get(key) > time_max) {
time_max = d.get(key);
val_max = key;
}
}
System.out.println(val_max);
}
參考:
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=185#/detail/pc?id=3339
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=185#/detail/pc?id=3340