问题
//给一个整数数组 nums , // 请找出一个具有"最大和"的连续子数组(子数组最少包含一个元素), // 返回其最大和。
//子数组 是数组中的一个"连续部分"
分析
// 这是一道典型的使用「动态规划」
(连续、只要结果)解决的问题,
// 需要我们掌握动态规划问题设计状态的技巧(无后效性),
// 并且需要知道如何推导状态转移方程,
// 最后再去优化空间。
转换为若干个 子问题
连续:可以求出 ”所有“ 经过(以**结尾的)输入数组的 某一个数 的连续子数组的最大和(只要这个结果)。
如果编号为 i 的子问题的结果是负数或者 0 ,
那么编号为 i + 1 的子问题就可以把编号为 i 的子问题的结果”舍弃掉“(因为要求是最大和)
代码
main函数
package DataStructure_start;
public class DS20230109 {
public static void main(String[] args) {
int[] nums = {-1,9,-2,3,5,6};
System.out.println(maxSubArray(nums));
System.out.println(maxSubArray1(nums));
System.out.println(maxSubArray2(nums));
System.out.println(maxSubArray3(nums));
}
}
方法一:动态规划
参考1:(子数组)
时间复杂度:O(n),N是输入数组的长度
public static int maxSubArray(int[] nums) {
int len = nums.length;
// dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
int[] dp = new int[len];
dp[0] = nums[0];
// 如果编号为 i 的子问题的结果是负数或者 0 ,
// 那么编号为 i + 1 的子问题就可以把编号为 i 的子问题的结果”舍弃掉“(因为要求是最大和)
// 即:如果i的子问题的结果是正数,则保留
for (int i = 1; i < len; i++) {
// 如果前一个数为正数
if (dp[i - 1] > 0) {
// 则再加下一个数
// ?但只是相邻的两个数 ×
// dp[i] 表示:以 nums[i] 结尾的连续子数组的最大和
// 注意区别dp[](一个连续的数组)与num[](一个数)的区别
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
}
}
// 也可以在上面遍历的同时求出 res 的最大值,这里我们为了语义清晰分开写,大家可以自行选择
int res = dp[0];
for (int i = 1; i < len; i++) {
res = Math.max(res, dp[i]);//调用函数Math:max函数:判断谁是最大值(三元表达式)
}
return res;//返回最大和
}
补充:
三元表达式
public static int max(int a, int b) {
return (a >= b) ? a : b;
}
时间复杂度从小到大排序:
(由里向外分析时间复杂度;取最大的时间复杂度)
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
参考2:优化后
时间复杂度:O(n),N是输入数组的长度
public static int maxSubArray1(int[] nums) {
int pre = 0;//前一个数
int res = nums[0];//最大和
// 只适用于数组,循环累加(优化点★)
for (int num : nums) {
pre = Math.max(pre + num, num);
res = Math.max(res, pre);
}
return res;
}
有后效性:
如果之前的阶段求解的子问题的结果包含了一些不确定的信息,导致了后面的阶段求解的子问题无法得到,或者很难得到,这叫「有后效性」
解决「有后效性」的办法是固定住需要分类讨论的地方,记录下更多的结果。在代码层面上表现为:
状态数组增加维度;
把状态定义得更细致、准确:状态定义只解决路径来自左右子树的其中一个子树。
需要经常思考 为什么想到需要这样定义状态。
动态规划的是首先对数组进行遍历,当前最大连续子序列和为 sum,结果为 ans
如果 sum > 0,则说明 sum 对结果有增益效果,则 sum 保留并加上当前遍历数字
如果 sum <= 0,则说明 sum 对结果无增益效果,需要舍弃,则 sum 直接更新为当前遍历数字
每次比较 sum 和 ans的大小,将最大值置为ans,遍历结束返回结果
时间复杂度:O(n)
public static int maxSubArray3(int[] nums) {
int ans = nums[0];
int sum = 0;
// 此处类似方法一参考1中的优化部分,即for循环数组
for(int num: nums) {
// 正数
if(sum > 0) {
// 累加sum
sum += num;
// 负数,保持不变
} else {
sum = num;
}
// 三元表达式:比较取最大值
ans = Math.max(ans, sum);
}
return ans;
}
方法二:分治法(分类讨论,分成三部分)
复杂度分析:
时间复杂度:O(NlogN),这里递归的深度是对数级别的,每一层需要遍历一遍数组(或者数组的一半、四分之一);
空间复杂度:O(logN),需要常数个变量用于选取最大值,需要使用的空间取决于递归栈的深度。
连续子序列的最大和主要由这三部分子区间里元素的最大和得到:
第 1 部分:子区间 [left, mid];
第 2 部分:子区间 [mid + 1, right];
第 3 部分:包含子区间 [mid , mid + 1] 的子区间,
即 nums[mid] 与 nums[mid + 1] 一定会被选取(因为跨区。从中间向两边扩散,扩散到底 选出最大值)。
对这三个部分求最大值即可。
public static int maxSubArray2(int[] nums) {
int len = nums.length;
if (len == 0) {
return 0;
}
return maxSubArraySum(nums, 0, len - 1);
}
private static int maxCrossingSum(int[] nums, int left, int mid, int right) {
// 一定会包含 nums[mid] 这个元素
int sum = 0;
int leftSum = Integer.MIN_VALUE;
// 左半边包含 nums[mid] 元素,最多可以到什么地方
// 走到最边界,看看最值是什么
// 计算以 mid 结尾的最大的子数组的和
for (int i = mid; i >= left; i--) {
sum += nums[i];
if (sum > leftSum) {
leftSum = sum;
}
}
sum = 0;
int rightSum = Integer.MIN_VALUE;
// 右半边不包含 nums[mid] 元素,最多可以到什么地方
// 计算以 mid+1 开始的最大的子数组的和
for (int i = mid + 1; i <= right; i++) {
sum += nums[i];
if (sum > rightSum) {
rightSum = sum;
}
}
return leftSum + rightSum;
}
private static int maxSubArraySum(int[] nums, int left, int right) {
if (left == right) {
return nums[left];
}
int mid = left + (right - left) / 2;
return max3(maxSubArraySum(nums, left, mid),
maxSubArraySum(nums, mid + 1, right),
maxCrossingSum(nums, left, mid, right));
}
private static int max3(int num1, int num2, int num3) {
return Math.max(num1, Math.max(num2, num3));
}
参考
作者:liweiwei1419
来源:力扣(LeetCode)
作者:guanpengchn
来源:力扣(LeetCode)