準備刷算法題了,才發現自己連時間復雜度和空間復雜度都忘了

前言

最近打算好好刷刷算法題,然鵝發現自己對這個算法復雜度的知識記憶已全部返還給數據結構老師了

一、算法

算法(Algorithm)是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,算法代表著用系統的方法描述解決問題的策略機制。通俗地說,數據結構就是指存儲數據的結構。算法就是操作數據的方法。

二、算法的特征

  • 有窮性(Finiteness):算法的有窮性是指算法必須能在執行有限個步驟之后終止
  • 確切性(Definiteness):算法的每一步驟必須有確切的定義
  • 輸入項(Input):一個算法有0個或多個輸入,以刻畫運算對象的初始情況,所謂0個輸入是指算法本身定出了初始條件
  • 輸出項(Output):一個算法有一個或多個輸出,以反映對輸入數據加工后的結果,沒有輸出的算法是毫無意義的
  • 可行性(Effectiveness):算法中執行的任何計算步驟都是可以被分解為基本的可執行的操作步,即每個計算步都可以在有限時間內完成(也稱之為有效性)

三、算法效率的度量

對于同一個問題,使用不同的算法,也許最終得到的結果是一樣的,但在過程中消耗的資源和時間卻會有很大的區別。那么我們應該如何去衡量不同算法之間的優劣呢?主要還是從算法所占用的「時間」和「空間」兩個維度去考量。

時間維度:是指執行當前算法所消耗的時間,我們通常用「時間復雜度」來描述。

空間維度:是指執行當前算法需要占用多少內存空間,我們通常用「空間復雜度」來描述。

評價一個算法的效率主要是看它的時間復雜度和空間復雜度情況。有的時候時間和空間卻又是「魚和熊掌」不可兼得,那么我們就需要從中去取一個平衡點。

四、時間復雜度

【4.1】時間頻度 一個算法執行所耗費的時間,從理論上是不能算出來的,必須上機運行測試才能知道。但我們不可能也沒有必要對每個算法都上機測試,只需知道哪個算法花費的時間多,哪個算法花費的時間少就可以了。并且一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱為語句頻度或時間頻度,記為T(n)。

【4.2】時間復雜度: 在剛才提到的時間頻度T(n)中,n稱為問題的規模,當n不斷變化時,時間頻度T(n)也會不斷變化。但有時我們想知道它變化時呈現什么規律。為此,我們引入時間復雜度概念。 算法的時間復雜度也就是算法的時間度量,記作:T(n) = O(f(n))。它表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,稱作算法的漸進時間復雜度,簡稱時間復雜度

【4.3】大O表示法:像前面用O( )來體現算法時間復雜度的記法,我們稱之為大O表示法。 算法復雜度可以從最理想情況、平均情況和最壞情況三個角度來評估,由于平均情況大多和最壞情況持平,而且評估最壞情況也可以避免后顧之憂,因此一般情況下,我們設計算法時都要直接估算最壞情況的復雜度。

【4.4】常見的時間復雜度量級:

  • 常數階O(1)
  • 線性階O(n)
  • 平方階O(n2)
  • 立方階O(n3)
  • 對數階O(logn)
  • 線性對數階O(nlogn)
  • 指數階O(2?)

【4.5】計算時間復雜度時的程序分析法則:

⑴. 對于一些簡單的輸入輸出語句或賦值語句,近似認為需要O(1)時間

⑵. 對于順序結構,需要依次執行一系列語句所用的時間可采用大O求和法則

  • 例一:算法的2個部分時間復雜度分別為 T1(n)=O(f(n)) 和 T2(n)=O(g(n)),則時間復雜度 T1(n)+T2(n)=O(max(f(n), g(n)))
  • 例二:算法的2個部分時間復雜度分別為T1(m)=O(f(m)) 和 T2(n)=O(g(n)),則時間復雜度為 T1(m)+T2(n)=O(f(m) + g(n))

⑶. 對于選擇結構,如if語句,它的主要時間耗費是在執行then字句或else字句所用的時間,需注意的是檢驗條件也需要O(1)時間

