代码随想录之贪心算法

202 阅读7分钟

贪心算法

T53-最大子数组和

见LeetCode第53题[最大子数组]

题目描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 子数组是数组中的一个连续部分。

示例 1:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6 。

我的思路

  • 这道题看上去有点子像是动态规划,根据前置数组的和当前元素的值来判断是否应该进行状态转移
  • 状态转移方程为:
    • pre = Math.max(pre, curNum)
    • max = Math.max(pre, max)

我的题解

public int maxSubArray(int[] nums) {
    int max = nums[0];
    int pre = nums[0];
    for (int i = 1; i < nums.length; i++) {
        pre = Math.max(pre + nums[i], nums[i]);
        max = Math.max(pre, max);
    }
    return max;
}

复杂度分析

  • 时间复杂度:O(N)O(N),遍历了一遍数组
  • 空间复杂度:O(1)O(1),临时变量用来存储pre

T122-买卖股票的最佳时机II

见LeetCode第122题[买卖股票的最佳时机II]

题目描述

给你一个整数数组prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例 1:

输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7

我的思路

  • 根据题意,可以在一个周期内买卖任意次股票
  • 若想获得最大利润,就必须在最低点买入,并在最高点抛出
  • 可以使用快慢双指针的做法,快指针指向最高点,慢指针指向最低点,之差即为利润
  • 先通过单调递减性,找到最低点,在通过单调递增找到最高点
  • 注意:非严格单调性,即使相等,指针也要往后走

我的题解

/**
 * 买卖股票的最佳时机
 * @param prices
 * @return
 */
public int maxProfit(int[] prices) {
    int maxProfit = 0;
    int fast = 1;
    int slow = 0;
    if (prices == null || prices.length <= 1) return 0;
    while (fast < prices.length) {
        // 先找买入的点
        while (fast < prices.length && prices[fast] <= prices[fast - 1]) fast++;
        slow = fast - 1;
        // 找到卖出的点
        while (fast < prices.length && prices[fast] >= prices[fast - 1]) fast++;
        maxProfit += (prices[fast - 1] - prices[slow]);
    }
    return maxProfit;
}

计算复杂度分析

  • 时间复杂度:O(N)O(N),总体上是快指针遍历了一遍数组
  • 空间复杂度:O(1)O(1),用来存储快慢指针

T55-跳跃游戏

见LeetCode第55题跳跃游戏

题目描述

给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。

我的思路

  • 站在远点,能够到达的最大索引就是maxRange
  • 遍历从[0, maxRange]的范围
    • 如果maxRange >= nums.length - 1,表示可以到达数组末尾,直接返回true
    • 如果当前索引 + 能够走的步数 > 最大距离,更新最大距离
public boolean canJump(int[] nums) {
    if (nums.length <= 1) return true;
    int maxRange = nums[0];
    for (int i = 0; i <= maxRange; i++) {
        if (maxRange >= nums.length - 1) return true;
        maxRange = Math.max(maxRange, i + nums[i]);
    }
    return false;


}

计算复杂度分析

  • 时间复杂度:O(N)O(N),最坏情况下就是找到结果,遍历一遍数组
  • 空间复杂度:$O(1),用来存储最远的索引maxRange

T45-跳跃游戏II

见LeetCode第45题[跳跃游戏II]

题目描述

我的思路

  • 记录当前位置和最远位置,以及需要的步数count
  • 最远位置或者等于数组尾部索引,直接返回步数
  • 在当前位置和最大位置中进行遍历,记录下一步能够调的最远的索引
  • 更新当前位置、最远索引以及步数
  • 注意最后要加上数组越界的一步

我的题解

/**
 * 跳跃游戏II:保证能够跳到数组末尾,但是求出需要跳跃的最小次数
 * 1.记录当前位置和最远位置,以及需要的步数count
 * 2.最远位置或者等于数组尾部索引,直接返回步数
 * 3.在当前位置和最大位置中进行遍历,记录下一步能够调的最远的索引
 * 4. 更新当前位置以及最远索引
 * @param nums
 * @return
 */
