程序员面试精粹02

求子数组最大和的程序


题目:输入一个整型数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。

            求所有子数组的和的最大值。要求时间复杂度为O(n)。


            举例来说:输入的数组为 1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,因此输出为该子数组的和18。

             ==================  1, -2, 3, 10, -4, 7,2, -5,和最大的子数组为3, 10, -4, 7, 2    =====================  


一.题目分析


    我们先不管算法神马的,先从最一般的的思考方式去分析这道问题。一串整数数组,有正有负,如何从中选出和最大的子数组呢?

    1.这串子数组一定是从一个正数开始;

    2.它一定也以一个正数作为最后一个元素结尾。

   

    为什么呢?举例来说:1, -2, 3, 10, -4, 7, 2, -5 。如果一个子数组以一个负数开始,那么这个子数组一定小于这个子数组中以正数开头的子数组... ...晕了是吧?

    这里我们用反证法来证明一下~

   

    假设一个数组的最大子数组是以一个负数开头的,即A = { -2, 3, 10, -4, 7, 2 }是最大的子数组,那么A的子数组B = { 3, 10, -4, 7, 2 }的和是一定大于A的。因为A = -2 + B;所以

B > A,而我们假设A是最大的子数组。显然结论与我们假设的相矛盾(B竟然更大~)。由此我们知道这串子数组一定是从一个正数开始。同理我们就可以得出这个子数组一定也以一个正数作为最后一个元素结尾的。


    那么这个结论给我们什么启示?难不成要让我们去找到所有的以正数开头结尾的子数组,比较他们的和?当然可以,只不过O(n)这个题设要求就不满足了。为什么呢,还是看看这个例子:1, -2, 3, 10, -4, 7, 2, -5

我们首先从头开始遍历:我们首先找到第一个正数,也就是1。

====1, -2, 3

====1, -2, 3, 10

====1, -2, 3, 10, -4, 7

====1, -2, 3, 10, -4, 7, 2

这是第一遍,总共经过比较了n个数,

那么第二遍我们继续向前找以正数开始,正数结尾的子数组... ...以此类推。你会发现最坏的情况下要有n*(n - 1)* ... *1 即 n!次才能全部找到(此时原数组全是正数)。最乐观的情况是{  1,-1, -1, ...  ,-1  }这种情况只要遍历一次就可以了,但前提是你得有一个记录的标记来告诉你有n个数,(n - 1)个负数才行。显然这种情况只是理想情况。我们在分析为题的时候要以一般情况来分析。算法设计分析中美其名曰:平均时间复杂度~


    所以这里我们就引出了一个叫做" 贪心算法 "的东西。所以大家想要扩展知识的话可以去学习一下" 贪心算法 "的相关知识。为什么这道问题要用贪心算法来解决呢?因为许多可以用贪心算法求解的问题中看到这类问题一般具有2个重要的性质:贪心选择性质最优子结构性质

    擦~什么意思... ...下面我们先用书本方法解释一下

1.贪心选择性质

    所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
    对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。

2.最优子结构性质

    当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。

    贪心算法的基本思路如下:

    1.建立数学模型来描述问题。

    2.把求解的问题分成若干个子问题。

    3.对每一子问题求解,得到子问题的局部最优解。

    4.把子问题的解局部最优解合成原来解问题的一个解。


    所以,根据上面的介绍我们想找到和最大子数组,那就从头开始找这个子数组。我们就从第一个数开始( 1, -2, 3, 10, -4, 7, 2, -5 ) ,此时要假设子数组的和sum开始为0,并且我们能确保最后的子数组一定大于0(因为这个数组中有正有负,大不了最后的子数组就是一个数,他是整个数组中的最大正数罢了)。所以当前面的和相加小于0的时候,立刻抛弃。因为根据贪心选择性,我们认为如果前面的和小于0,那还不如跳过它重新开始。想想这里的证明是不是和前面的那个一定要以正数开头的证明是一样的呢,所以前面的那个证明正式一种贪心选择性的表现。如果前面的和大于0,假设新加进来的数大于0,那么新组成的组就是更新后的最优的解!!

    如果还是有点思路混乱,让我们在代码中继续深入理解~

    程序如下:

