那些年忽略的知識:時間複雜度和空間複雜度詳解

學習中的苦與樂發表於2021-12-09

概述

尷尬,學妹問我“氣泡排序、二分查詢、希爾排序、快速排序方法”演算法的『時間複雜度』,我只能使用百度查詢答案進行了回答,但這不符合我的人設,我必須要弄懂這個東西。

作為一個「不稱職的攻城獅」,對複雜度的概念是很模糊的,更不要說去計算複雜度了。

但是在開發中對於程式碼快的執行,做到“提高程式碼執行率、降低記憶體佔用率”,巧了,這和我百度瞭解到的『複雜度』相似,然後查詢了相關資料,進行一個複習歸納。

簡單來說,就是同一個功能:

  • 別人寫出來執行,記憶體佔用100M,執行時間需要1秒;
  • 自己寫出來執行,記憶體佔用500M,執行時間需要5秒。

這你能忍?但是在程式碼開發中是沒辦法計算記憶體佔用和執行時間情況的,怎麼樣才能預估程式碼塊達到了較優呢?而且在不同的裝置上執行的效果也不同,

比如你新買的手機和用了5年的手機,開啟同一個軟體所需要的時間肯定是不同的。

但是我們基本的程式碼塊執行次數是可以計算的,這就是我們今天的主角『複雜度』。

PS:衡量不同演算法之間的優劣就要用到時間複雜度和空間複雜度。

時間複雜度

什麼是時間複雜度?舉個例子:

public string attack(int n)
{
   for (int i = 0; i < n; i++)
   {
      Console.WriteLine("被攻擊");
   }
   return "快來救我";
}

上面程式碼執行如下:

i = 0                : 執行 1 次
i <  n                : 執行 n+1 次
i++                 : 執行 n+1 次
Console.WriteLine("被攻擊");  : 執行 n 次
return "快來救我"         : 執行 1

這個方法的總執行次數就是 3n + 4 次,

但是開發過程中不可能都這樣去數,所以根據程式碼執行時間推導過程就有一個規律,這就是所有程式碼執行時間 T(n)和程式碼的執行次數 f(n) ,這個是成正比的,而這個規律有一個公式(大歐表示法):

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

n    是輸入資料的大小或者輸入資料的數量  
T(n) 表示一段程式碼的總執行時間   
f(n) 表示一段程式碼的總執行次數   
O    表示程式碼的執行時間 T(n) 和 執行次數f(n) 成正比

套用公式可以得到上面方法的複雜度就是:O(3n+4),簡化為O(n)。

為什麼可以簡化為O(n)呢,我們看下簡化過程。

  • 如果只是常數直接估算為1,O(3) 的時間複雜度就是 O(1),不是說只執行了1次,而是對常量級時間複雜度的一種表示法。一般情況下,只要演算法裡沒有迴圈和遞迴,就算有上萬行程式碼,時間複雜度也是O(1)
  • O(3n+4) 裡常數4對於總執行次數的幾乎沒有影響,直接忽略不計,係數 3 影響也不大,因為3n和n都是一個量級的,所以作為係數的常數3也估算為1或者可以理解為去掉係數,所以 O(3n+4) 的時間複雜度為 O(n)
  • 如果是多項式,只需要保留n的最高次項,O( 666n³ + 666n² + n ),這個複雜度裡面的最高次項是n的3次方。因為隨著n的增大,後面的項的增長遠遠不及n的最高次項大,所以低於這個次項的直接忽略不計,常數也忽略不計,簡化後的時間複雜度為 O(n³)

是不是很簡單,我們看下常用的時間複雜度。

1、常數階 O(1)

通過上面的簡化過程知道,一般情況下,只要演算法裡沒有迴圈和遞迴,就算有上萬行程式碼,時間複雜度也是 O(1),因為它的執行次數不會隨著任何一個變數的增大而變長,比如下面這樣

public string attack(int n)
{
    int power = 100;
    int speed = 65;
    Console.WriteLine("力量是:", power);
    Console.WriteLine("速度是:", speed);

    if (power == 200)
        Console.WriteLine("快來救我");

    .....

    return "我沒事,不用擔心";
}

2、線性階 O(n)

只有一層迴圈或者遞迴等,時間複雜度就是 O(n),這樣消耗時間隨著N變化而變化,比如下面這種

