數據結構:時間複雜度&空間複雜度(遞歸)

轉載文章

時間複雜度:

  一般情況下,算法中基本操作重複執行的次數是問題規模n的某個函數f(n),進而分析f(n)隨n的變化情況並確定T(n)的數量級。這裏用"O"來表示數量級,給出算法的時間複雜度。

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

  它表示隨着問題規模的n的增大,算法的執行時間的增長率和f(n)的增長率相同,這稱作算法的漸進時間複雜度,簡稱時間複雜度。而我們一般討論的是最壞時間複雜度,這樣做的原因是:最壞情況下的時間複雜度是算法在任何輸入實例上運行時間的上界,分析最壞的情況以估算算法指向時間的一個上界。

時間複雜度的分析方法:

  • 1、時間複雜度就是函數中基本操作所執行的次數
  • 2、一般默認的是最壞時間複雜度,即分析最壞情況下所能執行的次數
  • 3、忽略掉常數項
  • 4、關注運行時間的增長趨勢,關注函數式中增長最快的表達式,忽略係數
  • 5、計算時間複雜度是估算隨着n的增長函數執行次數的增長趨勢
  • 6、遞歸算法的時間複雜度爲:遞歸總次數 * 每次遞歸中基本操作所執行的次數

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

 

 

空間複雜度:

  算法的空間複雜度並不是計算實際佔用的空間,而是計算整個算法的輔助空間單元的個數,與問題的規模沒有關係。算法的空間複雜度S(n)定義爲該算法所耗費空間的數量級。

  S(n)=O(f(n))  若算法執行時所需要的輔助空間相對於輸入數據量n而言是一個常數,則稱這個算法的輔助空間爲O(1); 

  遞歸算法的空間複雜度:遞歸深度N*每次遞歸所要的輔助空間, 如果每次遞歸所需的輔助空間是常數,則遞歸的空間複雜度是 O(N).

 

進一步分析:

兩段算法串聯在一起的複雜度 T1(n) + T2(n) = max( O(f1(n)) + O(f2(n)) ) , 即比較慢的那個算法決定了串聯後的效率。

 

兩段算法嵌套在一起的複雜度 T1(n) * T2(n) =  O( f1(n)  *  f2(n)  )。

 

if - else 結構的複雜度取決於if條就愛你判斷複雜度和兩個分枝部分的複雜度,總體複雜度取三者中最大。即度結構

If(p1)   /*p1 的複雜度爲O(f1)*/

P2;/*p2 的複雜度爲O(f2)*/

else

P3 /*p3 的複雜度爲O(f3)*/

總複雜度爲max(O(f1),O(f2),O(f3)).

 

例:

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

非遞歸:

template<typename T>
T* 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;
}

分析:

循環的基本次數是log2 N,所以:

時間複雜度是O(log2 N);

由於輔助空間是常數級別的所以:

空間複雜度是O(1);

 

 

遞歸:

template<typename T>
T* 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;
       }
}

遞歸的次數和深度都是log2 N,每次所需要的輔助空間都是常數級別的:

時間複雜度:O(log2 N)

空間複雜度: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)

說明一下:爲什麼這裏空間複雜度是O(N)?

輔助空間指的是爲局部變量和形參所開闢的空間,這有形參,需要分配一個存儲單元。

對於遞歸算法,由於運行時有附加堆棧,所以遞歸的空間複雜度是遞歸的深度*每壓棧所需的空間個數

遞歸有運行時堆棧,求的是遞歸最深的那一次壓棧所耗費的空間的個數遞歸最深的那一次所耗費的空間足以容納它所有遞歸過程。(遞歸是要返回上一層的,所以它所需要的空間不是一直累加起來的)

所以最深的那次壓棧就是 遞歸的空間複雜度。

舉個栗子:現在有一個Stack,現在向裏面push數據 push(1) , push(2) , pop(2) , push(3) 總共向裏面push了3次數據,但是棧的深度爲2,所需要棧的大小也爲2. 這與遞歸是一樣的,它堆棧最深的那一次所開闢的空間就是它所耗費空間的個數。

所以,遞歸的深度是n,而每次遞歸所需的輔助空間個數爲1.

//求前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; i++)
       {

              array[i] = array[i - 1] + array[i - 2];

       }
       return array;
}

循環的基本操作次數是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)

 

最大子列和問題:

(思想:分而治之)

給定K個整數組成的序列{ N1​​N2​​, ..., NK​​ },「連續子列」被定義爲{ Ni​​Ni+1​​, ..., Nj​​ },其中 1ijK。「最大子列和」則被定義爲所有連續子列元素的和中最大者。例如給定序列{ -2, 11, -4, 13, -5, -2 },其連續子列{ 11, -4, 13 }有最大的和20。現要求你編寫程序,計算給定整數序列的最大子列和。

視頻講解:

 

核心代碼:

int Max3( int A, int B, int C )

{ /* 返回3個整數中的最大值 */

    return A > B ? A > C ? A : C : B > C ? B : C;

}

int DivideAndConquer( int List[], int left, int right ){ /* 分治法求List[left]到List[right]的最大子列和 */

    int MaxLeftSum, MaxRightSum; /* 存放左右子問題的解 */

    int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界線的結果*/

    int LeftBorderSum, RightBorderSum;

    int center, i;

    if( left == right )  { /* 遞歸的終止條件,子列只有1個數字 */

        if( List[left] > 0 )  return List[left];

        else return 0;

    }

    /* 下面是"分"的過程 */

    center = ( left + right ) / 2; /* 找到中分點 */

    /* 遞歸求得兩邊子列的最大和 */

    MaxLeftSum = DivideAndConquer( List, left, center );

    MaxRightSum = DivideAndConquer( List, center+1, right );


    /* 下面求跨分界線的最大子列和 */

    MaxLeftBorderSum = 0; LeftBorderSum = 0;

    for( i=center; i>=left; i-- ) { /* 從中線向左掃描 */

        LeftBorderSum += List[i];

        if( LeftBorderSum > MaxLeftBorderSum )

            MaxLeftBorderSum = LeftBorderSum;

    } /* 左邊掃描結束 */

    MaxRightBorderSum = 0; RightBorderSum = 0;

    for( i=center+1; i<=right; i++ ) { /* 從中線向右掃描 */

        RightBorderSum += List[i];

        if( RightBorderSum > MaxRightBorderSum )

            MaxRightBorderSum = RightBorderSum;

    } /* 右邊掃描結束 */

    /* 下面返回"治"的結果 */

    return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum );

}

int MaxSubseqSum3( int List[], int N )

{ /* 保持與前2種算法相同的函數接口 */

    return DivideAndConquer( List, 0, N-1 );

}

時間複雜度的計算:

  1. T(n) = 2*T(n/2) + O(n)   因爲 T(n/2) = 2*T(N/2) + O(n/2)
  2. .....................
  3. --->  T(n) = 2^k * T(n/(2^k)) + k*O(n)
  4. --->  n/(2^k) = 1 --> k = log2n
  5. --->  T(n) = log2n + nlog2n
  6. --->  O(nlog2n)

 

由於遞歸而產生的空間複雜度:

  • 有時間複雜度我們可知,遞歸的深度是 log2n,而遞歸產生的輔助空間是1,所以 遞歸而產生的空間複雜度爲 O(log2n)。

算法的整體空間複雜度:

  1. 因爲存在數組list[n],所以對於整體而言還有 n個輔助空間,因爲,list爲數組,所以不用每次遞歸都開闢n個輔助空間,
  2. 總之,s(n) = max( O(n) + O(log2n) ) = O(n)。