1. 複雜度:如何衡量程序運行的效率?

複雜度是什麼

複雜度是衡量代碼運行效率的重要的度量因素

如何衡量複雜度

  1. 這段代碼消耗的資源是什麼

一般而言,代碼執行過程中會消耗計算時間和計算空間,那需要衡量的就是時間複雜度和空間複雜度

  1. 這段代碼對於資源的消耗是多少

我們不會關注這段代碼對於資源消耗的絕對量,因爲不管是時間還是空間,它們的消耗程度都與輸入的數據量高度相關,輸入數據少時消耗自然就少。爲了更客觀地衡量消耗程度,我們通常會關注時間或者空間消耗量與輸入數據量之間的關係

如何計算複雜度

複雜度是一個關於輸入數據量 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 元,求總共有多少種可能性

  1. 暴力解法
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() 的時間複雜度
  1. 無效操作處理

代碼中最內層的 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 ] 中,查找出現次數最多的數值

  1. 暴力解法
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²)。而且代碼中,幾乎沒有冗餘的無效計算。如果還需要再去優化,就要考慮採用一些數據結構方面的手段,來把時間複雜度轉移到空間複雜度了

  1. 時空轉換,空間換時間
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

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