public int jump(int[] nums) {
    if (nums == null || nums.length <= 1) return 0;
    int step = 0;
    int cur = 0;
    int maxRange = nums[0];
    while (maxRange < nums.length - 1) {
        // 找到当前最远的位置以及坐标
        int nextStep = cur;
        int curRange = maxRange;
        for (int i = cur; i <= curRange; i++) {
            if (i + nums[i] > maxRange) {
                maxRange = i + nums[i];
                nextStep = i;
            }
        }
        cur = nextStep;
        step++;
    }
    return step + 1; // 需要加上最后越界的一步
}

复杂度分析

  • 时间复杂度:O(N)O(N),快指针遍历一遍数组
  • 空间复杂度:O(1)O(1),用来存储快慢指针和中间变量

T1005-K次取反后最大化的数组

见LeetCode第1005题[K次取反后最大化的数组]

题目描述

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。

重复这个过程恰好 k 次。可以多次选择同一个下标 i 。

以这种方式修改数组后,返回数组 可能的最大和 。

示例 1:

  • 输入:A = [4,2,3], K = 1
  • 输出:5
  • 解释:选择索引 (1) ,然后 A 变为 [4,-2,3]。

我的思路

  • 首先对数组从小到大进行排序
  • 如果开始位置为自然数,则对开始索引翻转K次
  • 如果开始位置为负数
    • 先将所有的负数翻转为正数
    • 对最小的数翻转剩余的次数

我的题解

/**
     * K 次取反之后最大化数组的和
     * @param nums
     * @param k
     * @return
     */
    public int largestSumAfterKNegations(int[] nums, int k) {

        Arrays.sort(nums);
        if (nums[0] >= 0) {
            nums[0] = k % 2 == 0 ? nums[0] : -nums[0];
            return calculateSum(nums);
        }
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] >= 0 || k == 0) break;
            nums[i] = - nums[i];
            k--;

        }
        // 重新进行排序
        Arrays.sort(nums); // 可以优化,直接在当前的 i 索引位置 或者 i - 1进行翻转
        if (k > 0) {
            // 对第一个进行翻转
            nums[0] = k % 2 == 0 ? nums[0] : -nums[0];
        }
        return calculateSum(nums);

    }

    /**
     * 计算数组的和
     * @param nums
     * @return
     */
    private int calculateSum(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        return sum;
    }

计算复杂度分析

  • 时间复杂度:O(NlogN)O(N\log N), 主要的计算复杂度在数组的排序上
  • 空间复杂度:O(N)O(N),排序过程中的临时数组

T134-加油站

见LeetCode第134题[加油站]

题目描述

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] **升。

你有一辆油箱容量无限的的汽车,从第 **i **个加油站开往第 **i+1 **个加油站需要消耗汽油 cost[i] **升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

示例 1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释: 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

我的思路

  • 使用remainder = gas - cost可以得到每一站盈余的汽油
  • 对剩余的汽油进行累加,得到一个remainders数组
  • 如果想要汽车能够绕行一周,那么这个数组中的每个元素都应该大于等于 0
  • 因此,应该从值最小的下标的下一个开始出发

我的题解

/**
     * 加油站
     * @param gas
     * @param cost
     * @return
     */
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int[] remainders = new int[gas.length];
        int remainderSum = 0;
        for (int i = 0; i < remainders.length; i++) {
            remainderSum += gas[i] - cost[i];
            if (i == 0) {
                remainders[i] = gas[i] - cost[i];
            } else {
                remainders[i] = remainders[i - 1] + gas[i] - cost[i];
            }
        }
        if (remainderSum < 0) return -1;
        // 只需要遍历remainders 找到最小值的坐标
        int resIndex = -1;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < remainders.length; i++) {
            if (remainders[i] < min) {
                min = remainders[i];
                resIndex = i;
            }
        }
        return (resIndex + 1) % remainders.length;


    }

计算复杂度分析

  • 时间复杂度:O(N)O(N),单层遍历数组,时间复杂度为O(N)O(N)
  • 空间复杂度:O(N)O(N),需要数组存储剩余汽油的变化趋势

T135-分发糖果

见LeetCode第135题[分发糖果]

题目描述

我的思路

  • 相邻的两个孩子,评分最高的获得更多的糖果
  • 相邻分为左边相邻和右边相邻,所以可以遍历两遍评分数组,分别得出对比左边或者右边的孩子,自己应该得到的最小的糖果
  • 当前比左边的大,就多一块,否则的话就只给一块糖果
  • 最后对这两个数组进行比较,分到的最少得糖果为二者之间的最大值

