今天咱們要討論的是經典的問題--最大子數組問題。題目以下:給定一個數組A[0,....n-1],求A的連續子數組,使得該數組的和最大。例如:數組:1,-2,3,10,-4,7,2,-5; 最大子數組:3,10,-4,7,2;算法
這個問題已經算是比較經典的問題,這個問題有好幾種的求法,可是咱們將去除暴力法,由於時間複雜度過高。接下來就是具體體現。數組
一、分治法。spa
分治法是算法中常見的方法,它的主要思想就是分而治之。將問題最小化直到不能分爲止,而後進行比較大小。具體的作法是將數組從中間分開,那麼最大子數組只有三種狀況,要麼所有在左邊,要麼所有在右邊,要麼一點在左邊,一點在右邊。因此說,徹底在左數組、右數組就用遞歸解決。跨在分界點上:其實是左數組的最大後綴和右數組的最大前綴和。所以,從分界點向前掃,向後掃就好了。如今咱們就以題目中的數組爲例,咱們先來看看代碼:code
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 管窺算法 { class Program { struct SubArray { public int start; public int end; public int sum; } static SubArray MaxAddSub(int []a,int start,int end) { if (start == end) { SubArray subArray; subArray.start = start; subArray.end = end; subArray.sum = a[end]; return subArray; } int middle = (start + end) / 2; SubArray subArray1=MaxAddSub(a, start, middle); SubArray subArray2 = MaxAddSub(a, middle + 1, end); //第三種狀況分在兩邊的 int sum1 = a[middle]; int startIndex = middle; int total = 0; for (int i = middle; i >= start; i--) { total += a[i]; if (sum1 < total) { sum1 = total; startIndex = i; } } int sum2 = a[middle + 1]; int endIndex = middle + 1; total = 0; for (int i = middle+1; i <=end; i++) { total += a[i]; if (sum2 < total) { sum2 = total; endIndex = i; } } SubArray subArray3; subArray3.start = startIndex; subArray3.end = endIndex; subArray3.sum = sum1 + sum2; if (subArray1.sum >= subArray2.sum && subArray1.sum >= subArray3.sum) { return subArray1; } else if (subArray2.sum >= subArray1.sum && subArray2.sum >= subArray3.sum) { return subArray2; } else { return subArray3; } } static void Main(string[] args) { int[] a = { 1, -2, 3, 10, -4, 7, 2, -5 }; SubArray subArray = MaxAddSub(a, 0, a.Length - 1); Console.WriteLine("最大最大子數組爲:"); for (int i = subArray.start; i <= subArray.end; i++) { Console.Write(a[i] + " "); } Console.ReadKey(); } } }
接下來再來看看這個圖片:blog
在代碼中是有這句話的:遞歸
SubArray subArray1=MaxAddSub(a, start, middle);
SubArray subArray2 = MaxAddSub(a, middle + 1, end);索引
這兩段代碼的做用顯而易見的是用來只判斷左數據和右數組組的,可是真實的其實不是這樣的,接下來咱們就對遞歸進行分析。咱們在執行第一次代碼的時候,左邊的爲0 1 2 3(這個爲索引。下面都是這個意思),右邊爲4 5 6 7;而後subArray1會在一次的調用自己第二次左邊的爲0 1,右邊爲爲2 3;接着的第三次仍是同樣的 左邊爲0 ,在這個時候代碼就到頭了,而後就會返回給上一級的subArray1,注意一下這個爲第三次的subArray1,這個時候左數組的使命正式完成了,終於能夠進行第一次右邊數組的遍歷了,在第一次右邊數組由於只有一個1因此沒有辦法進行遍歷,因此只能進行返回的操做了,這樣與第三次subArray1同層的subArray也出來了。在這個時候咱們能夠進行註明一下:先把第三次subArray1設爲第三層,第三層 :subArray1爲0 ,subaArray2爲1;這兩個左右數組賦值完了,就能夠進行中間狀況的判斷了,subArry3就是subArray1+subArray2的值,而後他們三個進行比較最大值並進行返回 。在這裏其實咱們並不難看出01比較出來的之就是第二層左邊的值。同理右邊進行分類的時候能夠相同狀況。這樣的話,咱們就能夠知道了其實分治法中並無大中間的意思。咱們在理解分治法的時候,必定要明白分支法就是讓問題進行分離,把它們進行分解一直分到不可分爲止,而後讓兩個最小的變量進行比較大小,真正的中間狀況實際上是在不可再分下兩個相鄰值得相加。最後在說一句,在理解遞歸的時候,咱們應該把落腳點放在不可再分的地方,這個纔是遞歸的本質。圖片
二、動態規劃法string
在上面咱們使用的分治法的方法,它的時間複雜度爲Ο(nlogn) ,相對於暴力法的時間複雜度n^2也是進步了很多;可是咱們是否能夠再跟進一步呢?答案固然是能夠的,就是咱們的動態規劃算法,這個算法將更加先進,由於它把時間複雜度降低到n,只須要須要一個for循環就能夠完成目標。接下來讓咱們看看代碼以下:it
static SubArray MaxSubDPMethod(int[] args) { int result = args[0]; int sum=args[0]; SubArray subArray; subArray.start=0; subArray.end=1; for (int i = 1 ; i < args.Length-1; i++) { if (sum > 0) { sum += args[i]; subArray.end = i; } else { sum = args[i]; subArray.start = i; subArray.end = i; } if(result<sum) result=sum; } subArray.sum = result; return subArray; }
其實咱們看到這裏也是不難的發現,咱們在思考最大子數組問題的時候,總喜歡將一端進行固定,在暴力法和分治法那裏都是這樣的,因而咱們的時間複雜度將會大幅度的提升,由於你對一端進行固定的話,那麼每一次進行循環操做的時候都會從新來一遍沒有意義的操做。最大子數組的問題其實就是尋找最大值,在數組中的開始到結尾什麼狀況會有最大值,若是有幾個相鄰的數字他們已經都是爲負的了,那咱們在對下面的數字進行運算的時候,爲何還要加上他們嗎?動態規劃的本質就是兩邊的標誌位當添加不知足的時候都要進行動做,這樣就看起來他們一直都在動做,是一個動態變化的過程,因此說就死動態規劃了。在代碼中咱們能夠看到當咱們sum>0時只須要尾標誌位動,由於這種狀況是知足題目要求的,因此頭標誌位就不用進行移動了。另外一種狀況就是小於0了,只要小於0,再進行加下去就沒有意思了,因此兩邊都須要進行歸位。若是循環往復下去。
最後附上結果:
最後進行一下總結,當咱們在解決問題的時候,優先放棄暴力法。