题目描述
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
题目难度:简单
** 示例1**
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
暂且忽略时间复杂度要求,这题看起来很熟悉,好像是我本科算法课上老师用来讲时间复杂度的例子,看起来《不难》,但是却有多种解法,下面我记录了几种比较常见的解法,供读者参考。
解法1(暴力破解)
总体思路
这题比较容易想到的方法可能就是暴力破解法(似乎大多数题目都可以用暴力破解法来解决),其思路也很简单,就是遍历出所有的可能,然后找出符合条件的即可。就本题而言,就是遍历所有的连续子数组(包括不同位置,不同长度等),依次计算出他们的和,从中找出最大的即可。
代码实现
public static int maxSubArray(int[] nums) {
int maxValue = nums[0];//连续数组最大值
for(int i=0;i<nums.length;i++){//遍历连续数组起始位置
for (int j=i;j<nums.length;j++){//遍历连续数组终止位置
//计算所得连续数组的和
int sumValue = 0;
for (int k=i; k<=j;k++){
sumValue += nums[k];
}
//找出最大和
if (sumValue > maxValue){
maxValue = sumValue;
}
}
}
return maxValue;
}
运行结果
结果分析
由以上运行结果可知该种解法对于所给示例能够得到正确答案,但是很容易想到肯定会超时,因为题目要求的时间复杂度是O(n),而此种解法的时间复杂度是O(n³),显然差距过大,提交到leetcode平台上后果然不出所料,确实超时了。
才发现,好像还有测试用例不通过的,暂且不管它了。(不过leetcode也是真的狗啊,竟然给出了个这么长的数组)
解法2(小暴力)
总体思路
解法2是在解法1的基础上进行了改进。不难发现解法1在计算连续数组的和的时候进行了很多重复计算,而这些重复计算并不是必须的,完全可以采用累加的方式实现,即这个连续数组的和是在上一个连续数组和的基础上得到的,如此一来便可以去掉一层循环。
代码实现
public static int maxSubArray(int[] nums) {
int maxValue = nums[0];//记录连续数组最大和
for(int i=0;i<nums.length;i++){
//记录连续数组的和
int sumValue = 0;
for (int j=i;j<nums.length;j++){
//本次和是在上一次的基础上进行得到的
sumValue += nums[j];
//获取最大和
if (sumValue > maxValue){
maxValue = sumValue;
}
}
}
return maxValue;
}
运行结果
结果分析
由以上结果可知,此种解法也能够得出正确结果,虽然时间复杂度较解法1有所改进,但是还有O(n²),依然不能满足本题要求,提交到leetcode平台后结果也是如此。
解法3(分而治之)
总体思路
记得浙江大学陈越老师曾经在他的课里说过,作为一个合格的程序员,当你设计了一个算法的复杂度为O(n²)时要想想能不能将其降为O(nlogn)。受此启发我们也可以想办法将其时间复杂度降为O(nlogn),其实我接触过的时间复杂度为O(nlogn)的算法并不多,其中还有印象的应该是归并排序,,而归并排序的核心思想就是“分治”,在本题中我们依然可以借助这种“分而治之”的思想。“分治法”的主要思想就是将大问题分解为若干个小问题,然后将各个小问题各个击破,然后再将各个子问题合并即可得到原问题的解,其实这和递归的思想有些相似,因此“分治法”一般和递归是分不开的。具体到本题而言就是利用数组的“中点”将原数组分解为左右两个小数组,分别计算出左右两个子数组中的连续子数组最大和,然后将左右两个数组合并得到包含所选中点的连续数组的最大和,最后再选出左右连续数组最大和、合并后包含中点连续数组最大和三者中的最大值即可,然后分别对左右两个小数组执行以上过程即能得到正确结果。
代码实现
public static int maxSum(int[] nums,int left,int right){
//递归返回
if (left == right){
return nums[left];
}
//得到中点值
int center = (left + right) / 2;
//得到左边数组中连续数组最大和
int leftMaxSum = maxSum(nums,left,center);
//得到右边数组中连续数组最大和
int rightMaxSum = maxSum(nums,center+1,right);
//计算从中点向左连续子数组最大和
int leftBorderSum = 0,leftMaxBorderSum = nums[center];
for (int i = center;i>=left;i--){
leftBorderSum += nums[i];
if (leftBorderSum > leftMaxBorderSum){
leftMaxBorderSum = leftBorderSum;
}
}
//计算从中点向右连续数组最大和
int rightBorderSum = 0,rightMaxBorderSum = nums[center + 1];
for (int i = center + 1;i<=right;i++){
rightBorderSum += nums[i];
if (rightBorderSum > rightMaxBorderSum){
rightMaxBorderSum = rightBorderSum;
}
}
//返回三者中最大值
return Math.max(Math.max(leftMaxSum,rightMaxSum),leftMaxBorderSum+rightMaxBorderSum);
}
public static int maxSubArray(int[] nums) {
return maxSum(nums,0,nums.length-1);
}
运行结果
结果分析
由以上运行结果可以知道该种解法能够得到正确结果,但是时间复杂度为O(nlogn),而题目要求的时间复杂度为O(n),猜想可能无法满足要求,但是提交到leetcode平台后发现竟然通过了测试。
由此可见时间复杂度O(nlogn)和O(n)是非常接近的。
解法4
总体思路
解法3虽然通过了测试,但是实际上并不满足题目要求,因此此题应该还有更加快速的解法。经分析发现,直接遍历进行累加即可得到正确结果,但是需要不断调整累加和的值,在累加和为负时需要将累加和置为0(即将这一段连续数组舍弃),因为题目要求的是连续子数组最大值,不能进行跳跃,而负值对于得到连续子数组最大值是没有任何帮助的,反而会削弱最大值。基于以上思想,可以得到解法4.
代码实现
public static int maxSubArray(int[] nums) {
//最大值和累加值
int maxSum = nums[0],sum = 0;
for (int i=0;i<nums.length;i++){
//对数组进行累加
sum += nums[i];
//更新最大值
if (sum > maxSum ){
maxSum = sum;
}
//更新累加值
if (sum < 0){
sum = 0;
}
}
return maxSum;
}
运行结果
结果分析
由以上运行结果可知解法4也能够得到正确结果,而且时间复杂度为O(n),提交到leetcode平台后也能够得到正确结果。
总结
此题共用了4种解法进行解决,时间复杂度也不尽相同,解法1和解法2是常规解法,解法3值得认真学习,解法4具有一定的技巧,有时难以想到。