⑷. 對于循環結構,循環語句的運行時間主要體現在多次迭代中執行循環體以及檢驗循環條件的時間耗費,一般可用大O乘法法則

  • 例一:算法的2個部分時間復雜度分別為 T1(n)=O(f(n)) 和 T2(n)=O(g(n)),則時間復雜度為 T1T2=O(f(n)g(n))

⑸. 對于復雜的算法,可以將它分成幾個容易估算的部分,然后利用求和法則和乘法法則技術求出整個算法的時間復雜度

⑹. 另外還有以下2個運算法則

  • 若 g(n)=O(f(n)),則O(f(n))+ O(g(n))= O(f(n))
  • O(Cf(n)) = O(f(n)),其中C是一個正常數

【4.6】常見的時間復雜度示例:

常數階 O(1)

    let j = temp;
    let i = j;
    let Temp = i;

解:以上三條單個語句的頻度均為1,該程序段的執行時間是一個與問題規模n無關的常數。算法的時間復雜度為常數階,記作T(n)=O(1)。注意:如果算法的執行時間不隨著問題規模n的增加而增長,即使算法中有上千條語句,其執行時間也不過是一個較大的常數。此類算法的時間復雜度是O(1)。

線性階 **O(n) **

    let a = 0;
    let b = 1; // 語句1  
    for (let i = 1; i <= n; i++) { // 語句2
      let s = a + b; // 語句3
      let b = a; // 語句4
      let a = s; // 語句5
    }

解: 語句1的頻度為2;語句2的頻度為n;語句3的頻度為 n-1;語句4的頻度為n-1;語句5的頻度為n-1;T(n)=2+n+3(n-1)=4n-1=O(n)

平方階 O(n2)

  let sum = 0; // 1次
  for(let i = 1; i <= n; i++) { // n+1次
    for (j = 1; j <= n; j++) { // n2次
      sum ++ ; // n2次
    }
  }

解:因為O(2n2+n+1)=n2(即:去低階項,去掉常數項,去掉高階項的常參得到),所以T(n)=O(n2);

  for (let i = 1; i < n; i++) {
    y = y + 1; // 語句1
    for (let j = 0; j <= (2 * n); j++)
      x++; // 語句2
  }

解: 語句1的頻度是n-1, 語句2的頻度是(n-1)*(2n+1)=2n2-n-1,即 f(n)=2n2-n-1+(n-1)=2n2-2;又O(2n2-2)=n2,該程序的時間復雜度T(n)=O(n2)。一般情況下,對步進循環語句只需考慮循環體中語句的執行次數,忽略該語句中步長加1、終值判別、控制轉移等成分,當有若干個循環語句時,算法的時間復雜度是由嵌套層數最多的循環語句中最內層語句的頻度f(n)決定的。

對數階 O(log2?)

  let i = 1; //語句1
  while (i <= n) {
    i = i * 2; // 語句2
  }

解: 上面的代碼,在while循環里面,每次都將 i 乘以 2,乘完之后,i 距離 n 就越來越近了,直到i不小于n退出。我們試著求解一下,假設循環次數為x,也就是說 2 的 x 次方等于 n,則由2^x=n得出x=log2n。因此這個代碼的時間復雜度為T(n)=O(log2? )

【4.7】復雜度的比較

其中x軸代表n值,y軸代表T(n)值(時間復雜度)。T(n)值隨著n的值的變化而變化,其中可以看出O(n!)和O(2?)隨著n值的增大,它們的T(n)值上升幅度非常大,而O(logn)、O(n)、O(1)隨著n值的增大,T(n)值上升幅度則很小。
常用的時間復雜度按照耗費的時間從小到大依次是:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2?)<O(n!)

五、空間復雜度

時間復雜度,換句話說,就是它們運行得有多快。但有些時候,我們還得以另一種名為空間復雜度的度量方式,去估計它們會消耗多少內存。當內存有限時,空間復雜度便會成為選擇算法的一個重要的參考因素。比如說,在給小內存的小型設備寫程序時,或是處理一些會迅速占滿大內存的大數據時都會考慮空間復雜度。

