2.時間復雜度和空間復雜度

1.算法好壞的度量方法

  1. 事后統(tǒng)計方法:用設計好的測試程序和數據,對完成的算法進行測試,從而確定算法效率的高低
  2. 事先分析估算方法:在實現算法之前,使用事先統(tǒng)計方法對算法進行估算

考慮到成本和不同機器的差異,我們在解決問題選取一個算法時,一般采用事先分析估算方法

2. 事先分析估算的衡量標準

我們如何事先來衡量一個算法的好壞呢?當然是既要馬兒跑得快,又要馬兒吃得少

可以從下面兩個方面來衡量:

  1. 時間復雜度:跑得快,就是算法效率的體現,對于同一規(guī)模的輸入,處理的時間越快,算法越好
  2. 空間復雜度:吃得少,就是算法開銷的體現,對于同一規(guī)模的輸入,算法所需存儲空間的開銷越低,算法越好
總結:使用時間復雜度和空間復雜度來事先評判一個算法的好壞

3. 時間復雜度

3.1 定義

時間復雜度就是用來衡量程序隨著輸入規(guī)模的增大,程序需要執(zhí)行時間的增長規(guī)模。我們關注的是增長的量級,而不是具體的運行時間。

我們用下面的表達式來表示程序運行時間T(n)關于程序輸入規(guī)模n的關系

T(n) = O(f(n))

關于上面的表達式,做如下介紹:

T(n): 程序執(zhí)行時間關于數據輸入規(guī)模n的函數
O(f(n)): 大O時間復雜度表示法
f(n):程序執(zhí)行代碼行數總和關于輸入規(guī)模n的函數
n: 數據輸入規(guī)模

我們可以這么理解上面的表達式:

程序的執(zhí)行時間T(n)和程序執(zhí)行代碼行數總和f(n)并不完全對等,因為我們的高級語言需要被解釋編譯才能被計算機執(zhí)行,但是它們有一定的關系,理論上說,程序需要執(zhí)行代碼行數的總和f(n)越多,需要執(zhí)行的時間T(n)就越長。所以,我們只需要關心執(zhí)行代碼行數的總和f(n)關于輸入規(guī)模n的增長規(guī)模,為了更好地表達增長量級,我們使用大O來表示。

3.2 計算方法

我們來計算1 + 2 + 3 + .... n 的和

有如下兩種算法

算法1:


    private int method1(int n) {
        int sum = 0;    // 1次
        for (int i = 1; i <= n; i++) {    // n次
            sum += i;  // n次
        }
        return sum;  
    }

