最詳細的解說—時間和空間複雜度

Java資訊庫發表於2018-08-30

演算法的選擇

我們都知道同一個問題有不同的演算法解決,這些演算法在執行時間、執行佔用記憶體、程式碼易讀性等方面都不相同,而在這些演算法中,我們只能選擇一種解決方案,這時判斷選擇哪個演算法的依據是什麼呢?

這裡寫圖片描述

在這裡,我們引入了時間複雜度和空間複雜度這兩個概念作為選擇適合演算法的重要依據,一般對比演算法的好壞基本上從它的時間複雜度和空間複雜度來綜合判斷就可以得出哪個更適合,複雜度通常來說越小越好。

演算法的時間複雜度和空間複雜度的作用:時間複雜度是指執行這個演算法所需要的計算工作量;而空間複雜度是指執行這個演算法所需要的記憶體空間。時間和空間(即暫存器)都是計算機資源的重要體現,而演算法的複雜性就是體現在執行該演算法時的計算機所需的資源多少。

時間複雜度 (Time complexity)

一個演算法語句總的執行次數是關於問題規模N的某個函式,記為f(N),N稱為問題的規模。語句總的執行次數記為T(N),當N不斷變化時,T(N)也在變化,演算法執行次數的增長速率和f(N)的增長速率相同。

則有T(N) = O(f(N)),這稱作演算法的漸進時間複雜度,簡稱時間複雜度。

1、演算法的時間複雜度反映了程式執行時間隨輸入規模增長而增長的量級,在很大程度上能很好地反映出演算法的優劣與否。

2、演算法執行時間需要依據該演算法編制的程式在計算機上執行執行時所消耗的時間來度量,度量方法有兩種:事後統計方法和事前分析估算方法。因為事後統計方法更多地依賴計算機的硬體、軟體等環境因素,有時容易掩蓋演算法本身的優劣,因此常採用事前分析估算的方法。

3、一個演算法是由控制結構(順序、分支、迴圈三種)和原操作(固有資料型別的操作)構成的,而演算法時間取決於兩者的綜合效率。

4、一個演算法花費的時間與演算法中語句的執行次數成正比,執行次數越多,花費的時間就越多,其執行次數稱為語句頻度或時間頻度,記為T(n)。

5、在時間頻度中,n為問題的規模,當n不斷變化時,它所呈現出來的規律,我們稱為時間複雜度。

6、在各種演算法中,若演算法中的語句執行次數為一個常數,則時間複雜度為o(1),同時,若不同演算法的時間頻度不一樣,但他們的時間複雜度卻可能是一樣的。比如:T(n)=n^2+2n+4 與 T(n)=4n^2+n+8,他們的時間頻度顯然不一樣,但他們的時間複雜度卻是一樣的,均為O(n^2),時間複雜度只關注最高數量級,且與之係數也沒有關係。

一、最壞時間複雜度和平均時間複雜度

最壞情況下的時間複雜度稱最壞時間複雜度,一般不特別說明,討論的時間複雜度均是最壞情況下的時間複雜度。這樣做的原因是:最壞情況下的時間複雜度是演算法在任何輸入例項上執行時間的上界,這就保證了演算法的執行時間不會比任何更長。

平均時間複雜度是指所有可能的輸入例項均以等概率出現的情況下,演算法的期望執行時間,設每種情況的出現的概率為pi,平均時間複雜度則為sum(pi*f(n)) 。

二、求解演算法的時間複雜度的具體步驟

⑴ 找出演算法中的基本語句

演算法中執行次數最多的那條語句就是基本語句,通常是最內層迴圈的迴圈體。

⑵ 計算基本語句的執行次數的數量級

只需計算基本語句執行次數的數量級,這就意味著只要保證基本語句執行次數的函式中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的係數。這樣能夠簡化演算法分析,並且使注意力集中在最重要的一點上:增長率。

⑶ 用大Ο記號表示演算法的時間效能

將基本語句執行次數的數量級放入大Ο記號中,如果演算法中包含巢狀的迴圈,則基本語句通常是最內層的迴圈體;如果演算法中包含並列的迴圈,則將並列迴圈的時間複雜度相加。