【5.1】空間復雜度:空間復雜度是執行算法的空間成本,是對一個算法在運行過程中臨時占用存儲空間大小的量度,它同樣適用了大O表示法。程序占用空間大小的計算公式記作S(n)=O(f(n)),其中n為問題的規模,f(n)為算法所占儲存空間的函數

【5.2】算法存儲量包括:

  1. 程序本身所占空間
  2. 輸入數據所占空間
  3. 輔助變量所占空間

輸入數據所占空間只取決于問題本身,和算法無關,空間復雜度只需分析除了輸入數據所占空間和程序本身所占空間之外的輔助變量所占空間。

【5.3】常見空間復雜度:

O(1):算法執行所需要的臨時空間不隨著某個變量n的大小而變化,即此算法空間復雜度為一個常量

O(n):當一個算法的空間復雜度與n成線性比例關系時,可表示為O(n)。若形參為數組,則只需要為它分配一個存儲由實參傳送來的一個地址指針的空間,即一個機器字長空間;若形參為引用方式,則也只需要為其分配存儲一個地址的空間,用它來存儲對應實參變量的地址,以便由系統自動引用實參變量。

O(log2n):當一個算法的空間復雜度與以2為底的n的對數成正比時

【5.3】空間復雜度示例:

空間復雜度 O(1)

  function makeUpperCase(arr) {
    for (let i = 0; i < arr.length; i++) {
      arr[i] = arr[i].toUpperCase();
    }
    return arr;
  }

因為該函數并不消耗額外的內存空間,所以我們把它的空間復雜度描述為O(1)

空間復雜度 O(n)


  function makeUpperCase(arr) { // makeUpperCase函數接收一個數組作為參數arr。
    let newArr = []; // 然后它創建了一個全新的數組,名為newArr
    for (let i = 0; i < arr.length; i++) {
      newArr[i] = arr[i].toUpperCase(); // 并將原數組arr里的字符串的大寫形式填進去。
    }
    return newArr;
  }

分析該函數的話,你會發現它接收一個n元素的數組,就會產生另一個新的n元素數組。因此,我們會說這個makeUpperCase函數的空間復雜度是O(n)

**注意 **

① 空間復雜度相比時間復雜度分析要少

② 對于遞歸算法來說,代碼一般都比較簡短,算法本身所占用的存儲空間較少,但運行時需要占用較多的臨時工作單元;若寫成非遞歸算法,代碼一般可能比較長,算法本身占用的存儲空間較多,但運行時將可能需要較少的存儲單元

六、常用的時間復雜度和空間復雜度

一個經驗規則:其中c是一個常量,如果一個算法的復雜度為c 、 log2? 、n 、 n*log2? ,那么這個算法時間效率比較高 ,如果是2? 、3? 、n!,那么稍微大一些的n就會令這個算法不能動了,居于中間的幾個則差強人意。

| 排序法| 最差時間分析 | 平均時間復雜度 | 穩定度 | 空間復雜度 |
| 冒泡排序 | O(n2) | O(n2) | 穩定 | O(1) |
| 快速排序 | O(n2) | O(nlog2?) | 不穩定 | O(log2?)~O(n) |
| 選擇排序 | O(n2) | O(n2) | 穩定 | O(1) |
| 二叉樹排序 | O(n2) | O(n
log2?) | 不一定 | O(n) |
| 插入排序| O(n2) | O(n2) | 穩定 | O(1) |
| 堆排序 | O(nlog2?) | O(nlog2?) | 不穩定 | O(1) |
| 希爾排序 | O | O | 不穩定 | O(1) |

七、時間復雜度和空間復雜度的關系

對于一個算法,其時間復雜度和空間復雜度往往是相互影響的。當追求一個較好的時間復雜度時,可能會使空間復雜度的性能變差,即可能導致占用較多的存儲空間;反之,當追求一個較好的空間復雜度時,可能會使時間復雜度的性能變差,即可能導致占用較長的運行時間。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。