算法2:

    private int method2(int n) {
        return  (n + 1) * n / 2;  // 1次
    }`

易知,兩個算法代碼執(zhí)行行數總和f(n)如下:

算法1: f(n) = 2n + 1

算法2: f(n) = 1

圖形表示如下所示:


image.png

可以看出,算法1中隨著輸入規(guī)模n的增長,f(n)呈線性增長; 而算法2,不管輸入規(guī)模n為多少,f(n)恒為1。

那我們如何用大O來表示時間復雜度呢?

既然是表示增長規(guī)模,我們就要重點關注主要影響因素,忽略次要影響因素,總結如下:

  1. 找出程序執(zhí)行代碼行數總和f(n)關于輸入規(guī)模n的函數,即f(n)
  2. 保留n的最高次項,去除其他次項,并把與最高次項相乘的常數設置為1

我們拿上面的兩個算法來舉例

首先,找到程序執(zhí)行代碼行數總和f(n)關于輸入規(guī)模n的函數f(n),即:
f(n) = 2n + 1 和 f(n) = 1

保留n的最高次項,去除其他次項,并把與最高次項相乘的常數設置為1:
n 和 1

所以兩個算法的時間復雜度用大O表示為:
O(n) 和 O(1)

3.3 關于時間復雜度計算的探索

上面我們說到,計算時間復雜度的計算規(guī)則如下:

保留最高次項,并把與最高次項相乘的常數設置為1

根據這個規(guī)則,我們可以很輕易地算出下列算法的時間復雜度

序號 程序執(zhí)行代碼行數總和 時間復雜度
1 f(n) = 1 O(1)
2 f(n) = 3 O(1)
3 f(n) = n + 1 O(n)
4 f(n) = 2n + 1 O(n)
5 f(n) = n^2 + 1 O(n^2)
6 f(n) = n^2 + 2n + 1 O(n^2)

可以看到,雖然有6個算法,但是它們實際上只要3個時間復雜度來表示,分別為O(1)、O(n)、O(n^2)

我們把f(n)在圖形上呈現出來:

image.png

從圖形上可以看出

f(n) = n^2 + 2n + 1 和 f(n) = n^2 + 1 的增長規(guī)模是同一量級的,增長較快

f(n) = 2n + 1 和 f(n) = n + 1的增長規(guī)模是同一量級的,增長緩慢

f(n) = 3 和 f(n) = 1的增長規(guī)模是同一量級的,恒為常數,不增長

我們可以很容易地把6個算法分成3組,即它們對應的時間復雜度分別為:O(n^2)、O(n)、O(1)

3.4 如何在代碼層面快速看出時間復雜度

1. 抓大放小

根據上面的規(guī)則,我們只要關注最高次項,在代碼層面,只要關注最復雜的循環(huán)就可以了,比如下面的代碼

    private void test(int n) {
        int sum = 0;   // 1 次

        for (int i = 0; i < n; i++) {
            sum = sum + i;     // n 次
        }

        // 這里是最復雜的循環(huán)
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; i++) {
                sum  = sum + i * j;     // n^2 次
            }
        }
    }

可以看出,f(n) = n^2 + n + 1,時間復雜度為O(n^2)。

在上面的代碼里,我們只要找最復雜的循環(huán),即雙層循環(huán),所以時間復雜度就是O(n^2)

2. O(n+m)

我們之前提到的數據輸入規(guī)模,一般都是單個輸入n,但程序可以存在多個輸入,比如下面的算法,程序執(zhí)行代碼的行數,受到n和m兩個參數的影響

    private void oNAndM(int n, int m) {
        int sum = 0;   // 1 次

        for (int i = 0; i < n; i++) {
            sum = sum + i;     // n 次
        }

        for (int i = 0; i < m; i++) {
            sum = sum + i;     // m 次
        }
    }

上面的f(n,m) = n + m + 1,因為不知道m(xù)和n的具體關系,所以m不能被忽略,這時候時間復雜度就是:O(n+m)

3. O(n * m)

上面提到的是n和m是平行的,沒有相互影響,所以是相加,而如果n、m是嵌套的,則需要相乘,如下:

    private void oNMultM(int n, int m) {
        int sum = 0;   // 1 次

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; i++) {
                sum = sum + i * j;     // n * m 次
            }
        }
    }

可以知道f(n,m) = n * m + 1,則時間復雜度為:O(n * m)

其實2次方階O(n^2)可以看成n和m相等的特殊情況

3.5 常見的時間復雜度

我們平??吹降乃惴〞r間復雜度一般都是以下這幾種或這幾種的組合

序號 時間復雜度 名稱
1 O(1) 常數階
2 O(logn) 對數階
3 O(n) 線性階
4 O(n^k) k次方階 (比如k=2,即O(n^2),則是平方階)

我們來看看這幾種時間復雜度的算法體現

1. O(1)

f(n)不會隨輸入規(guī)模的n增大而變化

    private void o1(int n) {
        int i = n;
        int sum = i * 2;
        System.out.print("sum 為:" + sum);
    }

可以看到,不管n為多少,f(n) 恒為3,就算f(n)恒為1000還是10000,時間復雜度都是O(1)

2. O(logn)
1.    private int oLogn(int n) {
2.        int i = 1;
3.        while (i < n) {
4.           i = i * 2;  
5.      }
6.      return i;
7.   }

可以看到,上述代碼,主要的執(zhí)行代碼是循環(huán)體中的第4行代碼,而上述的算法就是:i 初始化為1,每次循環(huán)乘以2,當 i > n時,結束循環(huán),我們假設x次循環(huán)后,跳出循環(huán),則i的值如下:
2^0、 2^1、 2^2.... 2^x ,所以滿足2^x >= n,則x >= log2n(這里的2是底數)

所以x就是第一個大于等于log2n的整數,對數可以相互轉換底數,且我們求的是數量級,所以我們可以不關心底數,直接用O(logn)來表示所有的對數階

3. O(n)
    private int oN(int n) {
        int sum = 1;
        for (int i = 0; i < n; i ++) {
            sum = sum + i;
        }
        return sum;
    }

這種線性階,其實就是單個的for循環(huán)

4. O(n^2)
    private void oN2(int n) {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; i++) {
                sum  = sum + i * j;     // n^2 次
            }
        }
    }

這就是平方階,其實就是2個循環(huán)。如果3個循環(huán),就是立方階,k個循環(huán),就是k次方階

4. 空間復雜度

空間復雜度表示的是存儲空間跟數據輸入規(guī)模的增長關系。

算法所需的存儲空間可以使用如下表達式來表示:

S(n) = O(f(n))

跟時間復雜度類似,空間復雜度也是用大O來表示。

我們來分析如下兩個算法的空間復雜度

算法1

    public void o1() {
        int i =1;  // 1個
    }

可以看到,我們只需要申請一個存儲空間給i,所以空間復雜度為O(1)

算法2

    public void oN(int n) {
        int[] arr = new int[n];  // n個
    }

易知,我們需要申請n個存儲空間給數組arr,所以空間復雜度為O(n)

關于空間復雜度的計算跟時間復雜度一樣,都是保留最高次項,并把與最高次項相乘的常數設置為1,這里就不詳細描述了

5. 復雜度的排序

對于我們常見的復雜度,從低階到高階的排序如下:

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) 

評估一個算法,時間復雜度和空間復雜度,都是越低越好

6. 應用

在后續(xù)的學習中,不管是各種數據結構的CURD操作還是評判各種算法好壞,我們都會從時間復雜度和空間復雜度兩個方面來分析。

比如遍歷HashMap的時間復雜度是多少?快速排序的平均時間復雜度和空間復雜度是多少?

我們沒必要去記住它們,而是要懂得分析的方法,然后應用在日常的編程中,利于我們更好地選取數據結構和算法,你鋼鐵了,自然就戰(zhàn)士了

7. 思考

關于《智行火車票》把加速包開到最大搶到票以后,智行上不付錢,然后登錄12306在未完成訂單中以原價購買

如果你是智行的員工

  1. 這種情況可以從技術層面避免嗎?為什么?
  2. 如果不能避免,怎么去優(yōu)化?
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容