面试重灾区—区间算法题

191 阅读2分钟

基本介绍

所谓区间问题,就是线段问题,让你合并所有线段、找出线段的交集等等。主要有两个技巧:

1、排序。常见的排序方法就是按照区间起点排序,或者先按照起点升序排序,若起点相同,则按照终点降序排序。当然,如果你非要按照终点排序,无非对称操作,本质都是一样的。

2、画图。就是说不要偷懒,勤动手,两个区间的相对位置到底有几种可能,不同的相对位置我们的代码应该怎么去处理。

废话不多说,下面我们来做题。

1288:删除被覆盖区间

力扣1288题:leetcode-cn.com/problems/re…

给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。

只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。

在完成所有删除操作后,请你返回列表中剩余区间的数目。

image.png

题目问我们,去除被覆盖区间之后,还剩下多少区间,那么我们可以先算一算,被覆盖区间有多少个,然后和总数相减就是剩余区间数

对于这种区间问题,如果没啥头绪,首先排个序看看,比如我们按照区间的起点进行升序排序:

image.png

排序之后,两个相邻区间可能有如下三种相对位置:

image.png

对于这三种情况,我们应该这样处理:

对于情况一,找到了覆盖区间。

对于情况二,两个区间可以合并,成一个大区间。

对于情况三,两个区间完全不相交。

依据几种情况,我们可以写出如下代码:

class Solution {
    public int removeCoveredIntervals(int[][] intervals) {
        int res = 0;//记录有多少个区间是被覆盖的
        ////按照起始点升序排序,起点相同,按照终点降序排序
        Arrays.sort(intervals, (a, b) -> {
            if(a[0] == b[0]) {
                return b[1] - a[1];
            }
            return a[0] - b[0];
        });
        int left = intervals[0][0];//当前处理的区间左边界
        int right = intervals[0][1];//当前处理的区间右边界
        for(int i = 1; i < intervals.length; i++) {
            if(left <= intervals[i][0] && right >= intervals[i][1]) {
                //情况一:找到覆盖集合
                res++;   
            } else if(right >= intervals[i][0] && right <= intervals[i][1]) {
                //情况二:存在交集,可以合并集合  
                right = intervals[i][1];
            } else {    
                //情况三,不存在交集,重新赋值left, right为当前区间
                left = intervals[i][0];
                right = intervals[i][1];
            }
        }
        //将总的区间减去被覆盖的区间 
        return intervals.length - res;
    }
}

以上就是本题的解法代码,起点升序排列,终点降序排列的目的是防止如下情况:

image.png 对于这两个起点相同的区间,我们需要保证长的那个区间在上面(按照终点降序),这样才会被判定为覆盖,否则会被错误地判定为相交,少算一个覆盖区间。

56. 合并区间

题目链接:leetcode.cn/problems/me…

image.png

这里我们发现,判断区间是否可以合并的关键,在于它们左边界的大小关系。所以我们可以先把所有区间,按照左边界进行排序。

那么在排完序的列表中,可以合并的区间一定是连续的,接着如果存在交集的话,就进行区间合并。

完整代码如下:

class Solution {
    public int[][] merge(int[][] intervals) {
        List<int[]> result = new ArrayList<>();
        Arrays.sort(intervals, (a,b) -> {
            return a[0] - b[0];
        });

        for(int[] temp : intervals) {
            int left = temp[0];
            int right = temp[1];
            int length = result.size();
            if(length == 0 || left > result.get(length-1)[1]) {
                result.add(temp);
            } else {
                //存在交集,需要进行合并
                int mergeLeft = result.get(length-1)[0];
                int mergeRight = Math.max(right, result.get(length-1)[1]);
                result.set(length-1, new int[]{mergeLeft, mergeRight}); 
            }
        }
        return result.toArray(new int[result.size()][]);
    }
}

986. 区间列表的交集

题目链接:leetcode.cn/problems/in…

image.png

题目很好理解,就是让你找交集,注意区间都是闭区间。

解决区间问题的思路一般是先排序,以便操作,不过题目说已经排好序了,那么可以用两个索引指针在AB中游走,把交集找出来,代码大概是这样的:

# A, B 形如 [[0,2],[5,10]...]
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
    List<int[]> res = new ArrayList<>();
    int i = 0;
    int j = 0;
    int m = firstList.length;
    int n = secondList.length;
    while(i < m && j < n) {
        int a1 = firstList[i][0];
        int a2 = firstList[i][1];
        int b1 = secondList[j][0];
        int b2 = secondList[j][1];
        ......
        i++;
        j++;
      
    }
    return res.toArray(new int[res.size()][]);
}

不难,我们先老老实实分析一下各种情况。

首先,对于两个区间,我们用[a1,a2][b1,b2]表示在AB中的两个区间,那么什么情况下这两个区间没有交集呢:

image.png

只有这两种情况,写成代码的条件判断就是这样:

if b2 < a1 || a2 < b1:
    [a1,a2] 和 [b1,b2] 无交集

那么,什么情况下,两个区间存在交集呢?根据命题的否定,上面逻辑的否命题就是存在交集的条件:

# 不等号取反,or 也要变成 and
if b2 >= a1 and a2 >= b1:
    [a1,a2] 和 [b1,b2] 存在交集

接下来,两个区间存在交集的情况有哪些呢?穷举出来:

image.png

这很简单吧,就这四种情况而已。那么接下来思考,这几种情况下,交集是否有什么共同点呢?

image.png

我们惊奇地发现,交集区间是有规律的!如果交集区间是[c1,c2],那么c1=max(a1,b1)c2=min(a2,b2)!这一点就是寻找交集的核心,我们把代码更进一步:

List<int[]> res = new ArrayList<>();
int i = 0;
int j = 0;
int m = firstList.length;
int n = secondList.length;
while(i < m && j < n) {
    int a1 = firstList[i][0];
    int a2 = firstList[i][1];
    int b1 = secondList[j][0];
    int b2 = secondList[j][1];
    if(b2 >= a1 && a2 >= b1) {
        //存在交集
        int left = Math.max(a1, b1);
        int right = Math.min(a2, b2);
        res.add(new int[]{left, right});
    }
    # ...

最后一步,我们的指针ij肯定要前进(递增)的,什么时候应该前进呢?

取决于a2b2的大小关系:

List<int[]> res = new ArrayList<>();
int i = 0;
int j = 0;
int m = firstList.length;
int n = secondList.length;
while(i < m && j < n) {
    int a1 = firstList[i][0];
    int a2 = firstList[i][1];
    int b1 = secondList[j][0];
    int b2 = secondList[j][1];
    if(b2 >= a1 && a2 >= b1) {
        //存在交集
        int left = Math.max(a1, b1);
        int right = Math.min(a2, b2);
        res.add(new int[]{left, right});
    }
    if(b2 < a2) {
        j++;
    } else {
        i++;
    }
}    

完整代码如下:

class Solution {
    public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
        List<int[]> res = new ArrayList<>();
        int i = 0;
        int j = 0;
        int m = firstList.length;
        int n = secondList.length;
        while(i < m && j < n) {
            int a1 = firstList[i][0];
            int a2 = firstList[i][1];
            int b1 = secondList[j][0];
            int b2 = secondList[j][1];
            if(b2 >= a1 && a2 >= b1) {
                //存在交集
                int left = Math.max(a1, b1);
                int right = Math.min(a2, b2);
                res.add(new int[]{left, right});
            }
            if(b2 < a2) {
                j++;
            } else {
                i++;
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}