public string attack(int n)
{
    for (int i = 0; i < n; i++)
    {
        Console.WriteLine("我沒事");
    }
    return "我很好";
}
public string runaway(int n)
{
    while(--n>0)
        Console.WriteLine("跑跑跑");
    return "我跑的很快";
}
public string gameover(int n)
{
    Console.WriteLine("hhh");
    if (--n > 0)
    {
        Console.WriteLine(gameover(n));
    }
    return "結束";
}

3、平方階 O(n²)

就是雙重for迴圈,如果外層的是n,內層的是m,那麼複雜度就是O(m*n),比如下面這種

public string attack(int n)
{
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            Console.WriteLine("我沒事");
        }
    }
    return "我很好";
}

還記得上面的簡化不?這樣的,總執行次數為 n + n²,如果是多項式,取最高次項,所以這個時間複雜度也是 O(n²)

public string attack(int n)
{
    for (int i = 0; i < n; i++)
    {
        Console.WriteLine("我沒事");
    }
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            Console.WriteLine("我沒事");
        }
    }
    return "我很好";
}

4、對數階 O(logN)

下面的迴圈就按i*2何時=N,就是2的多少次方是N,即log2^N,讀作以2為底N的對數。對數公式[1] log不瞭解的可以點選檢視一下,這裡不做過多講解。

int i = 1;
while (i < n)
    i = i * 2;

5、線性對數階 O(nlogN)

就是for迴圈套while,如下nlog2^N,讀作n倍以2為底N的對數。。對數公式[1] log不瞭解的可以點選檢視一下,這裡不做過多講解。

for (int j = 0; j < n; j++)
{
    int i = 1;
    while (i < n)
        i = i * 2;
}

6、更多...

其他還有一些時間複雜度的運用,下面由快到慢排列了一下:

複雜度名稱
O(1) 常數複雜度
O(logn) 對數複雜度
O(n) 線性時間複雜度
O(nlogn) 線性對數複雜度
O(n²) 平方
O(n³) 立方
O(2^n) 指數,一點資料量就卡的不行
O(n!) 階乘,就更慢了

 

 這些時間複雜度有什麼區別呢,看張圖

 

 

 

隨著資料量或者 n 的增大,時間複雜度也隨之增加,也就是執行時間的增加,會越來越慢,越來越卡。

總的來說時間複雜度就是執行時間增長的趨勢,那麼空間複雜度就是儲存空間增長的趨勢。

空間複雜度

空間複雜度就是演算法需要多少記憶體,佔用了多少空間。

常用的空間複雜度有 O(1)O(n)O(n²)。

O(1) 就是演算法執行臨時空間不會隨著N發生變化 都算成一個常量,如下。

public string attack(int n)
{
    int j = 0;
    for (int i = 0; i < n; i++)
    {
        j = i;
        j++;
    }
    return "我很好";
}

O(n)就是一開始就開闢的記憶體,不會在改變,如下new出來了N個,但是N下面迴圈並沒有再開臨時變數

public string attack(int n)
{
    int[] m = new int[n];
    int j = 0;
    for (int i = 1; i < n; i++)
    {
        j = i;
        j++;
    }
    return "我很好";
}

『空間複雜度』和『時間複雜度』判斷標準差不多,主要是看開了多少個臨時變數,是否跟N有一定的線性關係。

這都是一些簡單的,如果是複雜的怎麼計算呢, 下面都計算時間複雜度為例子:

  1. T(n) = n + 29     一般說是O(n)因為常數項影響函式增長很小
  2. T(n) = n^3 + n^2 + 29 一般說為O(n3),n3 的增長速度是遠超 n^2 的,同時 n^2 的增長速度是遠超 n 的,所以有了高次項可以忽略低次項
  3. T(n) = 3n^3            一般說為O(n^3)因為階數比乘數影響增長速度最顯著我們通常忽略

總結

重新梳理了一遍以後,現在寫程式碼眼裡浮現的都是一堆O(1)、O(n)、O(n²)、logN,感覺要瘋了。

參考資料

1.百度百科:對數公式

2.如何理解時間複雜度和空間複雜度:https://www.cnblogs.com/magicg/p/15178750.html

3.C#演算法的時間複雜度和空間複雜度:https://blog.csdn.net/q_17600689511/article/details/100189543

4.c#演算法時間複雜度 面試必看:https://blog.csdn.net/weixin_44370124/article/details/119321322

 

 

 
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說
出處: https://www.cnblogs.com/xiongze520/p/15666448.html
您可以隨意轉載、摘錄,但請在文章內註明作者和原文連結。  

 

 

 

相關文章