我的题解

/**
 * 分发糖果
 * @param ratings
 * @return
 */
public int candy(int[] ratings) {
    if (ratings.length == 1) return 1;
    int n = ratings.length;
    int[] minCandies = new int[n];
    // 首先和自己左边的同学进行比较
    minCandies[0] = 1;
    for (int i = 1; i < n; i++) {
        if (ratings[i] > ratings[i - 1]) {
            minCandies[i] = minCandies[i - 1] + 1;
        } else {
            minCandies[i] = 1;
        }
    }

    // 在和自己右边的同学比较
    minCandies[n - 1] = Math.max(1, minCandies[n - 1]);
    int candies = minCandies[n - 1];
    for (int i = n - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            minCandies[i] = Math.max(minCandies[i], minCandies[i + 1] + 1);
        } else  {
            minCandies[i] = Math.max(minCandies[i], 1);
        }
        candies += minCandies[i];
    }
    return candies;

}

计算复杂度分析

  • 时间复杂度:O(N)O(N),正反遍历了两遍rating数组
  • 空间复杂度:O(N)O(N),记录每个学生应该得到的最少得糖果

T860-柠檬水找零

见LeetCode第860题[柠檬水找零]

题目描述

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。

输入: bills = [5,5,5,10,20]
输出: true
解释: 前 3 位顾客那里,我们按顺序收取 35 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true

我的思路

  • 首先可以使用一个数组存储零钱
  • 对于每个顾客,先把零钱找开,然后顾客的钱入库
    • 找零钱,先从大面额的开始找
    • 贪心就在优先找10块的
  • [建议参考LeetCode官方解答]
/**
     * 柠檬水找零钱
     * @param bills
     * @return
     */
    public boolean lemonadeChange(int[] bills) {
        if (bills.length == 1 && bills[0] > 5) return false;

        // 创建一个零钱匣子
        int[] changes = new int[3];
        for (int bill : bills) {
            if (!canChange(bill, changes)) {
                return false;
            }
            // 找零,并入库
            change(changes, bill);
        }
        return true;

    }

    private void change(int[] changes, int bill) {
        int units = bill / 5 - 1;
        int curUnit = 2;
        while (units != 0) {
            while (curUnit > 0 && changes[curUnit] >= 1 && units - curUnit >= 0) {
                units -= curUnit;
                changes[curUnit]--;
            }
            curUnit --;
        }
        if (bill != 20) {
            changes[bill / 5] ++;
        }
    }

    /**
     * 判断能否找开
     * @param bill
     * @param changes
     * @return
     */
    private boolean canChange(int bill, int[] changes) {
        int[] copyChange = Arrays.copyOf(changes, changes.length);
        int units = bill / 5 - 1;
        int curUnit = 2;
        while (units != 0) {
            if (curUnit == 0) return false;
            while (copyChange[curUnit] >= 1 && units - curUnit >= 0) {
                units -= curUnit;
                copyChange[curUnit]--;
            }
            curUnit --;
        }
        return true;
    }

计算复杂度分析

  • 时间复杂度:O(N)O(N),本质上就是对所有bills数组做了一个遍历
  • 空间复杂度:O(1)O(1),定长数组,甚至可以使用两个变量来存储10元和5元的

T406-根据身高体重重建队列

见LeetCode第406题[根据身高体重重建队列]

题目描述 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1:

输入: people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出: [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 01 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0123 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

我的思路

  • 首先对二维数组按照身高排序,如果身高相同,则按照位次排序
  • 对结果数组queue赋初值,保证每个元素的第一个元素为越上界的非法整数,即queue[i] = {1e5+1, 0}
  • 遍历排序之后的people
    • 对于每个人,遍历结果队列queue,临时变量counts记录超过此人的人数
    • 如果counts = people[i][1],则将这个人插入到队列queue[j]的位置

我的题解

/**
 * 按根据身高和次序将人插入到正确的位置
 * @param people
 * @return
 */
public int[][] reconstructQueue(int[][] people) {
    if (people.length <= 1) return people;

    // 对 people中的元素按照身高和次序进行排序
    Arrays.sort(people, (o1, o2) -> {
        if (o1[0] == o2[0]) {
            return Integer.compare(o1[1], o2[1]);
        }
        return Integer.compare(o1[0], o2[0]);
    });

    int[][] queue = new int[people.length][2];
    for (int[] e : queue) {
        e[0] = Integer.MAX_VALUE;
    }

    for (int[] person : people) {
        int count = 0; // 记录当前有多少人大于或等于当前人的身高
        for (int[] human : queue) {
            if (count == person[1] && human[0] == Integer.MAX_VALUE) {
                human[0] = person[0];
                human[1] = person[1];
                break;
            }
            // 当前位置不匹配
            if (human[0] >= person[0]) count++;
        }
    }
    return queue;
}

计算复杂度分析

  • 时间复杂度:O(N2)O(N^2),对原始数组进行排序需要O(NlogN)O(N\log N),构造结果数组需要O(N2)O(N^2)的计算复杂度
  • 空间复杂度:O(N)O(N),保存结果数组

其他思路

  • 高的人往前看是看不到矮的人的
  • 可以按照身高从高到低排序,如果身高相同,那么位次较低的就在前面
  • 先让高个子落座,矮个子在高个子之间插空坐
输入: [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]
输出:
[[7, 0]]
[[7, 0], [7, 1]]
[[7, 0], [6, 1], [7, 1]]
[[5, 0], [7, 0], [6, 1], [7, 1]]
[[5, 0], [7, 0], [5, 2], [6, 1], [7, 1]]
[[5, 0], [7, 0], [5, 2], [6, 1], [4, 4], [7, 1]]

T452-用最少的箭引爆气球

见LeetCode第452题[用最少的箭引爆气球]

题目描述

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足  xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数

示例 1:

输入: points = [[10,16],[2,8],[1,6],[7,12]]
输出: 2
解释: 气球可以用2支箭来爆破:
-x = 6处射出箭,击破气球[2,8][1,6]-x = 11处发射箭,击破气球[10,16][7,12]

我的思路

  • 这一题本质上就是求解相交区间的数量
  • 临时变量List<int[]> temp存储相交的区间
  • points中取出每个气球和temp中的香蕉区间做匹配
    • 如果不香蕉,则将当前气球放temp
    • 如果香蕉,更新香蕉之后的区间
  • 返回temp.size()即可

我的题解

/**
     * 使用最少的箭引爆气球
     * @param points
     * @return
     */
    public int findMinArrowShots(int[][] points) {
        if (points.length == 1) return 1;
        // 存储气球香蕉区间的集合
        List<int[]> temp = new ArrayList<>();



        for (int[] point : points) {
            boolean flag = false;
            for (int[] pubZone : temp) {
                if (broadenZone(point, pubZone)) {
                    flag = true;
                    break;
                }
            }
            if (!flag) {
                temp.add(point);
            }

        }
        return temp.size();
    }

    /**
     * 判断是否有香蕉的地方,如果有,则扩张区间
     * @param point
     * @param pubZone
     * @return
     */
    private boolean broadenZone(int[] point, int[] pubZone) {
        int[] leftPoint = point[0] < pubZone[0] ? point : pubZone;
        int[] rightPoint = pubZone[0] >= point[0] ? pubZone : point;

        // 如果左点的右端点大于等于右点的左端点,即为香蕉
        if (leftPoint[1] < rightPoint[0]) return false;
        // 香蕉,扩展区间
        pubZone[0] = rightPoint[0];
        pubZone[1] = Math.min(leftPoint[1], rightPoint[1]);
        return true;
    }

计算复杂度分析

  • 时间复杂度O(N2)O(N^2),双重for循环,最坏的可能计算复杂度为O(N2)O(N^2)
  • 空间复杂度O(N)O(N),需要临时数组存储相交区间

优化思路

在进行区间相交判断的时候,对于已经排序好的区间,我们仍然是从香蕉区间集合temp的第一个元素遍历到最后一个元素。但是,我们只需要判断香蕉区间的最后一个元素能否和当前气球区间香蕉即可。其他的不必判断。这样,计算复杂度下降到O(NlogN)O(N\log N)级别。

T435-无重叠区间

见LeetCode第435题[无重叠区间]

题目描述

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

注意 只在一点上接触的区间是 不重叠的。例如 [1, 2] 和 [2, 3] 是不重叠的。

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

我的朴素思路

  • 首先要对坐标按照从小到大进行排序,相同起点的,长度短的排前面
  • 使用List<int[]>存储无碰撞区间的坐标集合
  • 对于排序后的每一个区间
    • 如果没有和当前无碰撞区间集合的最后一个元素发生碰撞,则直接入集合
    • 如果发生碰撞了,那么判断当前区间和最后一个区间哪个右边界更小,更小的添加到集合
  • 结果为intervals.length - unOverlappings.size()

我的题解

/**
     * 非重叠区间
     * @param intervals
     * @return
     */
    public int eraseOverlapIntervals(int[][] intervals) {
        if (intervals.length <= 1) return 0;

        int n = intervals.length;
        // 排序
        Arrays.sort(intervals, ((o1, o2) ->
                {
                    if (o1[0] == o2[0]) {
                        return Integer.compare(o1[1], o2[1]);
                    }
                    return Integer.compare(o1[0], o2[0]);
                }
        ));
        List<int[]> nonOverlappings = new ArrayList<>();
        for (int[] interval : intervals) {
            // 如果当前无碰撞则直接入集合
            if (nonOverlappings.isEmpty() || !isOverlapped(interval, nonOverlappings.get(nonOverlappings.size() - 1))) {
                nonOverlappings.add(interval);
            } else {
                // 有碰撞,处理碰撞,比较谁的边界更短
                int[] targetInterval = nonOverlappings.get(nonOverlappings.size() - 1);
                if (interval[1] <= targetInterval[1]) {
                    nonOverlappings.remove(nonOverlappings.size() - 1);
                    nonOverlappings.add(interval);
                }
            }
        }
        return n - nonOverlappings.size();

    }

    /**
     * 判断两个区间是否重叠,排序好的
     * @param interval
     * @param target
     * @return
     */
    private boolean isOverlapped(int[] interval, int[] target) {
        return target[1] > interval[0];
    }

复杂度分析

  • 时间复杂度O(NlogN)O(N\log N),主要的计算复杂度在排序
  • 空间复杂度O(N)O(N),用来存储无碰撞数组nonOverlappings

优化思路

观察上面的代码可以看出,在判断当前区间是否发生碰撞的时候,仅仅使用了非碰撞集合的最后一个元素,因此可以使用一个int类型变量right来存储非碰撞区间的右边界,对于当前区间interval

  • 如果当前区间的左边界小于非碰撞区间的右边界,即right > interval[0],发生碰撞,直接将当前区间移除,结果计数值count++
  • 否则,更新非碰撞区间的右边界right = interval[1]
public int eraseOverlapIntervalsII(int[][] intervals) {
    if (intervals.length <= 1) return 0;
    Arrays.sort(intervals, (Comparator.comparingInt(o -> o[1])));
    int count = 0;
    int right = Integer.MIN_VALUE;
    for (int[] interval : intervals) {
        if (interval[0] < right) {
            count++;
        } else {
            right = interval[1];
        }
    }
    return count;
    }

T763-划分字母区间

见LeetCode第763题[划分字母区间]

题目描述

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。

返回一个表示每个字符串片段的长度的列表。

示例 1:

输入: s = "ababcbacadefegdehijhklij"
输出: [9,7,8]
解释:
划分结果为 "ababcbaca""defegde""hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。 

我的思路

  • 这道题本质上和上一题没什么区别,每个字母的开始位置和结束位置记作一个区间
  • 那么就是保证结果集合中有更多的区间
  • 首先对每个字母的区间按照从小到大进行排序
    • 如果产生了碰撞,就进行合并
    • 未产生碰撞,就count++

我的题解

public List<Integer> partitionLabels(String s) {
    List<Integer> resList = new ArrayList<>();
    if (s.length() == 1) {
        resList.add(s.length());
        return resList;
    }
    // 使用HashMap记录每个字符出现的区间
    HashMap<Character, int[]> intervals = new HashMap<>();
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (!intervals.containsKey(c)) {
            int[] interval = new int[]{i, i};
            intervals.put(c, interval);
        } else {
            int[] interval = intervals.get(c);
            interval[1] = i;
        }
    }
    // 将HashMap的值转换为一个List
    List<int[]> ranges = new ArrayList<>(intervals.values());
    // 排序
    ranges.sort(new Comparator<int[]>() {
        @Override
        public int compare(int[] o1, int[] o2) {
            if (o1[0] == o2[0]) {
                return Integer.compare(o1[1], o2[1]);
            }
            return Integer.compare(o1[0], o2[0]);
        }
    });

    int[] curRange = {-1, -1};
    // 合并区间
    for (int[] range : ranges) {
        if (range[0] < curRange[1]) {
            // 更新当前 range
            curRange[1] = Math.max(curRange[1], range[1]);
        } else {
            // 计算curRange的大小
            if (curRange[0] != -1) {
                int curLen = curRange[1] - curRange[0] + 1;
                resList.add(curLen);
            }
            curRange = range;
        }
    }
    resList.add(curRange[1] - curRange[0] + 1);
    return resList;

}

