第五章 高频面试系列

200 阅读4分钟

跳跃问题

题目

image.png

版本1 正确

    public boolean canJump(int[] nums) {

        // 目前能够到达的最远距离
        int farthDis = 0;

        for(int i = 0; i < nums.length - 1; i ++) {
            farthDis = Math.max(farthDis, i + nums[i]);

            if (farthDis <= i) {
                return Boolean.FALSE;
            }
        }
        return Boolean.TRUE;
    }
    

正确的原因

(1) 对于每个i计算能够到达的最远距离, 如果当前i到不了下一个i, 就会被farthDis <= i的条件拦截, 返回错误. 正常来说, 因为nums[i]没有负数, 通过farthDis = Math.max(farthDis, i + nums[i]), farthDis应该是大于i的, 但是当nums[i] = 0的时候, 就会存在farthDis = i的可能, 这也是对于本道题目来说, 唯一无法继续跳月的可能, 因此这里判断条件只能是farthDis <= i

(2) 只需要执行到倒数第二个元素, 并且能够到达最后一个元素即可, 因为如果最后元素的值为0, 也应该返回正确, 而不是错误, 因此i < nums.length - 1

跳跃问题2

题目

image.png

版本1 正确 dp + 部分贪心

    public static int jump(int[] nums) {

        // 明确状态
        // 状态就是你所处在nums数组的哪个位置

        // 明确dp数组的含义
        // dp[i] = x; 表示跳到i这个位置, 需要的最少次数为x
        int [] dp = new int[nums.length];

        // base case
        // dp[0] = 0; 起始位置就是数组的第一个位置

        // 枚举所有状态
        for (int i = 1; i < nums.length; i ++) {
            // 每次计算dp[i] 需要计算i之前的每个位置能否跳到当前i
            dp[i] = i;
            for(int j = 0; j < i; j ++) {
                if (nums[j] >= i - j) {
                    // 表示能够从j一次跳到i
                    dp[i] = Math.min(dp[i], dp[j] + 1);
                    break;
                }
            }
        }

        return dp[nums.length - 1];

    }

正确的思路

(1) 注意关于j的循环, 距离i最远的j, 如果j能够到达i, 此时的dp[j] 一定是最小的, 因此是可以用break截断的, 这个思想也是借鉴了贪心的思想

版本2 错误

    public static int jump(int[] nums) {

        // 完全考虑贪心
        // 每个位置在跳的时候, 选择下个格子最大的那个格子进行跳跃, 得到的跳跃次数最少

        // start是当前的位置
        int start = 0;
        // len是当前位置能够到达的最远距离
        int len = nums[start];

        int count = 0;

        // 判断是否需要跳跃
        while (start + len < nums.length - 1) {
            // 这里的len一定不可能为0
            int tempIndex = start + len;
            for (int i = len; i >= 1; i --) {
                if (nums[start + i] > nums[tempIndex]) {
                    tempIndex = start + i;
                }
            }
            start = tempIndex;
            len = nums[tempIndex];
            count ++;
        }

        if (start == nums.length - 1) {
            // 此时已经位于最后位置了
            return count;
        } else {
            // 当前位置可以直接跳到最后
            return count + 1;
        }

    }

错误的原因

(1) 错误的认为, 贪心算法是在当前的选择中, 选择最大的那个数字, 但是对于下面的情况就出问题了

image.png

(2) 注意在当前位置start, 选择下一步跳的时候, 如果范围内最大的元素是多个, 一定要跳到最后一个元素的地方.

例如输入是 {1, 2, 1, 1, 2}的时候, 在从索引1开始跳的时候, 要跳到索引3, 而不是索引2

版本3 正确

    public static int jump(int[] nums) {

        // 完全考虑贪心
        // 每个位置在跳的时候, 选择的应该是下一个格子j + nums[j]最大的情况
        // 而不是单纯的nums[j]最大的情况

        // start是当前的位置
        int start = 0;
        // len是当前位置能够到达的最远距离
        int len = nums[start];

        int count = 0;

        // 判断是否需要跳跃
        while (start + len < nums.length - 1) {
            // 这里的len一定不可能为0
            int tempIndex = start + len;
            for (int i = len; i >= 1; i --) {
                if (start + i + nums[start + i] > nums[tempIndex] + tempIndex) {
                    tempIndex = start + i;
                }
            }
            start = tempIndex;
            len = nums[tempIndex];
            count ++;
        }

        if (start == nums.length - 1) {
            // 此时已经位于最后位置了
            return count;
        } else {
            // 当前位置可以直接跳到最后
            return count + 1;
        }
        
    }