int max_sub_array(int arr[], int size)
{
	int i 	= 0;			//一个记录原数组的游标
	int sum = 0;			//sum来记录前k个数的和(k < n)
	int max = -(1 << 30);		//max记录我们找到的最大和,开始我们设置为-2^31
	
	//循环到size位置,我们可以寻找子数组的子数组,即假设 { 1,-2, 3, 10, -4, 7, 2 }
	//当我们设size = 4,那么其实测试的数组就是{ 1, -2, 3, 10 }。
	while ( i < size )			
	{
		//这就是我们前面提到的,sum一直向后加arr[i],当sum < 0时,我们抛弃sum,就是跳过前k个数,因为
		//前k个数不是最优的子结构,我们就将sum置0,从新开始。
		sum += arr[i++];
		
		if ( sum < 0 )
		{
			sum = 0;
		}
		
		//当sum加上一个正数的时候(sum新) > (sum原),而max就是(sum原)。于是我们更新max。
		//当sum加上一个负数的时候(sum新) < (sum原),max不更新。这就是为什么最后一个数
		//一定是大于0的了
		else if ( sum > max )
		{
			max = sum;
		}

	}
	
	return max;
}

测试函数如下:

int main()
{
	int arr[] = {-1,3,5,-2, 7, 10, 3, -1};
	int a = 0;
	int b = 0;
	
	a = max_sub_array(arr, 5);
	b = max_sub_array(arr, 8);
	
	printf( "%d\n", a );
	printf( "%d\n", b );
	
	return 0;
}


测试结果如下:


在理解的前提下,自己动手试试~


二.就这些么?


    这就完事了么?是的。对于题目已经完事了~但是我们还可以想得更多!

    请问,我们找到了最大的和max,但是这个子数组在哪里?A = { 1, -2, 3, 10, -4, 7, 2, -5} ,的子数组是从哪里开始的?她的起始位置在哪。

   

    为了解决这个问题我们,我们依然是跟着代码来看看它都做了什么。

int max_starter(int arr[], int count)
{
	//发现这3个和之前没啥区别
	int i = 0;
	int max = -(1 << 30);
	int sum = 0;
	
	//那这两个又是什么意思呢?
	//starter_final:是最大子组起始位置;额... 那个starter呢?
	//这个starter是记录临时的起始值,说的挺别扭的,要不看看例子? 对于int arr[] = {-1,3,5,-12, 7, 10, 3, -1}
	//第一个starter是2,即arr[1] = 3那里,因为(arr[0] = -1) < 0,则sum < 0,直接就跳到arr[1]那里从新开始了,所	
	//以我们看看这个starter都有哪些? arr = 3, arr = 7这里,是吧?自己动手算算。也就是说starter记录着所有可能的
	//开始位置,而这个starter_final通过比较记录着存在和最大的子数组的起始位置。
	int starter = 0;
	int starter_final = 0;
	
	while ( i < count )
	{
		sum += arr[i++];
		if ( sum < 0 )
		{
			starter = i;
			sum = 0;
		}	
		else if ( sum > max )
		{
			max = sum;
			starter_final = starter;
		}		
	}
	
	//我们返回的是位置,需要加1,即arr[4] = 7,的位置在数组的第5位。
	return (starter_final + 1);

}

测试结果如下:


在理解的前提下,自己动手试试~


三.More, I need more power(鬼泣DMC中,堕落维吉尔的台词)


    是的,我们还可以做的更多!

    现在知道了和最大的子数组了,又知道了子数组在原数组的起始位置。那么这个子数组长的什么样呢,我们能不能知道她就是 { 3, 10, -4, 7, 2 }呢。

   

    其实很简单啦,我们知道了数组的和,和起始位置。那么用总和减去起始位置那个元素,即 18 - 3 = 15,同时将那个指向起始位置的标志向后移一下,再用一个count来记录子数组有多少个元素。又是说的好乱,所以依然是例子来说话:这里 max = 18,count = 0是吧,

max = max - 3 = 15,count++ (所以count = 1了)

max = max - 10 = 5,count++  (count = 2)

max = max - (-4) = 9,count++  (count = 3)

max = max - 7 = 2,count++  (count = 4)

max = max - 2 = 0,count++  (count = 5)


    至此count计算完毕,共5个数在这个子数组里。知道了子数组在原数组中的位置,又知道了她的长度~那么就能确定她到底是什么样的了,不是么?

    我不说话,就看看代码~