下面舉一個簡單例子:

for(i=1;i<=n;i++)

{a++};

for(i=1;i<=n;i++)

{

for(j=1;j<=n;j++)

{

a++;

}

}

第一個for迴圈的時間複雜度為o(n),第二個for迴圈時間複雜度為o(n^2),則整個演算法的時間複雜度為o(n^2+n)。

o(1)表示基本語句的執行次數是一個常數,一般來說,只要演算法中不存在迴圈語句,時間複雜度就為o(1)。

三、時間複雜度的分析方法

1、時間複雜度就是函式中基本操作所執行的次數

2、一般預設的是最壞時間複雜度,即分析最壞情況下所能執行的次數

3、忽略掉常數項

4、關注執行時間的增長趨勢,關注函式式中增長最快的表示式,忽略係數

5、計算時間複雜度是估算隨著n的增長函式執行次數的增長趨勢

6、遞迴演算法的時間複雜度為:遞迴總次數 * 每次遞迴中基本操作所執行的次數

7、常用的時間複雜度有以下七種,演算法時間複雜度依次增加:O(1)常數型、O(log2 n)對數型、O(n)線性型、O(nlog2n)二維型、O(n^2)平方型、O(n^3)立方型、O(2^n)指數型.

四、特殊時間複雜度

1.二分查詢

因為每次的計算,都可以把查詢的範圍縮小一半,所以二分查詢的時間複雜度為O(log2 N)。

2.斐波那契的遞迴演算法

因為每次的展開都要把當前的已知項再拆分成當前數目的兩倍,所以斐波那契的遞迴演算法時間複雜度為2^N。

斐波那契的時間複雜度演算法如下圖所示,計算n第N個斐波那契數的大小時,共需計算2^N - 1次。

這裡寫圖片描述

五、常用排序演算法的時間複雜度

這裡寫圖片描述

空間複雜度(Space Complexity)

一個程式的空間複雜度是指執行完一個程式所需記憶體的大小,利用程式的空間複雜度,可以對程式的執行所需要的記憶體多少有個預先估計。一個程式執行時除了需要儲存空間和儲存本身所使用的指令、常數、變數和輸入資料外,還需要一些對資料進行操作的工作單元和儲存一些為現實計算所需資訊的輔助空間。程式執行時所需儲存空間包括以下兩部分。

(1)固定部分:這部分空間的大小與輸入/輸出的資料的個數多少、數值無關,主要包括指令空間(即程式碼空間)、資料空間(常量、簡單變數)等所佔的空間,這部分屬於靜態空間。

(2)可變空間:這部分空間的主要包括動態分配的空間,以及遞迴棧所需的空間等,這部分的空間大小與演算法有關。一個演算法所需的儲存空間用f(n)表示。S(n)=O(f(n)),其中n為問題的規模,S(n)表示空間複雜度。

1、空間複雜度是對一個演算法在執行過程中臨時佔用儲存空間大小的量度。

2、一個演算法在計算機上佔用的記憶體包括:程式程式碼所佔用的空間、輸入輸出資料所佔用的空間、輔助變數所佔用的空間這三個方面。程式程式碼所佔用的空間取決於演算法本身的長短,輸入輸出資料所佔用的空間取決於要解決的問題,是通過參數列呼叫函式傳遞而來,只有輔助變數是演算法執行過程中臨時佔用的儲存空間,與空間複雜度相關。

3、通常來說,只要演算法不涉及到動態分配的空間以及遞迴、棧所需的空間,空間複雜度通常為0(1)。

4、演算法的空間複雜度並不是計算實際佔用的空間,而是計算整個演算法的輔助空間單元的個數,與問題的規模沒有關係。

一、特殊空間複雜度

在斐波那契數求空間複雜度的過程中,我們需要考慮函式棧幀的過程,比如當我們求第五個斐波那契數的時候,這時候需要先開闢空間存放第四個數,然後再開闢空間存放第三個數;當開闢空間到第二個和第一個數的時候,第三個數得到結果並返回到第四個數中,第四個數的值已知後返回到第五個數中,在這個過程中,最大佔用空間即為層數減一。如下圖所示:

這裡寫圖片描述

開闢空間的大小最多等於層數+1,也就是說求第N個斐波那契數,空間複雜度即為O(N)。

二分查詢因為整個運算過程沒有空間的改變,所以空間複雜度為O(1)。

二、時間複雜度與空間複雜度的聯絡

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

1、求二分法的時間複雜度和空間複雜度

非遞迴:

templateT* BinarySearch(T* array,int number,const T& data)

{

      assert(number>=0);

      int left = 0;

      int right = number-1;

      while (right >= left)

      {

              int mid = (left&right) + ((left^right)>>1);

              if (array[mid] > data)

              {

                    right = mid - 1;

              }

              else if (array[mid] < data)

              {

                    left = mid + 1;

              }

              else

              {

                    return (array + mid);

              }

      }

      return NULL;

}

分析:

這裡寫圖片描述

迴圈的基本次數是log2N,所以,時間複雜度是O(log2N);由於輔助空間是常數級別的,所以,空間複雜度是O(1)。

遞迴:

templateT* BinarySearch(T* left,T* right,const T& data)

{

      assert(left);

      assert(right);

      if (right >=left)

      {

              T* mid =left+(right-left)/2;

              if (*mid == data)

                    return mid;

              else

                    return *mid > data ? BinarySearch(left, mid - 1, data) : BinarySearch(mid + 1, right, data);

      }

      else

      {

              return NULL;

      }

}

分析:

這裡寫圖片描述

遞迴的次數和深度都是log2N,每次所需要的輔助空間都是常數級別的:時間複雜度:O(log2N),空間複雜度:O(log2N )。

2、斐波那契數列的時間和空間複雜度

//遞迴情況下的斐波那契數列

long long Fib(int n)

{

      assert(n >= 0);

      return n<2 ? n : Fib(n - 1) + Fib(n-2);

}

遞迴的時間複雜度是: 遞迴次數*每次遞迴中執行基本操作的次數,所以,時間複雜度是: O(2^N)。

遞迴的空間複雜度是: 遞迴的深度*每次遞迴所需的輔助空間的個數,所以,空間複雜度是:O(N)。

//求前n項中每一項的斐波那契數列的值

long long *Fib(int n)

{

      assert(n>=0);

      long long *array = new long long[n + 1];

      array[0] = 0;

      if (n > 0)

      {

              array[1] = 1;

      }

      for (int i = 2; i

迴圈的基本操作次數是n-1,輔助空間是n+1,所以:時間複雜度O(n),空間複雜度O(n)。

//非遞迴

long long Fib(int n)

{

      assert(n >= 0);

      long long first=0,second=1;

      for (int i = 2; i <= n; i++)

      {

              first = first^second;

              second = first^second;

              first = first^second;

              second = first + second;

      }

      return second;

}

迴圈的基本次數是n-1,所用的輔助空間是常數級別的:時間複雜度:O(n),空間複雜度:O(1)。

總結

對於一個演算法,演算法的時間複雜度和空間複雜度往往是相互影響的,其所有效能之間都存在著或多或少的相互影響,因此,當設計一個演算法(特別是大型演算法)時,要綜合考慮演算法的各項效能、演算法的使用頻率、演算法處理的資料量的大小、演算法描述語言的特性、演算法執行的機器系統環境等各方面因素,才能夠設計出比較好的演算法。

為了讓學習變得輕鬆高效, 現在給大家提供一個學習平臺,讓你在實踐中積累經驗掌握原理。主要方向是JAVA架構師,在這裡你可以學習Java工程化、高效能及分散式、深入淺出、效能調優、Spring,MyBatis,Netty原始碼分析和大資料等知識點。想要了解詳情的可以加入Java後端技術群:819940388,或關注微信公眾號:Java資訊庫,回覆“架構”,免費的大型網際網路Java視訊分享給大家。
這裡寫圖片描述

相關文章