大家好,我是听雨,是一名跨考计算机专业的研一学生。为提高编程的水平,计划每天刷编程题目。由于我是算法小白,所以开始只能从简单题开始写贴,请大家多多包涵,希望和大家一起进步!**
题目
今天的题目选自力扣53. 最大子数组和
解题思路
想必刚开始接触算法的小伙伴都有见过这道经典题目。今天由我来介绍三种类型的解题方法。
一. 枚举法
1. 暴力枚举
还没有接触算法时候的我见到这道题第一印象便是通过循环来枚举出所有的结果,从而对比找到最大值
代码如下:
class Solution {
public int maxSubArray(int[] nums) {
int sum;
//随便设置一个比较小的初始值
int max = -1000;
int len = nums.length;
for(int i = 0; i < len; i ++){
for(int j = len - 1 ;j >= i; j --){
sum =0 ;
for(int k = i; k <= j ; k ++){
sum += nums[k];
}
if(sum >= max){
max = sum;
}
}
}
return max;
}
}
理论上这是可以求解出结果的,但三层for循环使得时间复杂度为 O ( n 3 ) O(n^3) O(n3),当数组变大时,显然不可行。当然,力扣的评测也给了超时。
2. 优化枚举
在进行暴力穷举的时候,可以发现,有些计算是重复的,例如:计算 nums[2:5] 的数组和的时候,需要进行三次加法,而进行 nums[2:6] 的数组和的时候又需要重新计算和,需要四次加法运算,但实际上 刚才的 nums[2:5] 已经计算出来了,只需要加上 nums[6] 即可求出结果。这样便可以对算法进行优化。
代码如下:
class Solution {
public int maxSubArray(int[] nums) {
int sum;
//随便设置一个比较小的初始值
int max = -100000;
int len = nums.length;
for(int i = 0; i < len; i ++){
sum = 0;
for(int j = i ;j < len; j ++){
sum = sum + nums[j];
max = max > sum ? max : sum;
}
}
return max;
}
}
二. 分治法
应该可以感觉到用枚举的方法已经不能再降低时间复杂度了。此时我们需要用换一种思路来寻求解决方案。本文的第二种方法便是分治法。分治法的主要思想便是分而治之。因此对于一个大数组,我们需要将其拆分成小数组来解决。最常用的便是使用折半拆分。
如图一个大数组求最大子数组变可以拆分为两个小数组分别求最大子数组,然后比较结果求得最大值。但显然这样只比较左右两个小数组的值是不对的。因为还可能存在一个子数组是横跨左右小数组的情况。所以还需要找到中间小数组的最大子数组的值。
如何寻找中间数组
S
3
S_3
S3 的最大值呢?通过观察可以发现,这个数组一定是贯穿中间的,那么将
S
3
S_3
S3数组按照中点分开分成 left 和 right 两部分来进行求最大值,最后将二者最大值加起来便可以得到结果。如上图所示求left和right数组的最大值是非常简单的,因为它们有一端边界是固定的,只需要使用一次for逐个对比即可。
因为仅仅遍历了一次数组,所以求中间数组的最大值的时间复杂度为 O ( n ) O(n) O(n)。最后比较左中右三部分哪个数组的最大值大即可。以下是算法的实例:
代码如下:
class Solution {
public int maxSubArray(int[] nums) {
int len =nums.length;
if(len == 1){
return nums[0];
}
int mid = len/2;
//将原数组分成左右两个数组
int[] left = new int[mid];
int[] right = new int[len - mid];
//数组拷贝
for(int i = 0; i < mid; i ++ ){
left[i] = nums[i];
}
for(int i = 0; i < len - mid; i ++ ){
right[i] = nums[mid + i];
}
//求解左右最大子数组
int leftMax = maxSubArray(left);
int rightMax = maxSubArray(right);
//求解跨中间最大子数组
int sum = 0, maxR = -10000000,maxL = -10000000;
for(int i = 0; i < right.length; i ++){
sum += right[i];
maxR = maxR > sum? maxR:sum;
}
sum = 0;
for(int i = left.length - 1; i >= 0; i --){
sum += left[i];
maxL = maxL > sum? maxL:sum;
}
int maxMid = maxL + maxR;
//比较左中右最大值
return Math.max(Math.max(leftMax,rightMax),maxMid);
}
}
该算法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),又加速了一些。令人激动的是虽然在内存和时间两方面都不太好,但终于可以通过测试,这足以让我高兴,毕竟代码能力确实很差,尽管思路正确,实现的也很臃肿。
三. 动态规划
对于这道题,有没有更好的方法呢?能否让时间复杂度为
O
(
n
)
O(n)
O(n)呢?还真有,那便是动态规划。动态规划的关键在于分析最优子结构,找到递推关系式,那对于这道题,它的关系式是如何的呢?
给定一个数组X,不妨设置其以 X[i] 开头的数组最大子数组和的值为D[i],则D数组存放的最大值便为X数组的最大子数组和。
通过观察可以发现 D[i] 的值与 D[i + 1] 的值的关系 取决于 X[i],并由此可以得到最优子序列的一个递推关系式:
D
[
i
]
=
{
X
[
i
]
+
D
[
i
+
1
]
,
D
[
i
+
1
]
>
0
X
[
i
]
,
D
[
i
+
1
]
≤
0
D[i] =\begin{cases} X[i] + D[i+1],\,\,D[i+1]>0\\ X[i],\,\,D[i+1]\le0\\ \end{cases}
D[i]={X[i]+D[i+1],D[i+1]>0X[i],D[i+1]≤0
初始条件是 D[length-1] = X[length-1],之后D数组的值都可以通过该递推公式得到,最后找到最大值即可。
代码如下:
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
int[] dp = new int[len];
dp[len - 1] = nums[len - 1];
int max = dp[len - 1];
for(int i = len - 2; i >= 0 ;i --){
if(dp[i + 1] > 0){
dp[i] = dp[i + 1] + nums[i];
}else{
dp[i] = nums[i];
}
if(dp[i] > max){
max = dp[i];
}
}
return max;
}
这次测试的结果让人满意
对于内存的优化,我认为可以将D数组换成两个变量,因为只需要找到最大值即可,显然用一个数组存放所有结果有些损耗。
总结
对于算法小白的我来说,只能写出优化枚举的方法,后续的分治和动态规划我参考了北航童咏昕教授的算法设计与分析课程。没有接触过算法的小伙伴可以看看来进行入门!