T56-合并区间

见LeetCode第56题[合并区间]

题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

示例 1:

输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3][2,6] 重叠, 将它们合并为 [1,6].

我的思路

  • 首先按照区间的右端点从小到大进行排序,左端点在合并过程中是逐渐往左边扩散的
  • 临时集合保存合并的临时结果,初始化为排序后区间的第一个元素
  • 从排序区间数组中第二个元素开始遍历
  • 如果当前区间的左端点小于等于临时集合中最后一个元素的右端点,即为发生碰撞
    • 将临时结果集中的元素弹出,合并区间,接着获取临时结果集中的最后一个元素
  • 没有发生碰撞,直接跳出对临时结果集的循环,将当前合并后区间添加到临时结果集
  • 外层循环:排序后的intervals
  • 内层循环:弹出temp末尾元素,直到没有发生碰撞
  • 将合并后的区间添加到temp

我的题解

public int[][] merge(int[][] intervals) {

    if (intervals.length <= 1) return intervals;

    // 将区间按照右端点从小到大排序
    Arrays.sort(intervals, (Comparator.comparingInt(o -> o[1])));
    List<int[]> temp = new ArrayList<>();
    temp.add(intervals[0]);
    for (int i = 1; i < intervals.length; i++) {
        while (!temp.isEmpty()) {
            int[] preInterval = temp.get(temp.size() - 1);
            // 当前区间的左端点小于等于前区间的右,可以合并
            if (intervals[i][0] <= preInterval[1]) {
                intervals[i][0] = Math.min(preInterval[0], intervals[i][0]);
                temp.remove(temp.size() - 1);
            } else {
                // 不可合并,当前区间加入
                break;
            }
        }
        temp.add(intervals[i]);

    }
    return temp.toArray(new int[0][]);
}

