题目
给你一个整数数组
nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
示例1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例2:
输入:nums = [1] 输出:1
示例3:
输入:nums = [5,4,-1,7,8] 输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
进阶:如果你已经实现复杂度为 O(n)
的解法,尝试使用更为精妙的 分治法 求解。
解题思路
法一:动态规划(执行用时:140ms,内存消耗:21.6MB)
动态规划中需要明确dp数组所存储元素的意义,即dp[i]存储的不是从0到 i 这个范围内所得到的最大的连续子数组的和,而是以 nums[i] 为结尾的子数组所能达到的最大的和。
为了节约不必要的空间开销,可以将 nums[i] 看成我们的dp数组。整体思路就是先设定一个初始值 res = nums[0],然后将nums数组从1到 len(nums) 进行迭代,每一步对以前面一个元素为结尾的连续序列的最大值 + 当前元素值 和当前元素值进行比较,将较大的一方记录在当前位置(即nums[i] = max(nums[i-1] + nums[i] , nums[i]),接着更新最大值res即可。
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
res = nums[0]
for i in range(1,len(nums)):
nums[i] = max(nums[i-1] + nums[i] , nums[i])
res = max(res,nums[i])
return res
时间复杂度:O(N)
空间复杂度:O(1)
法二:贪心算法(执行用时:72ms,内存消耗:21.4MB)
一个数要想变得更大,那肯定是要加上一个正数才能变大,而负数只会拉低总和,因此当我们遇到-2, 1 时,计算起点肯定是从1开始计算的。那么我们就可以考虑遍历nums,从头开始用count进行累计,当 count 加上 nums[i] 变成负数时,就应该从 nums[i+1] 开始重新从0累计count了,因为当count变成负数时,只会拖累总和。
动画演示效果链接:
https://pic.leetcode-cn.com/1609723214-aYExwW-0081Kckwly1gmbec53gn8g30bk08ynpd.gif
此外,需要注意的是数组第一个数字可能是很小的负数,因此用于存储最大和的初始值res应该设为负无穷大,即 res = -float('inf')
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
count = 0
res = -float('inf')
for i in range(len(nums)):
count += nums[i]
if count >= res:
res = count
if count < 0:
count = 0
return res
时间复杂度:O(N)
空间复杂度:O(1)
法三:分治(执行用时:916ms,内存消耗:22.3MB)
分治法是将整个数组切分成几个小组,然后每个小组再切分成几个更小的小组,一直到不能继续切分为止(也就是只剩一个数字)。然后每个小组计算出最优值,一级级汇报给上一级的小组,上级拿到下级的汇报找到最大值,得到最终结果。
根据示例1,nums = [-2,1,-3,4,-1,2,1,-5,4],数组中共有9个元素,根据mid = (start + end) / 2这个原则,可以得到中间元素的索引为4,也就是-1这个值,那么可以拆分为三个组合:
(1)[-2,1,-3,4,-1]以及它的子序列(在-1左边的并且包含它的为一组)
(2)[2,1,-5,4]以及它的子序列(在-1右边不包含它的为一组)
(3)任何包含-1以及它右边元素2的序列为一组(换言之就是包含左边序列的最右边元素以及右边序列最左边元素的序列,比如 [4,-1,2,1],这样就保证这个组合里面的任何序列都不会和上面两个重复)以上的三个组合内的序列没有任何的重复的部分,而且一起构成所有子序列的全集,那么计算出这三个子集合的最大值即可。
前两个子集合可以通过递归来解决,关键在于第三个跨中心的组合怎么计算最大值:
可以先计算左边序列中包含最右边元素的子序列的最大值,也就是从左边序列的最右边元素向左一个个累加起来,找出累加过程中每次累加的最大值,也就是左边序列的最大值。
同理找出右边序列的最大值,就得到了右边子序列的最大值,左右两边的最大值相加,就是包含这两个元素的子序列的最大值。
那么连续子序列的最大和主要由这三部分子区间里的元素最大和得到,对它们三者求最大值即可:
以上解释来自:
作者:lizhiqiang-3
链接:https://leetcode.cn/problems/maximum-subarray/solution/zheng-li-yi-xia-kan-de-dong-de-da-an-by-lizhiqiang/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
概括一下,分治法的整体思路为:最长的连续序列要么在左半部分,要么在右半部分,要么跨越边界(前提是跨越边界的最大序列一定包括边界两端的左端点和右端点)。
具体做法:
(1)分别求以边界左端点为终点的最大连续序列和以边界右端点为起点的最大连续序列;
(2)再将两个最大连续序列相加就可以得到跨越边界的最大连续序列
分治法有点难理解,可以试着画个图跟着代码走一遍,理一下思路。需要格外注意跨界最大子序和是由中间向两边延伸的,所以其必定至少包含左序列的尾部和右序列的头部。
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
#计算跨界最大子序和
def maxmid(left,right):
ml,mr = -float("inf"),-float("inf")
sum_l ,sum_r = 0,0
#从左序列尾端往前遍历,计算以左序列尾部为起点的最大连续子序和
for i in reversed(left):
sum_l += i
if ml < sum_l:
ml = sum_l
#从右序列头部往后遍历,计算从右序列头部为起点的最大连续子序和
for i in right:
sum_r += i
if mr < sum_r:
mr = sum_r
#左边最大连续序列加上右边最大连续序列就得到跨界最大连续序列
return ml + mr
#计算最大子序和
def maxsum(nums):
if len(nums) == 1:
return nums[0]
#左右片段切分
left = nums[:int(len(nums) / 2)]
right = nums[int(len(nums) / 2):]
#分别计算左边最大子序列和、右边最大子序列和及跨界最大子序和
max_L = maxsum(left)
max_R = maxsum(right)
max_mid = maxmid(left,right)
#返回最大子序列和
return max(max_L,max_R,maxmid)
return maxsum(nums)