今天咱们要讨论的是经典的问题--最大子数组问题。题目以下:给定一个数组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,再进行加下去就没有意思了,因此两边都须要进行归位。若是循环往复下去。
最后附上结果:
最后进行一下总结,当咱们在解决问题的时候,优先放弃暴力法。