《算法导论》4.1主要介绍了处理“最大子数组问题”的思路。
“最大子数组问题”是指,给定一个有正数负数(正数至少要有一个吧。。)的数组A,找出A的一个连续的子数组,使得这个子数组内部数字之和在A的所有连续子数组中最大。
例如,对于数组:
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
来说,它的最大子数组就是:
[100,1]
算法导论第四章中,针对这个问题给出了3个解决方法:在正文中的归并方法,在练习4.1-2,3中的暴力方法,以及4.1-5中的线性时间方法。下面就用python来实现这三种方法:
1,暴力强拆法:
# 最大子序列的暴力算法 def max_subarray_brute_force(array1): '目前找到的最大子序列 array[left,right+1],注意right+1....丑陋的python' array = array1[:] # 复制 max_sum = -1.0 * MAX_NUM # 目前已经找出的最大子序列的和,MAX_NUM是一个指定的很大的数 left = 0 # 最大子序列的左侧起点 right = 0 # 最大子序列的右侧终点 #寻找最大子序列 for i in range(len(array)): # i是子序列的左侧的起点 sum_ij = 0 for j in range(i,len(array)): # j是子序列的右侧的终点,逐步从i向数组结尾扩散 sum_ij += array[j] if sum_ij > max_sum: # 找到了一个总和大于原来的max_sum的子序列 left = i right = j max_sum = sum_ij #输出结果 print( "最大子序列为:array[%d:%d+1]="%(left,right), array[left:right+1], end='\t' ) print( "sum=%f"%sum(array[left:right+1]) )
2,算法导论介绍的归并方法:
对于列表array,它的中点为mid,则最大子序列有3种情况:全部在mid左边,全部在mid右边,包含了mid。
对于前两种情形,可以递归调用这个问题的算法,只不过被处理的数组为array[0,mid]和array[mid+1,len(array)]
对于最后一种情况,以mid为右端点向左搜索最大的子序列,再以mid为左端点向右搜索最大的子序列,合并起来就是结果。
因此首先找一个函数用于搜索array1[left]到array1[right]内的最大子序列。约定:这里left,right都取闭区间,与python的左闭右开不同。代码为:
# 真正的归并算法函数 def inner_max_subarray_merge_method(array1,sub_left,sub_right): ''' 这个函数寻找array1[sub_left],...,array1[sub_right]内最长的子序列,两端都是闭区间。 这个函数的返回值为[left,right,sum],表示array1[left],...,array1[right]为找到的子序列,和为sum ''' # 归并到头,就返回子序列的和为 -1.0 * MAX_NUM ,不会有东西比它小了 if ( sub_left > sub_right ): return [ -1,- 1, -1.0 * MAX_NUM ] # 中点 sub_mid = ( sub_right + sub_left ) // 2 # 归并过程 result_left = inner_max_subarray_merge_method(array1,sub_left,sub_mid - 1) # sub_mid以左最大的子序列 result_right = inner_max_subarray_merge_method(array1,sub_mid + 1,sub_right) # sub_mid以右最大的子序列 result_mid = find_max_crossing_subarray(array1,sub_left,sub_mid,sub_right) # 包含了sub_mid的最大子序列 # 在三个结果中选择和最大的 if ( result_left[2] >= result_right[2]) and (result_left[2] >= result_mid[2]): return result_left elif (result_right[2] > result_left[2]) and (result_right[2] > result_mid[2]): return result_right else : return result_mid找到包含了sub_mid的最大子序列的方法是:
#粘合过程:找出经过sub_mid点的所有子序列中最大的那个 def find_max_crossing_subarray(array,sub_left,sub_mid,sub_right): '''在array1[sub_left],...,array[sub_mid],...,array1[sub_right]中找到包含了rray[sub_mid]的最大子序列, 这个函数的返回值为[left,right,sum],表示array[left],...,array[right]为找到的子序列,和为sum''' #往sub_mid左边搜索 left_max_sum = 0 left = sub_mid left_sum = 0 for i in range(sub_mid-1,sub_left-1,-1): left_sum += array[i] if left_sum > left_max_sum: left = i left_max_sum = left_sum #往sub_mid右边搜索 right_max_sum = 0 right = sub_mid right_sum = 0 for i in range(sub_mid+1,sub_right+1,1): right_sum += array[i] if right_sum > right_max_sum: right = i right_max_sum = right_sum #找出左右两边最大的子序列,粘起来就是经过sub_mid的最大子序列 return [ left, right, left_max_sum + right_max_sum + array[sub_mid] ]
随后调用这两个函数就可以了:
# 调用函数 result = inner_max_subarray_merge_method(array1,0,len(array1)-1) left = result[0] right = result[1] #输出结果 print( "最大子序列为:array[%d:%d+1]="%(left,right), array[left:right+1], end='\t' ) print( "sum=%f"%sum(array[left:right+1]) )
3,线性时间算法:
仔细考虑人找到最大子序列的算法。先看两个命题:
命题1:最大子序列array[left],...,array[right]的开头(也就是array[left])一定是正数,否则去掉头这个序列就可以更大。最大子序列的末尾(也就是array[right])一定是正数,否则掐掉尾这个序列就可以更大。
命题2:上面的结论可以进一步扩展:最大子序列的包括了开头的任何一个子序列(也就是array[left],...array[中间某处])的都和必须大于0,否则把这段子序列去掉,就能得到更大的子序列。
首先,我们需要记录下当前找到的最大的连续子序列。然后进一步尝试其它子序列,每尝试一个新的子序列,看它们的和是否能超过这个最大子序列,如果超过则把它当作最大子序列,否则最大子序列不变。
当我们暂时找到了一个有潜力成为最大子序列的子序列、并将其与已经找出过的最大子序列比较过之后,开始观察它的尾巴的右边的数字。如果是一个正数,那么这个数就要加入子序列,他会让这个子序列更大,这一过程称之为“扩充已有子序列”。如果是一个负数,那么我们就应该考虑到底是把它加进这个子序列里也即继续“扩充已有子序列”,还是不把它加进去然后在它右边找到一个正数重新开始搜索一个子序列。
对于正数情况,“扩充已有子序列之后”,就需要和已经发现的最大子序列进行比较,取两者中序列和最大的那个作为最大子序列。
对于负数情况,我们可以试探性地把这个负数加入,即试探性地“扩充已有子序列”。然后看这个子序列的和是否仍然大于0,如果小于0,则这个序列就应该被丢弃,我们需要重新找一个正数开始搜素子序列,否则,还可以继续试探性地将右边的数加入,即“扩充已有子序列”。每次扩充已有子序列之后都应该和已经发现的最大子序列进行比较。
例如,对于上面的:
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
从array[2]开始搜索
①正在尝试的子序列:array[2] 。这一步之后最大子序列:array[2] 。
②正在尝试的子序列:array[2],array[3] 。
这一步之后最大子序列:array[2],array[3] 。
。。。
③正在尝试的子序列:array[2],array[3],array[4],array[5] 。最大子序列:array[2],array[3],array[4],array[5]
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
④正在尝试的子序列:array[2],array[3],array[4],array[5],array[6] = [1,1,1,1,-1]。
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
这一步新添加的是一个负数,需要引起注意了!!
最大子序列:array[2],array[3],array[4],array[5]
⑤正在尝试的子序列:array[2],....,array[7]=[1,1,1,1,-1,-1]。
最大子序列:array[2],....,array[5]
。。。
⑥正在尝试的子序列:array[2],....,array[10]=[1,1,1,1,-1,-1,-1,-1,-1]。
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
这时子序列之和小于0了,该重新寻找一个子序列,而不是继续扩充已有子序列。
最大子序列:array[2],....,array[5]
⑦正在尝试的子序列:array[13] =[8] 比原来的最大子序列大,所以把它当作新的最大子序列
最大子序列:array[13]
⑧正在尝试的子序列:array[13],array[14] =[8,-10] 小于0了,毙掉
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
最大子序列:array[13]
⑨正在尝试的子序列:array[17]=[100] 比原来的最大子序列大,所以把它当作新的最大子序列
最大子序列:array[17]
⑩正在尝试的子序列:array[17],array[18]=[100,1] 比原来的最大子序列大,所以把它当作新的最大子序列
array=[-1,-1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,8,-10,-1,-1,100,1]
最大子序列:array[17:18+1]
以上过程可以看出:数组内所有的数只需要遍历一次,且每个数被访问到时都是作为子序列的右端进行访问的,因此以子序列的右端作为循环变量。
代码为:
# 4.1-5 最大子序列的线性复杂度算法 def max_subarray_linear_time(array1): '''这个函数寻找array1内最大的子序列,函数的返回值为[left,right,sum], 表示array1[left],...,array1[right]为找到的子序列,和为sum''' #复制 array = array1[:] #目前找到的最大子序列 array[left,right+1],注意right+1。。丑陋的python max_sum = -1.0 * MAX_NUM # 最大子序列的和 left = 0 # 最大子序列的左侧 right = 0 # 最大子序列的右侧 #目前正在尝试的子序列 array[possible_left,possible_right+1] possible_max_sum = 0 #正在尝试的序列的和 possible_right = 0 #左 possible_left = 0 #右 #True:当前应该重新尝试一个新的子序列 ,False:当前应该继续试图扩充一个子序列 new_sub_array = True #寻找最大子序列,以possible_right为循环的变量 for possible_right in range(0,len(array)): #如果是应该尝试一个新的子序列,而不是扩充一个可能的子序列 if ( new_sub_array == True ): #作为一个新的子序列,第一个数必定应该是正的,所以应该跳过非正的数 if ( array[possible_right] <= 0): #还没找到一个可以作为最大子序列的开头的正数时: possible_max_sum = 0 possible_left = possible_right + 1 else: #找到了一个大于0的数,将其作为子序列的第一个数,就可以开始扩充它了 possible_left = possible_right * 1 possible_max_sum = 0 new_sub_array = False #标记改为扩充一个可能的子序列 #找到一个子序列之后,就可以扩充了。被扩充的子序列有两种情况: #情况1:上一个if中新尝试的子序列,情况2:上一遍循环仍在尝试的这个子序列 #先看情况2:上一遍循环仍在尝试的这个子序列 if ( new_sub_array == False ): possible_max_sum += array[possible_right] #possible_max_sum为当前尝试子序列的序列和 if ( possible_max_sum > max_sum ): #如果正在尝试的子序列的和大于max_sum,就拿这个作为最大子序列 left = possible_left right = possible_right max_sum = possible_max_sum elif ( possible_max_sum <= 0): #如果正在尝试的子序列的和小于0了,就说明当前的子序列不具有继续扩充的意义了 new_sub_array = True #重新开始尝试一个子序列 possible_max_sum = 0 #正在尝试的子序列的和清零 possible_left = possible_right + 1 #重新尝试 #返回找到的子序列 print( "最大子序列为:array[%d:%d+1]="%(left,right), array[left:right+1], end='\t' ) print( "sum=%f"%sum(array[left:right+1]) )