時間複雜度學習(下)

這一節將以一個具體的算法題給出4種不一樣解法,分析各自的時間複雜度並比較其各自的運行性能。java

給出兩個求和公式,如下分析中會用到:算法

\begin{gather}
\sum_{i=1}^Ni=\frac{N(N+1)}{2}  \tag{1}\\
\sum_{i=1}^Ni^2=\frac{N(N+1)(2N+1)}{6}  \tag{2}
\end{gather}

最大子序列和問題數組

A_1, A_2, A_3, ..., A_N,求 \sum_{k=i}^ jA_k 的最大值。(爲方便起見,若全部整數均爲負數,則最大子序列和爲0)。dom

例如:輸入 -2, 11, -4, 13, -5, -2,其最大子序列和爲 11+(-4)+13=20性能

1,時間複雜度爲 O(N^3)的解法

public static int maxSubSum1(int[] a) {
        int maxSum = 0;
        for (int i = 0; i < a.length; i++) {
            for (int j = i; j < a.length; j++) {
                int thisSum = 0;
                for (int k = i; k <= j; k++) {
                    thisSum += a[k];
                }
                if (thisSum > maxSum) {
                    maxSum = thisSum;
                }
            }
        }
        return maxSum;
    }
複製代碼

該種解法最簡單暴力,定義子序列的起始位置爲i,結束位置爲j,假設數組a的長度爲N,當 i=0時,j=0,1,2,3,...,N-1,共N種狀況,當 i=1時,j=1,2,3,...,N-1,共N-1種狀況,以此類推,當 i=N-1時,j=N-1,僅此一種狀況;將ij之間的全部元素和記爲thisSum,一旦thisSum的值比maxSum大,就更新maxSum的值爲thisSum測試

第一個循環大小爲N,第二個循環大小爲N-i,第三個循環大小爲j-i+1,則總運行次數和爲:優化

\sum_{i=0}^{N-1}\sum_{j=i}^{N-1} \sum_{k=i}^j1

首先有:this

\sum_{k=i}^j1 =j-i+1

接着:spa

\sum_{j=i}^{N-1}(j-i+1)= \frac{(N-i+1)(N-i)}{2}

那麼:code

\begin{align}
\sum_{i=0}^{N-1} \frac{(N-i+1)(N-i)}{2} &= \sum_{i=1}^{N}\frac{(N-i+1)(N-i+2)}{2}\\
&=\frac{1}{2}\sum_{i=1}^Ni^2-(N+\frac{3}{2})\sum_{i=1}^Ni
+\frac{1}{2}(N^2+3N+2)\sum_{i=1}^N1\\
&=\frac{1}{2}\frac{N(N+1)(2N+1)}{6}-(N+\frac{3}{2})\frac{N(N+1)}{2}+\frac{N^2+3N+2}{2}N\\
&=\frac{N^3+3N^2+2N}{6}
\end{align}

因此該種解法的時間複雜度爲 O(\frac{N^3+3N^2+2N}{6})=O(N^3)

2,時間複雜度爲 O(N^2)的解法

public static int maxSubSum2(int[] a) {
       int maxSum = 0;
       for (int i = 0; i < a.length; i++) {
           int thisSum = 0;
           for (int j = i; j < a.length; j++) {
               thisSum += a[j];
               if (thisSum > maxSum) {
                   maxSum = thisSum;
               }
           }
       }
       return maxSum;
   }
複製代碼

在第一種解法中,拿掉最裏面的那層循環,並稍作改動,就是如今的解法2。

其中第一層循環大小爲N,第二層循環爲N-i,則總運行次數爲:

\sum_{i=0}^{N-1} \sum_{j=i}^{N-1}1

其中:

\sum_{j=i}^{N-1}1 = N-1-i+1=N-i

那麼:

\begin{align}
\sum_{i=0}^{N-1}(N-i) &= N\sum_{i=0}^{N-1}1- \sum_{i=0}^{N-1} i \\
&= N(N-1+1) - \frac{(N-1)N}{2} \\
&= \frac{N^2-N}{2}
\end{align}

因此第二種解法的時間複雜度爲 O(\frac{N^2-N}{2})=O(N^2)

3,時間複雜度爲 O(NlogN)的解法

以下圖所示,能夠將數組分爲三部分,分別爲前中後三部分。

最大子序列和就可能出如今這三個部分中,其中 mid=\frac{start+end}{2}=\frac{0+5}{2}=2,前半部分是從startmid這一部分的元素,即 -2,11,-4,因此該部分最大元素爲11;後半部分是從mid+1end這一部分的元素,即 13,-5,-2,因此該部分最大元素爲13;而中間部分元素是以mid起始,分別向左和向右進行累加計算,分別求出其向左和向右部分的最大值,從mid向左獲得其最大值:-4+11=7,而向右是從mid+1開始算起獲得其最大值:13,最後將左右兩部分和相加即爲中間部分的最大值:7+13=20;比較前中後部分的最大值,發現中間部分的值20最大,因此該數組最大啊子序列和爲20