优化思路

  • 首先按照区间的左端点排序,如果左端点相同,则排序右端点
public int[][] mergeII(int[][] intervals) {

    if (intervals.length <= 1) return intervals;

    // 将区间按照左端点从小到大排序
    Arrays.sort(intervals, ((o1, o2) -> {
        if (o1[0] == o2[0]) {
            return Integer.compare(o1[1], o2[1]);
        }
        return Integer.compare(o1[0], o2[0]);
    }));

    List<int[]> temp = new ArrayList<>();
    int[] preInterval = intervals[0];
    for (int i = 1; i < intervals.length; i++) {
        // 如果当前区间的左节点小于等于前一个节点的右端点
        if (intervals[i][0] <= preInterval[1]) {
            preInterval[1] = Math.max(intervals[i][1], preInterval[1]);
        } else {
            // pre区间加入结果集
            temp.add(preInterval);
            preInterval = intervals[i];
        }

    }
    // 加入最后一个pre
    temp.add(preInterval);
    return temp.toArray(new int[0][]);
}
    

复杂度分析

  • 时间复杂度O(NlogN)O(N\log N),主要是用于排序
  • 空间复杂度O(logN)O(\log N),主要是用来存储中间结果

思考:区间题中,何时应该对左端点进行排序,何时应该对右边端点进行排序?

T738-单调递增的数字

见LeetCode第738题[单调递增的数字]

题目描述

当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增 。

示例 1:

输入: n = 10
输出: 9

我的思路[超时]

  • 朴素的遍历方式,
  • 从最高位减去1,即(x-1 9 9 9 9 ...)开始遍历,直到n
  • 如果符合要求,就更新最大值maxVal

