1.說說你不知道的時間複雜度

盛開的太陽發表於2022-01-26

一、為什麼要學習資料結構和演算法

其實,以前我們都會說,學習資料結構有多麼多麼的重要,長篇大論。這次,我們java程式設計師來看看資料結構和演算法重要性。

例題:判斷一個數是否是2的n次方。比如:2,4,8,16是2的n次方;6,10不是。

拿到這道題,用java的思路分析

2:2

4:2*2

8:2 * 2 * 2

16:2 * 2 * 2 * 2

如果一個數除以2,最後餘數是0,那麼這個數就是2的n次方;如果餘數是1,那麼就不是。程式碼實現如下:

public static void main(String[] args) {
				// 隨意給一個數,判斷這個數是不是2的n次方
        int n = 16;
  
        int yuShu = n%2;
        int chuShu = n/2;
        while (chuShu > 1) {
            yuShu = chuShu % 2;
            chuShu = chuShu / 2;
        }

        System.out.println( n + " 這個數" + (yuShu == 0?"是":"不是")+"2的n次方");

    }

但是,如果使用資料結構呢?下面來分析一下:將所有的十進位制數字都轉成2進位制

2:10

4:100

8:1000

16:10000

他們的特點是什麼呢?第一個數字是1,其餘的都是0

而這幾個數字之前的一個數字是什麼呢?

1:01

3:011

7:0111

15:01111

他們的特點是什麼呢?第一個是0,其餘的都是1.

利用這兩組數字,我們找規律,如果i和i-1按位&與的結果是0,就說明這個i是2的n次方;否則就不是

2 & 1 = 0

4 & 3 = 0

8 & 7 = 0

16 & 15 = 0

但是15 & 14呢,14的二進位制數是01110,01111 & 01110 = 00001

所以,通過對資料的分析,我們可以用一句程式碼判斷

if(n & (n=1) == 0) {
  // 這個數就是2的n次方
} else {
  // 否則不是
}

二、資料結構概述

資料結構包括:線性結構和非線性結構

1、線性結構

1)線性結構是最常用的資料結構,其特點是資料元素之間存在一對一的線性關係。

2)線性結構有兩種不同的儲存結構,即順序儲存結構和鏈式儲存結構。順序儲存的線性表成為順序表,順序表中的儲存元素是連續的

3)鏈式儲存的線性表成為連結串列,連結串列中的儲存元素不一定是連續的,元素節點中存放資料元素以及相鄰元素的地址資訊。

4)線性結構常見的有:陣列、佇列、連結串列和棧,後面我們會詳細講解

2、非線性結構

非線性結構包括:二維陣列、多維陣列、廣義表、樹結構、圖結構

三、演算法

演算法有五個特徵:有窮性,確定性,可行性,有輸入,有輸出

正確性,可讀性,健壯性,bug(寫出的程式碼bug少,而且系統穩定)

高效率與低儲存:記憶體+CPU 堆疊記憶體 OOM

記憶體佔用最小,CPU佔用最小,運算速度最快。

評價演算法的兩個重要指標:時間複雜度 和 空間複雜度

時間複雜度:執行一個程式所花費的時間。

空間複雜度:執行程式所需要的記憶體。

1、 時間複雜度

1) 計算時間複雜度的意義:分析介面的效能

2) 時間複雜度表示方法:大寫的O(n)表示,全稱是O(nlogn)

3)常見的時間複雜度

計算時間複雜度,通常是計算比較大的,而且是不確定的的數。如果是已經確定的,那麼就不用計算了,常量就是我們說的不用計算的一種。

> 常數:O(1) 1表示的是常數。不是迴圈的次數

比如下面的這個迴圈,時間複雜度是O(1)

public class BigO {

    public static void main(String[] args) {
        int n = 0; // 這句話的時間複雜度是O(1)
        
        for (int i = 0; i < 3; i++) {  // 這句話會執行4次, 它的時間複雜度也是O(1)
            n = n + 1;  // 這句話會執行3次, 他的時間複雜度也是O(1)
        }
    }
}

> 對數:O(logn) 或 O(nlogn)

那麼什麼情況下會使用時間複雜度是對數的這種情況呢?來看一下下面的程式碼:

public static void time_log() {
        int a = Integer.MAX_VALUE;
        int i = 1;
        while (i <= a) {
            i = i * 2;  // 這個迴圈一共要迴圈多少次呢?
            // 我們來看看i的值2,4,8,16,32,64,128    2^0,2^1,2^2,2^3,2^4,2^5 ====> 2(n) = a ===> n = log2a
        }
    }

這裡i = i * 2 這句話需要迴圈多少次呢?其實我們要求的就是:迴圈多少次,i<= a 呢?2^n = a ; 求n=log2a, log以2為底a的對數。

以上是O(logn)的情況,那麼什麼情況下使用O(nlogn)呢?看下面的程式碼:

public static void time_nlogn() {
        int a = Integer.MAX_VALUE;
        int i = 1;
        for (int j = 0; j < 10; j++) {
            while (i <= a) {
                i = i * 2;  // 這個迴圈一共要迴圈多少次呢?
                // 我們來看看i的值2,4,8,16,32,64,128    2(1),2(2),2(3),2(4),2(5)  2(n) = a ===> n = log2a
                // 外面還有個j,所以就是(j * log2a)次
            }
        }  
    }

只有while迴圈的時候,需要執行log2a次。那麼外面多了一層for迴圈,這次要迴圈多少次呢?(j * log2a)次

> 線性:O(n)

線性指的就是O(n),也就是執行n次。

