|
| 1 | +# 题目描述(简单难度) |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | +给一个数组,找出一个连续的子数组,长度任意,和最大。 |
| 6 | + |
| 7 | +# 解法一 动态规划思路一 |
| 8 | + |
| 9 | +用一个二维数组 dp\[ i \] \[ len \] 表示从下标 i 开始,长度为 len 的子数组的元素和。 |
| 10 | + |
| 11 | +这样长度是 len + 1 的子数组就可以通过长度是 len 的子数组去求,也就是下边的递推式, |
| 12 | + |
| 13 | +dp \[ i \] \[ len + 1 \] = dp\[ i \] \[ len \] + nums [ i + len - 1 ]。 |
| 14 | + |
| 15 | +当然,和[第 5 题](https://leetcode.windliang.cc/leetCode-5-Longest-Palindromic-Substring.html)一样,考虑到求 i + 1 的情况的时候,我们只需要 i 时候的情况,所有我们其实没必要用一个二维数组,直接用一维数组就可以了。 |
| 16 | + |
| 17 | +```java |
| 18 | +public int maxSubArray(int[] nums) { |
| 19 | + int n = nums.length; |
| 20 | + int[] dp = new int[n]; |
| 21 | + int max = Integer.MIN_VALUE; |
| 22 | + for (int len = 1; len <= n; len++) { |
| 23 | + for (int i = 0; i <= n - len; i++) { |
| 24 | + //直接覆盖掉前边对应的情况就行 |
| 25 | + dp[i] = dp[i] + nums[i + len - 1]; |
| 26 | + //更新 max |
| 27 | + if (dp[i] > max) { |
| 28 | + max = dp[i]; |
| 29 | + } |
| 30 | + } |
| 31 | + } |
| 32 | + return max; |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +时间复杂度:O(n²)。 |
| 37 | + |
| 38 | +空间复杂度:O(n)。 |
| 39 | + |
| 40 | +# 解法二 动态规划思路二 |
| 41 | + |
| 42 | +参考[这里](https://leetcode.com/problems/maximum-subarray/discuss/20193/DP-solution-and-some-thoughts)。 |
| 43 | + |
| 44 | +用一个一维数组 dp [ i ] 表示以下标 i 结尾的子数组的元素的最大的和,也就是这个子数组最后一个元素是下边为 i 的元素,并且这个子数组是所有以 i 结尾的子数组中,和最大的。 |
| 45 | + |
| 46 | +这样的话就有两种情况, |
| 47 | + |
| 48 | +* 如果 dp [ i - 1 ] < 0,那么 dp [ i ] = nums [ i ]。 |
| 49 | +* 如果 dp [ i - 1 ] >= 0,那么 dp [ i ] = dp [ i - 1 ] + nums [ i ]。 |
| 50 | + |
| 51 | +```java |
| 52 | +public int maxSubArray(int[] nums) { |
| 53 | + int n = nums.length; |
| 54 | + int[] dp = new int[n]; |
| 55 | + int max = nums[0]; |
| 56 | + dp[0] = nums[0]; |
| 57 | + for (int i = 1; i < n; i++) { |
| 58 | + //两种情况更新 dp[i] |
| 59 | + if (dp[i - 1] < 0) { |
| 60 | + dp[i] = nums[i]; |
| 61 | + } else { |
| 62 | + dp[i] = dp[i - 1] + nums[i]; |
| 63 | + } |
| 64 | + //更新 max |
| 65 | + max = Math.max(max, dp[i]); |
| 66 | + } |
| 67 | + return max; |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +时间复杂度: O(n)。 |
| 72 | + |
| 73 | +空间复杂度:O(n)。 |
| 74 | + |
| 75 | +当然,和以前一样,我们注意到更新 i 的情况的时候只用到 i - 1 的时候,所以我们不需要数组,只需要两个变量。 |
| 76 | + |
| 77 | +```java |
| 78 | +public int maxSubArray(int[] nums) { |
| 79 | + int n = nums.length; |
| 80 | + //两个变量即可 |
| 81 | + int[] dp = new int[2]; |
| 82 | + int max = nums[0]; |
| 83 | + dp[0] = nums[0]; |
| 84 | + for (int i = 1; i < n; i++) { |
| 85 | + //利用求余,轮换两个变量 |
| 86 | + if (dp[(i - 1) % 2] < 0) { |
| 87 | + dp[i % 2] = nums[i]; |
| 88 | + } else { |
| 89 | + dp[i % 2] = dp[(i - 1) % 2] + nums[i]; |
| 90 | + } |
| 91 | + max = Math.max(max, dp[i % 2]); |
| 92 | + } |
| 93 | + return max; |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +时间复杂度: O(n)。 |
| 98 | + |
| 99 | +空间复杂度:O(1)。 |
| 100 | + |
| 101 | + 再粗暴点,直接用一个变量就可以了。 |
| 102 | + |
| 103 | +```java |
| 104 | +public int maxSubArray(int[] nums) { |
| 105 | + int n = nums.length; |
| 106 | + int dp = nums[0]; |
| 107 | + int max = nums[0]; |
| 108 | + for (int i = 1; i < n; i++) { |
| 109 | + if (dp < 0) { |
| 110 | + dp = nums[i]; |
| 111 | + } else { |
| 112 | + dp= dp + nums[i]; |
| 113 | + } |
| 114 | + max = Math.max(max, dp); |
| 115 | + } |
| 116 | + return max; |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +而对于 |
| 121 | + |
| 122 | +```java |
| 123 | +if (dp < 0) { |
| 124 | + dp = nums[i]; |
| 125 | +} else { |
| 126 | + dp= dp + nums[i]; |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +其实也可以这样理解, |
| 131 | + |
| 132 | +```java |
| 133 | +dp= Math.max(dp + nums[i],nums[i]); |
| 134 | +``` |
| 135 | + |
| 136 | +然后就变成了[这里](https://leetcode.com/problems/maximum-subarray/discuss/20211/Accepted-O(n)-solution-in-java)提到的算法。 |
| 137 | + |
| 138 | +# 解法三 折半 |
| 139 | + |
| 140 | +题目最后说 |
| 141 | + |
| 142 | +> If you have figured out the O(*n*) solution, try coding another solution using the divide and conquer approach, which is more subtle. |
| 143 | +
|
| 144 | +[这里](If you have figured out the O(*n*) solution, try coding another solution using the divide and conquer approach, which is more subtle.)找到了种解法,分享下。 |
| 145 | + |
| 146 | +假设我们有了一个函数 int getSubMax(int start, int end, int[] nums) ,可以得到 num [ start, end ) (左包右不包) 中子数组最大值。 |
| 147 | + |
| 148 | +如果, start == end,那么 getSubMax 直接返回 nums [ start ] 就可以了。 |
| 149 | + |
| 150 | +```java |
| 151 | +if (start == end) { |
| 152 | + return nums[start]; |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +然后对问题进行分解。 |
| 157 | + |
| 158 | +先找一个 mid , mid = ( start + end ) / 2。 |
| 159 | + |
| 160 | +然后,对于我们要找的和最大的子数组有两种情况。 |
| 161 | + |
| 162 | +* mid 不在我们要找的子数组中 |
| 163 | + |
| 164 | + 这样的话,子数组的最大值要么是 mid 左半部分数组的子数组产生,要么是右边的产生,最大值的可以利用 getSubMax 求出来。 |
| 165 | + |
| 166 | + ```java |
| 167 | + int leftMax = getSubMax(start, mid, nums); |
| 168 | + int rightMax = getSubMax(mid + 1, end, nums); |
| 169 | + ``` |
| 170 | + |
| 171 | +* mid 在我们要找的子数组中 |
| 172 | + |
| 173 | + 这样的话,我们可以分别从 mid 左边扩展,和右边扩展,找出两边和最大的时候,然后加起来就可以了。当然如果,左边或者右边最大的都小于 0 ,我们就不加了。 |
| 174 | + |
| 175 | + ```java |
| 176 | + int containsMidMax = getContainMidMax(start, end, mid, nums); |
| 177 | + private int getContainMidMax(int start, int end, int mid, int[] nums) { |
| 178 | + int containsMidLeftMax = 0; //初始化为 0 ,防止最大的值也小于 0 |
| 179 | + //找左边最大 |
| 180 | + if (mid > 0) { |
| 181 | + int sum = 0; |
| 182 | + for (int i = mid - 1; i >= 0; i--) { |
| 183 | + sum += nums[i]; |
| 184 | + if (sum > containsMidLeftMax) { |
| 185 | + containsMidLeftMax = sum; |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + } |
| 190 | + int containsMidRightMax = 0; |
| 191 | + //找右边最大 |
| 192 | + if (mid < end) { |
| 193 | + int sum = 0; |
| 194 | + for (int i = mid + 1; i <= end; i++) { |
| 195 | + sum += nums[i]; |
| 196 | + if (sum > containsMidRightMax) { |
| 197 | + containsMidRightMax = sum; |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + return containsMidLeftMax + nums[mid] + containsMidRightMax; |
| 202 | + } |
| 203 | + ``` |
| 204 | + |
| 205 | + 最后,我们只需要返回这三个中最大的值就可以了。 |
| 206 | + |
| 207 | +综上,递归出口,问题分解就都有了。 |
| 208 | + |
| 209 | +```java |
| 210 | +public int maxSubArray(int[] nums) { |
| 211 | + return getSubMax(0, nums.length - 1, nums); |
| 212 | +} |
| 213 | + |
| 214 | +private int getSubMax(int start, int end, int[] nums) { |
| 215 | + //递归出口 |
| 216 | + if (start == end) { |
| 217 | + return nums[start]; |
| 218 | + } |
| 219 | + int mid = (start + end) / 2; |
| 220 | + //要找的数组不包含 mid,然后得到左边和右边最大的值 |
| 221 | + int leftMax = getSubMax(start, mid, nums); |
| 222 | + int rightMax = getSubMax(mid + 1, end, nums); |
| 223 | + //要找的数组包含 mid |
| 224 | + int containsMidMax = getContainMidMax(start, end, mid, nums); |
| 225 | + //返回它们 3 个中最大的 |
| 226 | + return Math.max(containsMidMax, Math.max(leftMax, rightMax)); |
| 227 | +} |
| 228 | + |
| 229 | +private int getContainMidMax(int start, int end, int mid, int[] nums) { |
| 230 | + int containsMidLeftMax = 0; //初始化为 0 ,防止最大的值也小于 0 |
| 231 | + //找左边最大 |
| 232 | + if (mid > 0) { |
| 233 | + int sum = 0; |
| 234 | + for (int i = mid - 1; i >= 0; i--) { |
| 235 | + sum += nums[i]; |
| 236 | + if (sum > containsMidLeftMax) { |
| 237 | + containsMidLeftMax = sum; |
| 238 | + } |
| 239 | + } |
| 240 | + |
| 241 | + } |
| 242 | + int containsMidRightMax = 0; |
| 243 | + //找右边最大 |
| 244 | + if (mid < end) { |
| 245 | + int sum = 0; |
| 246 | + for (int i = mid + 1; i <= end; i++) { |
| 247 | + sum += nums[i]; |
| 248 | + if (sum > containsMidRightMax) { |
| 249 | + containsMidRightMax = sum; |
| 250 | + } |
| 251 | + } |
| 252 | + } |
| 253 | + return containsMidLeftMax + nums[mid] + containsMidRightMax; |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +时间复杂度:O(n log ( n ))。由于 getContainMidMax 这个函数耗费了 O(n)。所以时间复杂度反而相比之前的算法变大了。 |
| 258 | + |
| 259 | +空间复杂度: |
| 260 | + |
| 261 | +# 总 |
| 262 | + |
| 263 | +解法一和解法二的动态规划,只是在定义的时候一个表示以 i 开头的子数组,一个表示以 i 结尾的子数组,却造成了时间复杂度的差异。问题就是解法一中求出了太多的没必要的和,不如解法二直接,只保存最大的和。解法三,一半一半的求,从而使问题分解,也是经常遇到的思想。 |
0 commit comments