优化思路

  • 从高位往低位遍历,如果出现了高位数字大于低位数字
  • 记下当前的索引 index
  • [index + 1, length)所有的位置都置为 9
  • 如果当前数字和前一个数字相同,则第一个数字置为 x - 1,其他置为 9

我的代码

public int monotoneIncreasingDigitsII(int n) {

    char[] numChars = String.valueOf(n).toCharArray();
    if (numChars.length == 1) return n;

    int markIndex = -1;
    for (int i = 0; i < numChars.length - 1; i++) {
        if (numChars[i] > numChars[i + 1]) {
            // 记下当前的位置 i
            markIndex = i;
            break;
        }
    }
    if (markIndex == -1) return n;
    for (int i = markIndex + 1; i < numChars.length; i++) {
        numChars[i] = '9';
    }
    for (int i = markIndex; i >= 0; i--) {
        if (i == 0 || numChars[i] != numChars[i - 1]) {
            numChars[i] -= 1;
            break;
        } else {
            numChars[i] = '9';
        }
    }

    return Integer.parseInt(new String(numChars));

}

T968-监控二叉树

见LeetCode第968题[监控二叉树]

题目描述

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例

image.png

输入: [0,0,null,0,0]
输出: 1
解释: 如图所示,一台摄像头足以监控所有节点。

提示:

  1. 给定树的节点数的范围是 [1, 1000]
  2. 每个节点的值都是 0。

我的思路

  • 如何标记安装监控的点?将该节点的值置为 1
  • 如果判断某个节点是否应该安装监控?
    • 判断该节点的父节点、子节点是否都没有监控
  • 如何安装最少的监控呢?
    • 对于当前合法节点,判断当前节点,左孩子、右孩子三个哪个覆盖的节点更多,则将其置为1

偶遇困难二叉树,题目晦涩难懂如同天书,脑子混乱好似浆糊,拼劲全力无法写出。

参考思路

  • 如何安装最少的监控?
    • 监控能覆盖到的节点越多越好
    • 空节点|叶子节点 都不需要安装
  • 如何去判断一个节点是否该安装监控?
    • 使用自地向上的递归,根据左右孩子节点的状态来判断根节点的状态
  • 一个节点有几种状态?
    • status_0:没有被覆盖到
    • status_1:被覆盖到了,但是不需要安装监控
    • status_2:被覆盖到了,并且监控就在本节点
  • 如何根据子节点的状态判断当前节点的状态?
    • 任意一个子节点没有被覆盖到(status_0),则根节点需要安装监控(status_2),监控数量加 1
    • 任意一个子节点安装了监控(status_2),则根节点不需要安装监控(status_1)
    • 所有的子节点都被覆盖到了但是都没装监控,根节点没覆盖到,也没装监控(status_0)

题解代码

private int count = 0;

/**
 * 监控二叉树
 * @param root
 * @return
 */
public int minCameraCover(TreeNode root) {
    // 如果是空树,则返回 0
    if (root == null) return 0;

    // 如果只有单个节点,则返回 1
    if (root.left == null && root.right == null) return 1;

    // 根据返回状态判断根节点是否被覆盖到
    int status = coverTree(root);

    // 没有覆盖到则摄像头 + 1
    if (status == 0) {
        count++;
    }
    return count;
}

/**
 * 使用摄像头覆盖树节点
 * @param root
 * @return 返回值有三种状态:0 1 2
 * 0:表示没有覆盖到根节点
 * 1:覆盖到根节点了,但是根节点没有摄像头
 * 2:覆盖到跟根节点了,并且根节点就是摄像头
 */
private int coverTree(TreeNode root) {
    if (root == null) return 1; // 空节点不需要摄像头,但是默认被覆盖

    if (root.left == null && root.right == null) return 0; // 不在叶子节点装摄像头

    // 对树进行后序遍历
    int leftStatus = coverTree(root.left);
    int rightStatus = coverTree(root.right);

    // 根据子节点的状态,判断父节点是否应该安装摄像头
    // 子节点没有覆盖全,则根节点肯定要加摄像头
    if (leftStatus == 0 || rightStatus == 0) {
        count++;
        return 2;
    }

    // 子节点被覆盖,并且有一个有监控把根节点也覆盖了
    if (leftStatus == 2 || rightStatus == 2) {
         return 1;
    }

    // 两个子节点都覆盖了,但是根节点没有被覆盖
    return 0;

}