public static void time_on() {
        int n = Integer.MAX_VALUE;
        int a = 0;
        for (int i = 0; i < n; i++) {
            a = a + 1; // 這句話的時間複雜度是什麼? O(n) n是幾就執行幾次. n是未知的,不確定的.如果n是確定的,就是常量了. 時間複雜度就是O(1)
        }
    }

這裡面a = a + 1;這句話會執行多少次呢?他和n有關係,如果n是10就執行10次,n是100就執行100次。有n決定,所以時間複雜度是O(n);

注意:這裡的n是不確定的。如果n是確定的,那麼時間複雜度就是O(1)le

> 線性對數:O(nlogn)

這個在上面已經說過了

> 平方:O(n ^ 2)

/**
     * 時間複雜度O(n^2)
     */
    public static void time_Onn() {
        int n = Integer.MAX_VALUE;
        int a = 0;
        for(int i = 0; i < n; i++) {
            for (int j = 0; j< n; j++) {
                a = a + 1; // 這句話執行多少次呢?也就是說它的時間複雜度是多少呢? 外層迴圈執行n次,內層迴圈也是n次,所以最終執行n*n次,所以時間複雜度是O(n^2)
            }
        }
    }

這裡有兩層迴圈,外層迴圈執行n次,記憶體迴圈也是n次,所以程式碼a = a + 1;執行O(n*n)次。

常見的O(n^2)還有氣泡排序

public static void time_Onn2() {
        int[] num = {3,6,1,0,8,5};
        int n = num.length;

        int a = 0;
        for(int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                a = a + 1; // 這句話執行多少次呢?也就是說它的時間複雜度是多少呢? 來找找它的規律
                /*
                 * 外層迴圈i=n, 內層程式碼執行1次
                 * 外層迴圈i=n-1,內層程式碼執行2次
                 * 外層迴圈i=n-2,內層程式碼執行3次
                 * 外層迴圈i=n-3,內層程式碼執行4次
                 * 外層迴圈i=1,內層程式碼執行n次
                 * 
                 * 一共執行多少次呢? 0+1+2+3+......+n = n * (n+1)/2次
                 * 也就是冒泡演算法的時間複雜度是O(n*(n+1)/2)次
                 */
            }
        }
    }

氣泡排序的時間複雜度是多少呢?我們來分析一下:

外層迴圈i=n, 內層程式碼執行1次
外層迴圈i=n-1,內層程式碼執行2次
外層迴圈i=n-2,內層程式碼執行3次
外層迴圈i=n-3,內層程式碼執行4次
外層迴圈i=1,內層程式碼執行n次

一共執行多少次呢? 0+1+2+3+......+n = n * (n+1)/2次
也就是冒泡演算法的時間複雜度是O(n*(n+1)/2)次。

在計算時間複雜度的時候去掉常數,所以就是O(n^2)

> N次方:

如果是三層迴圈,四層迴圈呢?那就是 n * n * n * n=n^4

上面的冒泡演算法會執行n*(n+1)/2 = (n^2 + n)/2 ===>當有加減法的時候,這個時間複雜度怎麼計算呢?

取最大的就可以,這個最大是:首先去掉常數後,n2比n的階層高,所以最後是O(n2)

4) 怎麼計算時間複雜度?

第一步:找有迴圈的地方

第二步:找有網路請求的地方,包括RPC協議請求,資料庫請求

​ 網路請求可以通過列印日誌來計算時間。

5) 常見時間複雜度的執行效率

O(1) > O(logn) > O(n) > O(nlogn) > O(n^2) > O(n^x)

O(1)的執行效率是最高的。

所以,我們在優化的時候,目標是將所有的時間複雜度往O(1)進行優化。

實際上,O(1) > O(logn) > O(n) > O(nlogn) 效果都是很好的,幾乎優化的空間不是很大。我們的最終目標就是將O(n^2) 和 O(n^x)往前優化。

  • 案例1:

再來看看最開始的這個案例:判斷一個數是否是2的n次方。比如:2,4,8,16是2的n次方;6,10不是。

如果一個數除以2,最後餘數是0,那麼這個數就是2的n次方;如果餘數是1,那麼就不是。程式碼實現如下:

public static void main(String[] args) {
				// 隨意給一個數,判斷這個數是不是2的n次方
        int n = 16;
  
        int yuShu = n%2;
        int chuShu = n/2;
        while (chuShu > 1) {
            yuShu = chuShu % 2;
            chuShu = chuShu / 2;
        }

        System.out.println( n + " 這個數" + (yuShu == 0?"是":"不是")+"2的n次方");

    }

那麼這段程式碼的時間複雜度是多少呢?log2n:log以2位底n的對數。O(logn)

後來通過二進位制程式碼進行優化,優化後的結果是

if(n & (n=1) == 0) {
  // 這個數就是2的n次方
} else {
  // 否則不是
}

這段程式碼的時間複雜度是O(1)。

我們優化以後,將O(logn)優化為O(1)了,效率大大提高了

  • 案例2

比如,我們在看一段程式碼的時候,發現有效能瓶頸,然後這段程式碼使用的排序方式是氣泡排序,如何對這個排序進行優化呢?

找更優秀的替代排序方法,快速排序,歸併排序,堆排序等。

2、空間複雜度

1)空間複雜度分析的意義

找出哪些地方花費記憶體多,哪些資料佔用的記憶體開銷大

2)如何找出程式的空間複雜度?

開空間的地方,比如:陣列,連結串列,快取Map,Set,佇列Queue,遞迴等

資料結構和演算法書籍推薦

image

相關文章