2.時(shí)間復(fù)雜度和空間復(fù)雜度

1.算法好壞的度量方法

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

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

2. 事先分析估算的衡量標(biāo)準(zhǔn)

我們?nèi)绾问孪葋砗饬恳粋€(gè)算法的好壞呢?當(dāng)然是既要馬兒跑得快,又要馬兒吃得少

可以從下面兩個(gè)方面來衡量:

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

3. 時(shí)間復(fù)雜度

3.1 定義

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

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

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

關(guān)于上面的表達(dá)式,做如下介紹:

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

我們可以這么理解上面的表達(dá)式:

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

3.2 計(jì)算方法

我們來計(jì)算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次
    }`

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

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

算法2: f(n) = 1

圖形表示如下所示:


image.png

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

那我們?nèi)绾斡么驩來表示時(shí)間復(fù)雜度呢?

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

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

我們拿上面的兩個(gè)算法來舉例

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

保留n的最高次項(xiàng),去除其他次項(xiàng),并把與最高次項(xiàng)相乘的常數(shù)設(shè)置為1:
n 和 1

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

3.3 關(guān)于時(shí)間復(fù)雜度計(jì)算的探索

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

保留最高次項(xiàng),并把與最高次項(xiàng)相乘的常數(shù)設(shè)置為1

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

序號 程序執(zhí)行代碼行數(shù)總和 時(shí)間復(fù)雜度
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個(gè)算法,但是它們實(shí)際上只要3個(gè)時(shí)間復(fù)雜度來表示,分別為O(1)、O(n)、O(n^2)

我們把f(n)在圖形上呈現(xià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ī)模是同一量級的,恒為常數(shù),不增長

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

3.4 如何在代碼層面快速看出時(shí)間復(fù)雜度

1. 抓大放小

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

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

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

        // 這里是最復(fù)雜的循環(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,時(shí)間復(fù)雜度為O(n^2)。

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

2. O(n+m)

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

    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,因?yàn)椴恢續(xù)和n的具體關(guān)系,所以m不能被忽略,這時(shí)候時(shí)間復(fù)雜度就是: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,則時(shí)間復(fù)雜度為:O(n * m)

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

3.5 常見的時(shí)間復(fù)雜度

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

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

我們來看看這幾種時(shí)間復(fù)雜度的算法體現(xiàn)

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,時(shí)間復(fù)雜度都是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,當(dāng) i > n時(shí),結(jié)束循環(huán),我們假設(shè)x次循環(huán)后,跳出循環(huán),則i的值如下:
2^0、 2^1、 2^2.... 2^x ,所以滿足2^x >= n,則x >= log2n(這里的2是底數(shù))

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

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

這種線性階,其實(shí)就是單個(gè)的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 次
            }
        }
    }

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

4. 空間復(fù)雜度

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

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

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

跟時(shí)間復(fù)雜度類似,空間復(fù)雜度也是用大O來表示。

我們來分析如下兩個(gè)算法的空間復(fù)雜度

算法1

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

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

算法2

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

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

關(guān)于空間復(fù)雜度的計(jì)算跟時(shí)間復(fù)雜度一樣,都是保留最高次項(xiàng),并把與最高次項(xiàng)相乘的常數(shù)設(shè)置為1,這里就不詳細(xì)描述了

5. 復(fù)雜度的排序

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

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

評估一個(gè)算法,時(shí)間復(fù)雜度和空間復(fù)雜度,都是越低越好

6. 應(yīng)用

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

比如遍歷HashMap的時(shí)間復(fù)雜度是多少?快速排序的平均時(shí)間復(fù)雜度和空間復(fù)雜度是多少?

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

7. 思考

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

如果你是智行的員工

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容