正确的原因

(1) 正确的识别了贪心的策略是每个位置在跳的时候, 选择的应该是下一个格子j + nums[j]最大的情况, 而不是单纯的nums[j]最大的情况

两地调度

题目

image.png

版本1 正确

    public int twoCitySchedCost(int[][] costs) {

        // 先让n个人都飞往B
        // 此时的费用 sum1 = (price_b_1 + price_b_2 + ... + price_b_n-1 + price_b_n);
        // 如果我让前n - 1人飞往B, 最后一个人飞往A
        // 此时的费用 sum2 = (price_b_1 + price_b_2 + ... + price_b_n-1 + price_a_n);
        // 可以发现sum1和sum2是有关系的
        // sum2 = sum1 + price_a_n - price_b_n
        // 那么如果题目要求的是只要一人飞往A, 那么我们在sum1中挑选出来某个人, 使得price_a_n - price_b_n最小, 是不是就满足条件了
        // 同样的如果要选出一半的人飞往A, 那么我们就选出price_a_n - price_b_n最小的那一半人飞往A就可以了

        Arrays.sort(costs, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if (o1[0] - o1[1] > o2[0] - o2[1]) {
                    return 1;
                }
                return -1;
            }
        });

        int n = costs.length;
        int sum = 0;
        for(int i = 0; i < n; i ++) {
            if (i < n / 2) {
                sum += costs[i][0];
            } else {
                sum += costs[i][1];
            }
        }

        return sum;

    }

正确的思路

(1) 先假设都飞往一个地方, 然后如果挑选其中一个人飞往另一个地方, 怎么挑选最划算, 然后就得到了一开始应该怎么挑选.

用最少数量的箭引爆气球

题目

image.png

版本1 正确

public int findMinArrowShots(int[][] points) {
        if (points.length == 0) {
            return 0;
        }

        // 气球在二维平面的区间, 如果出现重叠, 那么是可以被一箭射爆多个气球的
        // 因此需要求不重叠的区间有几个, 那么就最少需要几只箭
        
        Arrays.sort(points, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if (o1[1] > o2[1]) {
                    return 1;
                }
                return -1;
            }
        });
        
        int end = points[0][1];
        int count = 1;
        for (int i = 1; i < points.length; i ++) {
            int start = points[i][0];
            if (start <= end) {
                continue;
            }
             count ++;
            end = points[i][1];
        }
        return count;
        
    }

正确的思路

(1) 寻找最大不重叠的区间, 第一步, 将数组元素按照end升序排列

(2) 第一个元素就是end最小的区间, 遍历数组, 如果start <= end, 即区间有交集, 则跳过该区间

(3) 遇见start > end的情况, 就是新的一个不重叠的区间, count++, 然后end更新, 此时的end又是剩下的所有元素中的最小的end

最长有效括号

题目

image.png

版本1 正确

    public int longestValidParentheses(String s) {

        // 利用栈, 栈底一直保留最后一个不被匹配的右括号
        // 每当右括号匹配到一次左括号的时候, 都更新一次最大值, 最大值一定是产生在某一次左右匹配的情况

        Stack<Integer> stack = new Stack<>();
        // 维持栈底一直保留最后一个不被匹配的右括号的索引的原则
        stack.push(-1);
        int max = 0;
        for (int i = 0; i < s.length(); i ++) {
            if (s.charAt(i) == '(') {
                stack.push(i);
            } else {
                // 如果遇见右括号, 弹出栈顶的元素, 要么是能够和它匹配的左括号, 要么是不被匹配的右边括号的索引
                int index = stack.pop();
                if (index != - 1 && s.charAt(index) == '(') {
                    // 计算一次最大距离, 此时的最大距离是和目前栈顶的元素做比较的, 思路类似接雨水单调栈的问题
                    max = Math.max(max, i - stack.peek());
                } else {
                    // 更新最后一个不被匹配的右括号的索引为i
                    stack.push(i);
                }
            }
        }

        return max;

    }

正确的原因

(1) 注意计算长度的时机, 以及计算长度时, 是用目前栈顶的元素的索引, 而不是弹出的元素的索引.