最大子数组问题三种算法的Java代码实现——暴力求解、分而治之、线性方法

这是《算法导论》书中给的一个例子。下图给出了17天的股票价格。第0天的价格是100美元,你可以在此后任何时刻买进股票。人们都希望“低价买进,高价卖出”,并最大化收益。这里就是要求解在哪一天买进,哪一天卖出,可以获得的收益最大。

 

本文中,给出了暴力求解和分治法两种求解的方法。暴力求解方法就是尝试多有可能的买进卖出组合,找出收益最大的组合。

在分而治之的方法中,我们将原问题进行变换。即寻找一个日期段,在这个日期段的第一天买进,在这个日期段的最后一天卖出,这时获益最大。这样,我们要关心的就不再是每天股票的价格了,而是每天相对于前一天的价格变化量,如下图中的数组所示。

对于数组A[low…high],使用分治方法,意味着我们要将子数组划分为两个规模尽量相等的子数组。也就是说,找到数组的中央位置,比如mid。然后考虑两个子数组A[low…mid]和A[mid+1…high]。在A[low…high]中,任何连续子数组只有三种情况:

(1)      完全在左子数组A[low…mid]中;

(2)      完全在右子数组A[mid+1…high]中;

(3)      跨越了中点,一部分在A[low…mid]中,另一部分在A[mid+1…high]中。

因而,我们对于一个数组,可以在这三个情况中分别求解,选取其中最大一个。

用Java实现的暴力求解法和分而治之的方法代码如下所示。

/**
 * 最大子数组问题
 * 分别用暴力法和分治法求解
 * @author sdu20
 *
 */
public class MaxSubarray {
	
	public static final int NEG_INF = -10000;//当做负无穷使用

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] shares = {100,113,110,85,105,102,86,63,81,101,94,106,101,79,94,90,97};
		
		Result result1 = Brutefoce(shares);
		System.out.println("暴力求解结果:");
		System.out.println(result1.toString());
		
		Result result2 = Divide(shares);
		System.out.println("分而治之求解结果:");
		System.out.println(result2.toString());
	}
	
	/**
	 * 暴力求解法
	 * @param array
	 * @return
	 */
	public static Result Brutefoce(int[] array){
		int in = 0;
		int out = 1;
		int profit = array[1]-array[0];
		for(int i = 0;i<array.length;i++){
			for(int j = i+1;j<array.length;j++){
				if(array[j]-array[i]>profit){
					in = i;
					out = j;
					profit = array[j]-array[i];
				}
			}
		}
		return (new Result(in,out,profit));
	}
	
	/**
	 * 分而治之求解
	 * @param array
	 * @return
	 */
	public static Result Divide(int[] array){
		int[] a2 = new int[array.length];
		a2[0] = 0;
		for(int i = 1;i<a2.length;i++){
			a2[i] = array[i]-array[i-1];
		}
		Result result = Divide_Result(a2,1,a2.length-1);
		result.left = result.left-1;//因为a2数组中存储的是价格比昨天的变化,因而买入的日期应该加一,同理上一行中的左索引为1而不是0
		return result;
	}
	
	/**
	 * 分而治之的方法求解最大子数组
	 * @param array 
	 * @param left 左索引
	 * @param right 右索引
	 * @return
	 */
	public static Result Divide_Result(int[] array,int left,int right){
		if(left==right){
			return (new Result(left,right,array[left]));
		}
		int mid = (left+right)/2;
		Result left_result = Divide_Result(array,left,mid);
		Result right_result = Divide_Result(array,mid+1,right);
		Result mid_result = FIND_MAX_CROSSING_SUBARRAY(array,left,mid,right);
		if(left_result.sum>=right_result.sum && left_result.sum>=mid_result.sum){
			return left_result;
		}
		if(right_result.sum>=left_result.sum && right_result.sum>=mid_result.sum){
			return right_result;
		}
		return mid_result;
	}
	
	/**
	 * 分治法
	 * 当子数组跨越中点时
	 * @param array 数组
	 * @param left 左边起始索引
	 * @param mid 中点
	 * @param right 右边终止索引
	 * @return
	 */
	public static Result FIND_MAX_CROSSING_SUBARRAY(int[] array,int left,int mid,int right){
		int left_sum = NEG_INF;
		int right_sum = NEG_INF;
		int max_left_index = mid;
		int max_right_index = mid+1;
		int sum = 0;
		for(int i = mid;i>=left;i--){
			sum += array[i];
			if(sum>left_sum){
				left_sum = sum;
				max_left_index = i;
			}
		}
		sum = 0;
		for(int i = mid+1;i<=right;i++){
			sum += array[i];
			if(sum>right_sum){
				right_sum = sum;
				max_right_index = i;
			}
		}
		
		return (new Result(max_left_index,max_right_index,left_sum+right_sum));
	}
	
}

/**
 * 用于存储结果的数据结构
 * @author sdu20
 *
 */
class Result{
	
	int left; //左索引
	int right;  //右索引
	int sum;  //和
	
	public Result(int l,int r,int s){
		this.left = l;
		this.right = r;
		this.sum = s;
	}
	
	public String toString(){
		return "left index: "+this.left+"  right index: "+this.right+"  profit:"+this.sum;
	}
}


运行结果如下所示

在书中练习4.1-5题中提到解决最大子数组问题还存在线性时间复杂度的算法。题目中提到了根据提到的已知A[i..j]的最大子数组,去计算A[i..j+1]的最大子数组的思路,但是这种方法只是在根据A[i..j]的最大子数组,去计算A[i..j+1]的最大子数组的过程的时间复杂度是线性的,但是总的时间复杂度仍然很高,并不比分治方法好。

我们可以考虑另一种思路。对于一个数组的最大子数组,在最大子数组不仅仅有一个元素的情况下,任意的将这个最大子数组切分成前后两个部分,这两部分各自的和必然是正的,也就是任意一半的和肯定小于总的和。根据这一点我们可以有如下的线性时间复杂度的算法代码实现:

public static void linearAlgorithm(int[] array){
		int templeft = 0;
		int tempright = 0;
		int left = 0;
		int right = 0;
		int sum = array[0];
		int maxsum = array[0];
		for(int i = 1;i<array.length;i++){
			if(sum+array[i]>array[i]){
				sum = sum+array[i];
				tempright++;
			}else{
				sum = array[i];
				templeft = i;
				tempright = i;
			}
			
			if(maxsum<sum){
				maxsum = sum;
				left = templeft;
				right = tempright;
			}
		}
		
		System.out.println("left index: "+left+"\t"+"right index: "+right+"\tprofit: "+maxsum);
	}

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值