那麼在程序中如何實現呢?這就要採用分治策略,將數組a分爲先後兩半子數組b,c,再將前半數組b分爲先後兩半子數組d,e,後半數組c分爲先後兩半子數組f,g,……,直到數組不能再分爲止,此時子數組中就只有一個元素,一個元素就好判斷了,該元素爲正就直接把該元素值返回給上一級子數組,爲負就返回0,而後回到上一級子數組,將以前返回的先後部分子數組的最大值與中間部分最大值進行比較,得出其最大值,接着將最大值返回其上一級子數組,直至回到原數組,這時原數組就獲得了先後部分子數組的最大值,接着求出中間部分子數組的最大值並與先後部分進行比較便可獲得整個數組的最大子序列和。

Talk\ is\ cheap,\ show\ code:

public static int maxSubSum3(int[] a) {
        return a.length > 0 ? maxSumRec(a, 0, a.length - 1) : 0;
    }

    private static int maxSumRec(int[] a, int left, int right) {
        if (left == right) {
            if (a[left] > 0) {
                return a[left];
            } else {
                return 0;
            }
        }

        int center = (left + right) / 2;
        int maxLeftSum = maxSumRec(a, left, center);
        int maxRightSum = maxSumRec(a, center + 1, right);

        int maxLeftBorderSum = 0;
        int leftBorderSum = 0;
        for (int i = center; i >= left; i--) {
            leftBorderSum += a[i];
            if (leftBorderSum > maxLeftBorderSum) {
                maxLeftBorderSum = leftBorderSum;
            }
        }

        int maxRightBorderSum = 0;
        int rightBorderSum = 0;
        for (int i = center + 1; i <= right; i++) {
            rightBorderSum += a[i];
            if (rightBorderSum > maxRightBorderSum) {
                maxRightBorderSum = rightBorderSum;
            }
        }

        return max3(maxLeftSum, maxRightSum,
                maxLeftBorderSum + maxRightBorderSum);
    }

    private static int max3(int a, int b, int c) {
        return a > b ? a > c ? a : c : b > c ? b : c;
    }
複製代碼

其中center爲數組中間元素的下標,maxLeftSummaxRightSum分別爲數組先後部分的最大值,maxLeftBorderSum爲中間部分向左計算的最大值,maxRightBorderSum爲中間部分向右計算最大值;maxLeftBorderSum + maxRightBorderSum即爲中間部分的最大值。

計算中間部分,即計算maxLeftBorderSummaxRightBorderSum總花費時間爲 N,而計算先後兩半部分,即maxLeftSummaxRightSum每一個花費 T(N/2)個時間單元,則總共花費時間:

T(N)=2T(N/2)+N

其中 T(1)=1,則 T(2)=4=2*2T(4)=12=4*3T(8)=32=8*4T(16)=80=16*5

那麼當 N=2^k,則 T(N)=N*(k+1)=N(logN+1),忽略低階項,因此該方法的時間複雜度爲:O(NlogN)

4,時間複雜度爲 O(N)的解法

public static int maxSubSum4(int[] a) {
        int maxSum = 0;
        int thisSum = 0;

        for (int i = 0; i < a.length; i++) {
            thisSum += a[i];

            if (thisSum > maxSum) {
                maxSum = thisSum;
            } else if (thisSum < 0) {
                thisSum = 0;
            }
        }

        return maxSum;
    }
複製代碼

此種方法將時間複雜度優化到了 O(N),只需一輪循環便可找到最大子序列;其思路爲:若當前子序列的和thisSum爲負數,則將thisSum置爲0,下一個數組元素做爲新的子序列的起始位置,thisSum從該元素開始累加,直至找到最大子序列的和。

5,對比分析

使用下面代碼測試上述4中解法所消耗的時間:

public static void getTimingInfo(int n, int alg) {
        int[] test = new int[n];
        Random rand = new Random();

        long startTime = System.currentTimeMillis();
        long totalTime = 0;

        int i;
        for (i = 0; totalTime < 4000; i++) {
            for (int j = 0; j < test.length; j++) {
                test[j] = rand.nextInt(100) - 50;
            }
            switch (alg) {
                case 1:
                    maxSubSum1(test);
                    break;
                case 2:
                    maxSubSum2(test);
                    break;
                case 3:
                    maxSubSum3(test);
                    break;
                case 4:
                    maxSubSum4(test);
                    break;
                default:
            }

            totalTime = System.currentTimeMillis() - startTime;
        }
        System.out.print(String.format("\t%12.6f",
                (totalTime * 1000 / i) / (double) 1000000));
    }

    public static void main(String[] args) {
        for (int n = 100; n <= 1000000; n *= 10) {
            System.out.print(String.format("N = %7d", n));

            for (int alg = 1; alg <= 4; alg++) {
                if ((alg == 1 && n > 50000) || (alg == 2 && n > 500000)) {
                    System.out.print("\t NA ");
                    continue;
                }
                getTimingInfo(n, alg);
            }
            System.out.println();
        }
    }
複製代碼

運行結果以下圖,當預測時間過長,將其設爲NA,從圖中能夠看出,不一樣時間複雜度的程序雖然得出的結果是同樣的,但運行性能相差巨大,猶如波音與摩拜的差異。

總結:之後寫代碼以前要多思考,避免一上來就暴力求解,形成巨大的性能開銷,應儘可能將程序優化到線性階或線性對數階之內。

相關文章
相關標籤/搜索