【面试之旅—算法与数据结构-1】 最大最长子序列

    最近找实习面试、笔试不断,感觉自己的算法真心渣,赶快买几本书弥补一下。
    书中许多算法是非常经典的, 并且对我的意义也特别大,所以我把这部分的东西放在博客上,算作纪念。

最大最长子序列

渊源

我上大一时,第一次碰到关于时间效率的问题就是这个了。我当时根本不会做,只能百度来谷歌去,结果搜到一堆神一样的实现,没有注释,指针、下标倒是用的掉渣天,完全是Show自己的C语言、C++的考试成绩和面试选择题的技巧。当时我看完这些,就开始讨厌算法,一来就是3年。
现在终于看到了《数据结构与算法分析——Java语言描述》(第二版)这本书,书中第二章讲解的想对详细的多,解决了我的疑惑和恐惧。
并且最大最长子序列是动态规划的经典问题(其实不加这个称号,也许初学者就不怕了),体现了根据现有数据来决策未来操作的思想。

问题描述

例如:有一个序列,例如 1 2 -4 1 3 -5.
求出最长的和最大的子序列和。如本例的结果就是:4(1, 3)

思路

方法1

暴力解决。计算出从每个i<length和i<j<length的和。 O(n3)

方法2

在上面的基础上优化,考虑到i确定时,j递增的过程中,计算和只需要上一步的结果加a[j]。 O(n2)

方法3

考虑到如下事实:
1. 将一个序列等分成两部分,最大和序列可能在左边,可能在右端,也可能跨越左右边(包含了左边最右端点,和右边最左端点)。我们可以对它分别求和,然后选择其中最大的值最为这个序列的最大值。
2. 当序列只有一元素时,该元素为正数,则它就是最大和;反之,0为最大和。
因此使用分治方法递归地解决。 O(nlogn)

方法四

考虑到如下事实:
1. 负数不应该成为一个最大最长子序列的第一个元素。
2. 负数和序列不应该成为一个最大最长子序列的头部。
3. 在一次扫描过程中,如果已扫描部分的和为负数,我们可以将其抛弃,并且在由正变负的过程中,我们可以记录下最大值出现的位置。
采用动态规划的方法,最终时间复杂度为 O(n) .

解决方案

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.file.Files;
import java.util.Scanner;

/**
 * 求解最大最长子问题的四种解法。 From:《数据结构与算法分析 Java语言描述》 第2版
 */
abstract class LongestMaxSumSubSeq {
    protected int[] sequence;
    protected int length;

    /**
     * @param sequence
     *            原序列
     * @param length
     *            原序列的实际长度(可能小于等于数组长度)
     */
    public LongestMaxSumSubSeq(int[] sequence, int length) {
        this.sequence = sequence;
        this.length = length;
    }

    /**
     * 计算最大最长子序列和。
     * 
     * @return 最大最长子序列和
     */
    public abstract int calculate();

}

/**
 * 暴力枚举所有可能O(n^3)
 */
class Method1 extends LongestMaxSumSubSeq {

    public Method1(int[] sequence, int length) {
        super(sequence, length);
    }

    @Override
    public int calculate() {
        if (null == sequence) {
            return 0;
        }

        int maxSum = 0;

        // 选定起点
        for (int i = 0; i < length; i++) {
            // 选定终点
            for (int j = i; j < length; j++) {

                int curSum = 0;

                // 对每一段求和,与最大值比较
                for (int k = i; k <= j; k++) {
                    curSum += sequence[k];
                }
                if (curSum > maxSum) {
                    maxSum = curSum;
                }
            }
        }
        return maxSum;
    }

}

/**
 * 迭代枚举所有可能O(n^2)
 */
class Method2 extends LongestMaxSumSubSeq {

    public Method2(int[] sequence, int length) {
        super(sequence, length);
    }

    @Override
    public int calculate() {
        if (null == sequence) {
            return 0;
        }

        int maxSum = 0;

        // 起点
        for (int i = 0; i < length; i++) {
            int curSum = 0;
            // 终点
            for (int j = i; j < length; j++) {
                curSum += sequence[j];
                // 因为这一次仅仅是将终点后移一位,因此完全可以用上一次的加过来
                if (curSum > maxSum) {
                    maxSum = curSum;
                }
            }
        }
        return maxSum;
    }

}