//子组长度
int max_sub_array_length(int arr[], int count)
{
	int max_value	= 0;
	int starter 	= 0;
	int length 		= 0;
	
	max_value 	= max_sub_array(arr, count);
	starter 	= max_starter(arr, count) - 1;
	
	while (max_value > 0)
	{
		max_value -= arr[starter++];
		++length;
	}
	
	return length;
}

    至于怎么把它打印出来就交给大家来完成了,动手试试!!!

    最后把全部示例代码列出来:

/*************************************************
 *
 *作者:钟凌霄
 *时间:2014.1.6
 *题目:求子数组的最大和问题
 *
 *************************************************/

#include <stdlib.h>
#include <stdio.h>

//*******************子数组最大和**************************
int max_sub_array(int arr[], int size)
{
	int i 	= 0;			//一个记录原数组的游标
	int sum = 0;			//sum来记录前k个数的和(k < n)
	int max = -(1 << 30);		//max记录我们找到的最大和,开始我们设置为-2^31
	
	//循环到size位置,我们可以寻找子数组的子数组,即假设 { 1,-2, 3, 10, -4, 7, 2 }
	//当我们设size = 4,那么其实测试的数组就是{ 1, -2, 3, 10 }。
	while ( i < size )			
	{
		//这就是我们前面提到的,sum一直向后加arr[i],当sum < 0时,我们抛弃sum,就是跳过前k个数,因为
		//前k个数不是最优的子结构,我们就将sum置0,从新开始。
		sum += arr[i++];
		
		if ( sum < 0 )
		{
			sum = 0;
		}
		
		//当sum加上一个正数的时候(sum新) > (sum原),而max就是(sum原)。于是我们更新max。
		//当sum加上一个负数的时候(sum新) < (sum原),max不更新。这就是为什么最后一个数
		//一定是大于0的了
		else if ( sum > max )
		{
			max = sum;
		}
	}
	
	return max;
}


//*******************子数组起始位置*************************
int max_starter(int arr[], int count)
{
	//发现这3个和之前没啥区别
	int i 	= 0;
	int max = -(1 << 30);
	int sum = 0;
	
	//那这两个又是什么意思呢?
	//starter_final:是最大子组起始位置;额... 那个starter呢?
	//这个starter是记录临时的起始值,说的挺别扭的,要不看看例子? 对于int arr[] = {-1,3,5,-12, 7, 10, 3, -1}
	//第一个starter是2,即arr[1] = 3那里,因为(arr[0] = -1) < 0,则sum < 0,直接就跳到arr[1]那里从新开始了,所	
	//以我们看看这个starter都有哪些? a = 3, a = 7这两部分,是吧?自己动手算算。也就是说starter记录着所有可能的
	//开始位置,而这个starter_final通过比较记录着存在和最大的子数组的起始位置。
	int starter = 0;
	int starter_final = 0;
	
	while ( i < count )
	{
		sum += arr[i++];
		if ( sum < 0 )
		{
			starter = i;
			sum = 0;
		}	
		else if ( sum > max )
		{
			max = sum;
			starter_final = starter;
		}		
	}
	
	//我们返回的是位置,需要加1,即arr[4] = 7,的位置在数组的第5位。
	return (starter_final + 1);

}


//*********************子数组长度************************
int max_sub_array_length(int arr[], int count)
{
	int max_value	= 0;
	int starter 	= 0;
	int length 		= 0;
	
	max_value 	= max_sub_array(arr, count);
	starter 	= max_starter(arr, count) - 1;
	
	while (max_value > 0)
	{
		max_value -= arr[starter++];
		++length;
	}
	return length;
}


//**********************全部测试用例***********************
int main()
{
	int arr[] = {-1,3,5,-12, 7, 10, 3, -1};
	int a = 0;
	int b = 0;
	int c = 0;
	
	a = max_sub_array(arr, 6);
	b = max_starter(arr, 8);
	c = max_sub_array_length(arr, 5);

	printf( "子数组最大和:%d\n", a );
	printf( "子数组起始位置:%d\n", b );
	printf( "子数组长度:%d\n", c );
	
	return 0;
}

    这里我们并没有将该代码完善化,如果要问的话,那当然是留给大家的作业了~

    当我们取a = 1的时候是以个负数,那么该程序就会告诉你最大和是 -1073741824(哪来的?怎么解决)。还有一些问题就让大家去思考发现,并独立去解决了~恩,加油!  


    至此我们已经基本上把这道题弄明白了,如果有什么问题的话欢迎指正和留言。   



评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值