紮實打牢資料結構演算法根基,從此不怕演算法面試系列之008 week01 02-08 透過常見演算法,對常見的時間複雜度做梳理

皿哥的技術人生發表於2023-04-19

1、線性查詢法的複雜度

public static <E> int search(E [] data,E target){
    for (int i = 0; i < data.length; i++)
        if (data[i].equals(target))
            return i;
    return -1;
}

很容易看出,這個演算法的複雜度為 O(n)


2、一個陣列中的元素可以兩兩組成哪些資料對? O(n²)

需要實現一個演算法,這個演算法用於:找到一個陣列中的元素可以兩兩組成哪些資料對?
①、在不要求順序的情況下,即data[i],data[j]和data[j],data[i]看作同一個資料對;
②、每一個元素自己和自己不能組成資料對,即data[i],data[i]不是資料對。

這個演算法的程式碼如下:

  for(int i=0;i<data.length;i++)
    for (int j=i+1; j< data.length;j++) 
        //獲得一個資料對 (data[i],data[j])

注意,j從i+1開始。
可以分析得出,這個演算法的複雜度為O(n²)

雖然是2重迴圈,但是迴圈執行的次數並不是nn,其實執行的次數為1/2n²,但是1/2作為常數,不重要。


3、遍歷一個n*n的二維陣列 O(n²)

我們來實現一個需求:遍歷一個n*n的二維陣列 ,程式碼如下:

for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++) 
        //遍歷到A[i][j]

上述程式碼的時間複雜度是O(n²)
注:

在做複雜度分析時,一定要明確n是誰?

假設遍歷一個aa的二維陣列 O(a²)=O(n)
而 a
a=n,上面的n表示的是維度為n(每一個維度的元素個數都是n),下面的n表示的是元素總數為n。

 for (int i = 0; i < a; i++)
    for (int j = 0; j < a; j++) 
        //遍歷到A[i][j]

看時間複雜度也好,總結時間複雜度也好,一定一定要明確n到底誰?

mark


4、數字n的二進位制位數 O(logn)

我們來實現一個需求:求解數字n的二進位制位的位數?
如何理解位數,我們來舉個例子,一看就懂:
16這個數字的10進位制位數有幾位?
有2位嘛,1和16

520這個數字的10進位制位數有幾位?
3位嘛,5、2、0


我們看下這個演算法的實現的偽碼:

while(n){
    n%2 //n的二進位制中的一位
    n/=2;
}

上面偽碼的時間複雜度是:O(logn)


不同的底數相差的只是一個常數而已,可以複習下高中數學的換底公式;

所以,最終我們對數複雜度都是不關注底數的,都是O(logn)。


注意,分析演算法複雜度的時候,也不能只看迴圈個數。

這裡n每次除以2,而不是每次減1,所以它到0的速度非常快……所以不是O(n)級別,而是O(logn)級別。


5、求解數字N的所有約數? O(n)、O(√n) :根號n

我們來實現一個需求:我們來實現一個需求。
首先需要回顧下約數的概念,假設a是n的約數,表示n除以a沒有餘數,即n可以整除a

for (int i = 1; i <= n ; i++)
    if (n%i==0)
        // i是n的一個約數

接著,我們來對上述演算法做一個最佳化:

 for (int i = 1; i*i <= n ; i++)
    if (n%i==0) //i和n/i都是n的約數,同時找到兩個約數(需要排除i=n/i的情況)

再次強調,看演算法複雜度,不能看迴圈個數。
上述兩個演算法都是一重迴圈,但他們迴圈的結束條件不同,最佳化前的演算法是i<=n,最佳化後的是i*i<n,所以實際上這兩個演算法的執行次數是非常不同的。
一個是n,一個是根號n。

mark

所以這個演算法的實現,有2種複雜度:
O(n)和O(√n) :根號n。。

6、求所有長度位n的二進位制數字的個數? O(2^n);2的n次方

我們來實現一個需求:求所有長度位n的二進位制數字的個數?


如何理解這個題目,比如,所有長度位3的二進位制數字,一共有幾個?

一共有8個,分別是從0到7
0、1、10、11、100、101、110、111

比如,所有長度位4的二進位制數字。
一共有16個,分別是從0到15
0、1、10、11、100、101、110、111
1000、1001、1010、1011、1100、1101、1110、1111


這裡我們就是用排列組合來得到,長度為n,相當於有n個位置,每個位置或者填0,或者填1;
每個位置有2種選擇,現在有n個位置,則相當於n個2連續乘起來
222……222=2^n,是2的n次方。

所以,這個演算法的複雜度為:
O(2^n),是2的n次方

7、長度為n的陣列的所有排列 O(n!)

我們來是實現一個需求,求解長度為n的陣列的所有排列個數。

舉個例子,比如,1、2、3的排列個數為6,
1、2、3、4的排列個數為24。
這裡用到了數學中的全排列公式。

全排列個數,n!;
所以這個演算法的複雜度為:O(n!)

O(2^n),n次方複雜度和 O(n!)階乘複雜度,都是非常高,效能非常差的複雜度。


O(2^n),當n為10,基本就是1000了,程式設計師都熟悉,實際是1024,
當n為20,基本就是1000*1000=100w了,普通程式設計師都期望追求的年薪數字是吧哈哈
當n為20,基本就是10億了,你的10個小目標?這麼小的目標,MosesMin可不敢有,哈哈哈
甚至當n為100時,科學家統計,它的大小就接近於宇宙中所有原子的個數那麼多個了……
所以指數級別是一個非常恐怖的增長。


通常,演算法設計上,如果n<=20,可以考慮指數級別的複雜度;如果n>20,指數級別的複雜度是承受不起的。
所以演算法設計上,要儘可能避免指數級別的複雜度;
而階乘的複雜度就更高了,更是需要全力避免。
階乘級別也一樣,n<20可以考慮,大於20就不能考慮了。


8、判斷n是否是偶數? O(1)

判斷n是否是偶數?偽碼如下:

return n%2 == 0

只有一行程式碼,所以複雜度為:O(1)

9、常見演算法複雜度總結

結論很重要,要記住:
O(1)<O(logn)<O(√n)<O(n)<O(nlogn)<O(n²)<O(2^n)<O(n!)

mark

注意;
O(nlogn)很重要。

O(logn)<O(√n)如何比較,不做數學推導了,舉個例子記一下:
log以2為底的1000是多少,大概是10,因為2的10次方是1024嘛,所以log以2為底的1000大概是10;
1000的根號值是30多,因為30*30=900嘛,所以大概是30。
10<30,所以我們這樣記得:O(logn)<O(√n)

logn比n快很多。
n取100w,logn大概是20次左右,而n要100w次;
資料規模100w的時候,n和logn的效能差距在5w倍。

logn和nlogn這樣的演算法,會非常多,需要學習。
空間複雜度和時間複雜度的計算基本差不多。

時間更值錢,空間不太值錢;
所以演算法設計更重視時間複雜度,所以很多演算法設計思想的本質更多是以空間換時間。
即:可不可以考慮使用快取等等.

我們常見演算法的複雜度梳理就到這裡。

相關文章