Python实现最大子序和:分治与动态规划详解

橘子海
橘子海
发布时间:2026-10-13 | 6人浏览过
原创

本文深入解析Python中求解最大子序和问题的三种核心算法:遍历法、动态规划及分治递归。通过详细推导与代码示例,对比各方法的时间复杂度与空间优化策略,帮助开发者掌握从O(n)到O(nlogn)的不同解题思路,适用于算法面试与性能优化场景。

最大子序和问题定义

给定一个整数数组 nums,需要找到一个具有最大和的连续子数组(该子数组至少包含一个元素),并返回其最大和。

示例展示如下:

输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

进阶挑战:

如果已经实现了复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

思路分析:

首先分析题目,思考为什么最大和的连续子数组只包含特定元素。原因在于,若要在现有基础上扩展当前连续子数组,必须加入相邻元素,而相邻元素可能会减损当前的和。

遍历法实现与证明

思路一:

遍历法,时间复杂度 O(n):

算法过程:遍历数组,使用 onesum 维护当前元素累加的和。当 onesum 出现小于 0 的情况时,将其重置为 0。每次迭代后更新全局最大值。


class Solution:
  def maxSubArray(self, nums):
    """
    :type nums: List[int]
    :rtype: int
    """
    #onesum维护当前的和
    onesum = 0
    maxsum = nums[0]
    for i in range(len(nums)):
      onesum += nums[i]
      maxsum = max(maxsum, onesum)
      #出现onesum<0的情况,就设为0,重新累积和
      if onesum < 0:
        onesum = 0
    return maxsum

算法证明:初始时假设数组为空,将每次选择 nums[i] 加入 onesum 视为当前数组新增一个元素,即以动态的眼光思考。过程简单且代码简短,但其有效性源于加和是按顺序从数组第一个元素开始计算的。

当确定 i 并加入 onesum 后,存在两种情况:

1)假设 onesum 一直大于 0,从未小于等于 0。这意味着每次计算的 onesum 都大于 0,且每次计算的 onesum 都是包括开头元素的一段子序列(尾部随 i 变化)。看似未考虑所有可能序列,但实际上所有可能的序列都已被覆盖。简要说明如下,详见原文:

a) 以当前子序列开头为起点,中间任一处结尾的序列。这种情况一直在扫描并更新保存,因此不会丢失信息。

b) 以当前子序列结尾为终点,中间任一处开头的序列。这种情况的和一定小于以当前子序列开头为起点、结尾为终点的序列。因为前面缺失的那一段经过前提判断,其加和大于 0。

c) 以中间元素为起点和终点的序列。其和小于以当前子序列开头为起点、此分序列结尾为终点的序列。因为前面缺失的那一段经过前提判断,其加和大于 0。

2)出现小于 0 的情况,说明当前形成的子序列首次出现总和小于 0。此时,新的连续数组不能包含之前的整个连续子序列,因为在之前的子序列中累加出了小于 0 的结果。问题是是否需要保留部分?是否所有以当前子序列结尾为终点的任意开头的子序列都要被舍弃?答案是肯定的,因为那一段的和也一定小于 0(见前述证明)。因此,抛弃它们,重新开始新的子序列。

动态规划优化策略

思路二:

动态规划,时间复杂度 O(n)

算法过程:

设 sum[i] 为以第 i 个元素结尾的最大连续子数组的和。假设对于元素 i,所有以其前面元素结尾的子数组长度已求得,那么以第 i 个元素结尾且和最大的连续子数组,要么是以第 i-1 个元素结尾且和最大的连续子数组加上当前元素,要么只包含第 i 个元素。即 sum[i] = max(sum[i-1] + a[i], a[i])。通过判断 sum[i-1] + a[i] 是否大于 a[i] 来做选择,这等价于判断 sum[i-1] 是否大于 0。由于每次运算只需前一次结果,无需像普通动态规划那样保留所有计算结果,只需保留上一次结果,因此算法的时间和空间复杂度都很小。


class Solution:
 
 
  def maxSubArray(self, nums): 
    """ 
    :type nums: List[int] 
    :rtype: int 
    """ 
    length=len(nums) 
    for i in range(1,length): 
      #当前值的大小与前面的值之和比较,若当前值更大,则取当前值,舍弃前面的值之和 
      subMaxSum=max(nums[i]+nums[i-1],nums[i]) 
      nums[i]=subMaxSum#将当前和最大的赋给nums[i],新的nums存储的为和值 
    return max(nums) 

算法证明:代码直接使用了题目数据中的 nums 数组,因为只需遍历一遍。nums[i] 表示以当前第 i 号元素结尾(必须包含当前 i)的最大值,无非是看以 i-1 结尾的最大和子序能否加上 nums[i]。如果 nums[i] > 0,则加上。代码中虽未显式判断,但 Max 表达此意,确定 nums[i] 后继续。

分治递归算法解析

思路三:

分治递归

算法过程:

分治法中,最大子序和要么在左半部分,要么在右半部分,要么横跨两部分(即包括左半部分的最后一个元素和右半部分的第一个元素)。返回这三种情况的最大值。第三种情况中,包含左半部分最后一个元素的情形需挨个往前遍历更新最大值;包含右半部分第一个元素的情形类似。总的时间复杂度为 O(nlogn)。


class Solution(object):
  def maxSubArray(self, nums):
    #主函数
    left = 0
    #左右边界
    right = len(nums)-1
    #求最大和
    maxSum = self.divide(nums,left,right)
    return maxSum
    
  def divide(self,nums,left,right):
    #如果只有一个元素就返回
    if left==right:
      return nums[left]
    #确立中心点
    center = (left+right)//2
    #求子序在中心点左边的和
    leftMaxSum = self.divide(nums,left,center)
    #求子序在中心点右边的和
    rightMaxSum = self.divide(nums,center+1,right)
    
    #求子序横跨2边的和,分成左边界和和右边界和
    leftBorderSum = nums[center]
    leftSum = nums[center]
    for i in range(center-1,left-1,-1):
      leftSum += nums[i]
      if leftSum>leftBorderSum:
        #不断更新左区块的最大值
        leftBorderSum = leftSum
      
    rightBorderSum = nums[center+1]
    rightSum = nums[center+1]
    for i in range(center+2,right+1):
      rightSum += nums[i]
      if rightSum>rightBorderSum:
        #不断更新右区块的最大值
        rightBorderSum = rightSum
    #左边界的和 + 右边那块的和
    BorderSum = leftBorderSum + rightBorderSum
    return max(leftMaxSum,rightMaxSum,BorderSum)

算法证明:

整体思路巧妙,不断切割数组,将一块数组视为左、中、右三个部分。这类似于枚举,但利用二分思路优化了大量计算。因此枚举能达到目标,因为不断计算以一定包括中间节点的子序的最大和。

算法总结与启示

总结:

本文内容较多,虽无时间复习,但收获颇丰。例如动态规划不一定非要建立数组并返回最后一项,动态规划其实很灵活,但需明确每一项代表的意义。

分治递归实际在帮我们计算所有数组,只不过用了二分思路进行优化。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表