LeetCode算法题3:求最大子序列和


前言

      本文简单介绍递归的使用(依次打印出一个 int 整数的每一位)和求最大子序列的方法(以 int 数组为例)。

一、递归

      递归的基本法则:
      1,基准情形:能够直接解出结果的情形。
      2,不断推进:保证每一次递归调用能向基准情形推进。
      3,设计法则:所有的递归调用都能运行。(一般不考虑这点)
      4,合成效益法则:在求解一个问题时,切忌在不同的递归调用中做重复性的工作。比如:求斐波那契数列中的: f(n)=f(n-1)+f(n-2)。 f(n-2) 被单独计算一次,但是当计算 f(n-1) 时,f(n-2) 又被计算一次,以此类推,程序做了大量重复性的工作。

      递归示例,依次打印出一个整数的每一位:

public final class HelloWorld {
    
    
    public static void print(int n){
    
    
        if(n>=10)
            print(n/10);
        System.out.println(n%10);
    }
    public static void main(String[] args){
    
    
        int n=12345678;
        if(n>=0&&n<=Integer.MAX_VALUE)
            print(n);
    }
}

二、求最大子序列和

1,最朴素的解法

      其思想为:将一个序列的所有可能的子序列和都计算一遍,取最大值。最外层循环 i 用来标记待计算序列的起始下标,j 用来标记待计算序列的结束下标,k 用来遍历待计算序列,得到其和,并且保留最大的序列和值。

      该解法的时间复杂度为O(n3)。

public static int findMaxSeries_1(int[] n){
    
    
        int re=Integer.MIN_VALUE,tmp;
        for(int i=0;i<n.length;i++)
            for(int j=i;j<n.length;j++) {
    
    
                tmp=0;
                for(int k=i;k<j+1;k++)
                    tmp+=n[k];
                if(re<tmp)
                    re=tmp;
            }
        return re;
    }

2,较朴素的解法(更进一步)

      上面代码中在 j 循环中,可以在 j 每次变化时计算新的子序列和并判断大小,从而可以省掉不必要的最内层循环:

      该解法的时间复杂度为O(n2)。

public static int findMaxSeries_2(int[] n){
    
    
        int re=Integer.MIN_VALUE,tmp;
        for(int i=0;i<n.length;i++){
    
    
            tmp=0;
            for(int j=i;j<n.length;j++) {
    
    
                tmp+=n[j];
                if(re<tmp)
                    re=tmp;
            }
        }
        return re;
    }

3,分治和递归

      试一下时间复杂度为O(nlogn)的解法。采用分治的思想,那需要将原序列分为两个子序列来计算(递归进行分配,直到设定的某个约束条件),原序列的最大子序列和有三种情况:1,当最大子序列在左边序列时:它的值为左边序列中的最大子序列和值;2,当最大子序列在右边序列时:它的值为右边序列中的最大子序列和值;3,当最大子序列横跨左右序列时:此时需要,从左右两边的分界线处开始计算,分别求左边和右边序列中的最大值(时间复杂度为 O(n)),然后左右相加便得到结果。

      具体地见下面代码:

    public static int findMaxSeries_3(int[] n){
    
    
        return findMaxSeries_3_1(n,0,n.length-1);
    }
    public static int findMaxSeries_3_1(int[] n, int left, int right){
    
    
        if(left==right)//当序列只有一个值的时候,直接返回。
           return n[left];

        int mid=(left+right)/2;
        int maxLeftSum= findMaxSeries_3_1(n,left,mid);
        int maxRightSum= findMaxSeries_3_1(n,mid+1,right);

        int maxLeftBorderSum=Integer.MIN_VALUE,leftBorderSum=0;
        for(int i=mid;i>=left;i--)
        {
    
    
            leftBorderSum+=n[i];
            if(leftBorderSum>maxLeftBorderSum)
                maxLeftBorderSum=leftBorderSum;//求左边序列的最大值(从元素 n[center]开始)
        }
        int maxRightBorderSum=Integer.MIN_VALUE,rightBorderSum=0;
        for(int i=mid+1;i<=right;i++)
        {
    
    
            rightBorderSum+=n[i];
            if(rightBorderSum>maxRightBorderSum)
                maxRightBorderSum=rightBorderSum;//求左边序列的最大值(从元素 n[center+1])开始)
        }
        
        return maxLeftSum>maxRightSum? maxLeftSum: Math.max(maxRightSum, (maxLeftBorderSum + maxRightBorderSum));
    }

4,精妙的解法

      和上一个解法中求左右序列最大值的方法类似,不过需要带点儿变化。

      1,在一次遍历中,刚开始保存一个元素组成的序列和,即它本身。
      2,当子序列长度加一时,此时子序列中有两个元素,判断此时的序列和与之前值的大小关系,若比之前值大,认为增加的元素产生了积极因素,保存此时的序列和。若比之前值小,那表明此时增加了一个元素之后的结果更坏,而这样一个更坏的序列和值就不需要了,这时保存的值为序列中第一个元素的值;(注意:此时前两个元素已经处理过了,并且永远不会在使用它们了)
      3,然后重新计算仅由第三个元素组成的序列,判断它和之前保存的值的大小关系,执行步骤 2。
      依次类推。

      该解法的时间复杂度为O(n)。

    public static int findMaxSeries_4(int[] n){
    
    
        int max=Integer.MIN_VALUE,tmp=0;
        for(int i=0;i<n.length;i++){
    
    
            tmp+=n[i];
            if(tmp>max)
                max=tmp;
            else
                tmp=0;
        }
        return max;
    }

      该解法存在一个不太明显的缺陷:如果一个序列全为负数时,在某些情况下比如:int[] n={-5,-6,-3,-1,-5}; 它无法得到正确结果 -1。
      大致规律为:最大负值在奇数下标时(上例 -1 所在下标为 3),无法得到正确结果。


总结

      完,如有问题后续再更改。

猜你喜欢

转载自blog.csdn.net/Little_ant_/article/details/121572466