/**
 * 分治法求解O(n*log(n))
 */
class Method3 extends LongestMaxSumSubSeq {

    public Method3(int[] sequence, int length) {
        super(sequence, length);
    }

    @Override
    public int calculate() {
        if (null == sequence) {
            return 0;
        }

        int maxSum = 0;
        maxSum = recursedCalMax(0, length - 1);
        return maxSum;
    }

    /**
     * 二分治方法计算最大最长子序列
     * 
     * @param start
     *            第0个元素的位置
     * @param end
     *            最后一个元素的位置
     * @return
     */
    private int recursedCalMax(int start, int end) {
        int max = 0;

        // 递归基底:只有一个元素
        if (start == end) {
            return sequence[start] > 0 ? sequence[start] : 0;
        }

        // 递归步骤:二分
        int center = (start + end) / 2;
        /*
         * 最大值可能在左边,可能在右边,也可能在中间,先分别求出, 然后比较。左右递归分治求出,中间部分两端扩展,单独求出。
         * 三者彼此独立,计算顺序是无关的。
         */
        // 求出左边、右边最大值
        int maxLeft = recursedCalMax(start, center);
        int maxRight = recursedCalMax(center + 1, end);

        // 求中间部分,必须包含最左端最右元素,或者最右端最左元素
        int maxLeftBorder = 0;
        int maxRightBorder = 0;
        // 临时储存当前和
        int curLeftBorder = 0;
        int curRightBorder = 0;

        for (int i = center; i >= start; i--) {
            curLeftBorder += sequence[i];
            maxLeftBorder = curLeftBorder > maxLeftBorder ? curLeftBorder
                    : maxLeftBorder;
        }
        for (int i = center + 1; i <= end; i++) {
            curRightBorder += sequence[i];
            maxRightBorder = curRightBorder > maxRightBorder ? curRightBorder
                    : maxRightBorder;
        }

        max = Math.max(Math.max(maxLeft, maxRight), maxLeftBorder + maxRightBorder);

        return max;
    }

}

/**
 * 动态规划法求解O(n)
 */
class Method4 extends LongestMaxSumSubSeq {

    public Method4(int[] sequence, int length) {
        super(sequence, length);
    }

    @Override
    public int calculate() {
        if (null == sequence) {
            return 0;
        }

        int maxSum = 0;
        int curSum = 0;

        for (int i = 0; i < length; i++) {
            // 负值、负序列不做开头
            curSum += sequence[i];
            if (curSum > maxSum) {
                maxSum = curSum;
            }
            if (curSum < 0) {
                // 负值和序列舍弃
                curSum = 0;
            }
        }
        return maxSum;
    }

}

public class Main {

    public static void calculateAndPrint(LongestMaxSumSubSeq method) {
        int result = 0;
        long startTime = 0L;
        long endTime = 0L;

        startTime = System.currentTimeMillis();
        result = method.calculate();
        endTime = System.currentTimeMillis();
        System.out.println("-----" + method.getClass().getSimpleName()
                + "-----");
        System.out.println("结果: " + result);
        System.out.println("耗时: " + (endTime - startTime));
        System.out.println();
    }

    private static Scanner getScanner(String[] args) {
        Scanner scanner = null;
        // 无参数
        if (args.length == 0) {
            scanner = new Scanner(System.in);
        } else {
            FileInputStream fin;
            try {
                fin = new FileInputStream(args[0]);
                scanner = new Scanner(fin);
            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return scanner;
    }

    @SuppressWarnings("resource")
    public static void main(String[] args) {
        Scanner scanner = getScanner(args);
        if (null == scanner) {
            System.err.println("输入流错误");
            return;
        }
        int length = scanner.nextInt();
        int[] sequence = new int[length];
        for (int i = 0; i < length; i++) {
            sequence[i] = scanner.nextInt();
        }
        calculateAndPrint(new Method1(sequence, length));
        calculateAndPrint(new Method2(sequence, length));
        calculateAndPrint(new Method3(sequence, length));
        calculateAndPrint(new Method4(sequence, length));
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值