本节内容
435. 无重叠区间※ 建议 :
题目链接: https://leetcode.cn/problems/non-overlapping-intervals/ 文章讲解: https://programmercarl.com/0435.%E6%97%A0%E9%87%8D%E5%8F%A0%E5%8C%BA%E9%97%B4.html 视频讲解:
题目分析
方案一 将区间按照最右侧进行排序,然后按照从前向后对排序之后的进行遍历,然后判断区间的左值是否大于最大值来确定。
贪心贪在每次找右侧的最小值,如果有区间包括在内就直接排除掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int eraseOverlapIntervals (int [][] intervals) { Arrays.sort(intervals, (o1, o2) -> o1[1 ] - o2[1 ]); int index = Integer.MIN_VALUE; int result = 0 ; for (int i = 0 ; i < intervals.length; i++) { if (intervals[i][0 ] >= index) { index = intervals[i][1 ]; } else { result++; } } return result; } }
结果 解答成功: 执行耗时:49 ms,击败了75.94% 的Java用户 内存消耗:93.8 MB,击败了93.16% 的Java用户
分析 时间复杂度: O( n logn )
空间复杂度: O( n )
代码随想录 https://programmercarl.com/0435.%E6%97%A0%E9%87%8D%E5%8F%A0%E5%8C%BA%E9%97%B4.html
思路 相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?
其实都可以。主要就是为了让区间尽可能的重叠。
我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了 。
此时问题就是要求非交叉区间的最大个数。
这里记录非交叉区间的个数还是有技巧的,如图:
区间,1,2,3,4,5,6都按照右边界排好序。
当确定区间 1 和 区间2 重叠后,如何确定是否与 区间3 也重贴呢?
就是取 区间1 和 区间2 右边界的最小值,因为这个最小值之前的部分一定是 区间1 和区间2 的重合部分,如果这个最小值也触达到区间3,那么说明 区间 1,2,3都是重合的。
接下来就是找大于区间1结束位置的区间,是从区间4开始。那有同学问了为什么不从区间5开始?别忘了已经是按照右边界排序的了 。
区间4结束之后,再找到区间6,所以一共记录非交叉区间的个数是三个。
总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。
C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : static bool cmp (const vector<int >& a, const vector<int >& b) { return a[1 ] < b[1 ]; } int eraseOverlapIntervals (vector<vector<int >>& intervals) { if (intervals.size () == 0 ) return 0 ; sort (intervals.begin (), intervals.end (), cmp); int count = 1 ; int end = intervals[0 ][1 ]; for (int i = 1 ; i < intervals.size (); i++) { if (end <= intervals[i][0 ]) { end = intervals[i][1 ]; count++; } } return intervals.size () - count; } };
时间复杂度:O(nlog n) ,有一个快排
空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间
大家此时会发现如此复杂的一个问题,代码实现却这么简单!
补充(1) 左边界排序可不可以呢?
也是可以的,只不过 左边界排序我们就是直接求 重叠的区间,count为记录重叠区间数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : static bool cmp (const vector<int >& a, const vector<int >& b) { return a[0 ] < b[0 ]; } int eraseOverlapIntervals (vector<vector<int >>& intervals) { if (intervals.size () == 0 ) return 0 ; sort (intervals.begin (), intervals.end (), cmp); int count = 0 ; int end = intervals[0 ][1 ]; for (int i = 1 ; i < intervals.size (); i++) { if (intervals[i][0 ] >= end) end = intervals[i][1 ]; else { end = min (end, intervals[i][1 ]); count++; } } return count; }
其实代码还可以精简一下, 用 intervals[i][1] 替代 end变量,只判断 重叠情况就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : static bool cmp (const vector<int >& a, const vector<int >& b) { return a[0 ] < b[0 ]; } int eraseOverlapIntervals (vector<vector<int >>& intervals) { if (intervals.size () == 0 ) return 0 ; sort (intervals.begin (), intervals.end (), cmp); int count = 0 ; for (int i = 1 ; i < intervals.size (); i++) { if (intervals[i][0 ] < intervals[i - 1 ][1 ]) { intervals[i][1 ] = min (intervals[i - 1 ][1 ], intervals[i][1 ]); count++; } } return count; } };
补充(2) 本题其实和 452.用最少数量的箭引爆气球 非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。
把 452.用最少数量的箭引爆气球 代码稍做修改,就可以AC本题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : static bool cmp (const vector<int >& a, const vector<int >& b) { return a[1 ] < b[1 ]; } int eraseOverlapIntervals (vector<vector<int >>& intervals) { if (intervals.size () == 0 ) return 0 ; sort (intervals.begin (), intervals.end (), cmp); int result = 1 ; for (int i = 1 ; i < intervals.size (); i++) { if (intervals[i][0 ] >= intervals[i - 1 ][1 ]) { result++; } else { intervals[i][1 ] = min (intervals[i - 1 ][1 ], intervals[i][1 ]); } } return intervals.size () - result; } };
这里按照 左边界排序,或者按照右边界排序,都可以AC,原理是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : static bool cmp (const vector<int >& a, const vector<int >& b) { return a[0 ] < b[0 ]; } int eraseOverlapIntervals (vector<vector<int >>& intervals) { if (intervals.size () == 0 ) return 0 ; sort (intervals.begin (), intervals.end (), cmp); int result = 1 ; for (int i = 1 ; i < intervals.size (); i++) { if (intervals[i][0 ] >= intervals[i - 1 ][1 ]) { result++; } else { intervals[i][1 ] = min (intervals[i - 1 ][1 ], intervals[i][1 ]); } } return intervals.size () - result; } };
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int eraseOverlapIntervals (int [][] intervals) { Arrays.sort(intervals, (a,b)-> { return Integer.compare(a[0 ],b[0 ]); }); int count = 1 ; for (int i = 1 ;i < intervals.length;i++){ if (intervals[i][0 ] < intervals[i-1 ][1 ]){ intervals[i][1 ] = Math.min(intervals[i - 1 ][1 ], intervals[i][1 ]); continue ; }else { count++; } } return intervals.length - count; } }
按左边排序,不管右边顺序。相交的时候取最小的右边。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution { public int eraseOverlapIntervals (int [][] intervals) { Arrays.sort(intervals, (a,b)-> { return Integer.compare(a[0 ],b[0 ]); }); int remove = 0 ; int pre = intervals[0 ][1 ]; for (int i = 1 ; i < intervals.length; i++) { if (pre > intervals[i][0 ]) { remove++; pre = Math.min(pre, intervals[i][1 ]); } else pre = intervals[i][1 ]; } return remove; } }
763.划分字母区间※ 建议 :
题目链接: https://leetcode.cn/problems/partition-labels/ 文章讲解: https://programmercarl.com/0763.%E5%88%92%E5%88%86%E5%AD%97%E6%AF%8D%E5%8C%BA%E9%97%B4.html 视频讲解:
题目分析
方案一 具体逻辑看程序备注。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public List<Integer> partitionLabels (String s) { List<Character> characterList = new ArrayList <>(26 ); HashMap<Character, Integer[]> hashMap = new HashMap <>(26 ); for (int i = 0 ; i < s.length(); i++) { if (!hashMap.containsKey(s.charAt(i))) { characterList.add(s.charAt(i)); hashMap.put(s.charAt(i), new Integer []{i, i}); } else { hashMap.get(s.charAt(i))[1 ] = i; } } List<Integer> result = new ArrayList <>(); int indexStart = 0 ; int indexEnd = hashMap.get(characterList.get(0 ))[1 ]; for (int i = 1 ; i < characterList.size(); i++) { Integer[] integers = hashMap.get(characterList.get(i)); if (indexEnd > integers[0 ]) { indexEnd = Math.max(integers[1 ], indexEnd); } else { result.add(indexEnd - indexStart + 1 ); indexStart = integers[0 ]; indexEnd = integers[1 ]; } } result.add(indexEnd - indexStart + 1 ); return result; } }
结果 解答成功: 执行耗时:7 ms,击败了25.33% 的Java用户 内存消耗:40.7 MB,击败了8.91% 的Java用户
分析 时间复杂度: O( n )
空间复杂度: O( 26 )
代码随想录 https://programmercarl.com/0763.%E5%88%92%E5%88%86%E5%AD%97%E6%AF%8D%E5%8C%BA%E9%97%B4.html
思路 一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。
题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢?
如果没有接触过这种题目的话,还挺有难度的。
在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了 。此时前面出现过所有字母,最远也就到这个边界了。
可以分为如下两步:
统计每一个字符最后出现的位置
从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
如图:
明白原理之后,代码并不复杂,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : vector<int > partitionLabels (string S) { int hash[27 ] = {0 }; for (int i = 0 ; i < S.size (); i++) { hash[S[i] - 'a' ] = i; } vector<int > result; int left = 0 ; int right = 0 ; for (int i = 0 ; i < S.size (); i++) { right = max (right, hash[S[i] - 'a' ]); if (i == right) { result.push_back (right - left + 1 ); left = i + 1 ; } } return result; } };
时间复杂度:O(n)
空间复杂度:O(1),使用的hash数组是固定大小
总结
这道题目leetcode标记为贪心算法,说实话,我没有感受到贪心,找不出局部最优推出全局最优的过程。就是用最远出现距离模拟了圈字符的行为。
但这道题目的思路是很巧妙的,所以有必要介绍给大家做一做,感受一下。
补充 这里提供一种与 452.用最少数量的箭引爆气球 、435.无重叠区间 相同的思路。
统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是 435.无重叠区间 题目里的输入),将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Solution {public : static bool cmp (vector<int > &a, vector<int > &b) { return a[0 ] < b[0 ]; } vector<vector<int >> countLabels (string s) { vector<vector<int >> hash (26 , vector <int >(2 , INT_MIN)); vector<vector<int >> hash_filter; for (int i = 0 ; i < s.size (); ++i) { if (hash[s[i] - 'a' ][0 ] == INT_MIN) { hash[s[i] - 'a' ][0 ] = i; } hash[s[i] - 'a' ][1 ] = i; } for (int i = 0 ; i < hash.size (); ++i) { if (hash[i][0 ] != INT_MIN) { hash_filter.push_back (hash[i]); } } return hash_filter; } vector<int > partitionLabels (string s) { vector<int > res; vector<vector<int >> hash = countLabels (s); sort (hash.begin (), hash.end (), cmp); int rightBoard = hash[0 ][1 ]; int leftBoard = 0 ; for (int i = 1 ; i < hash.size (); ++i) { if (hash[i][0 ] > rightBoard) { res.push_back (rightBoard - leftBoard + 1 ); leftBoard = hash[i][0 ]; } rightBoard = max (rightBoard, hash[i][1 ]); } res.push_back (rightBoard - leftBoard + 1 ); return res; } };
代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public List<Integer> partitionLabels (String S) { List<Integer> list = new LinkedList <>(); int [] edge = new int [26 ]; char [] chars = S.toCharArray(); for (int i = 0 ; i < chars.length; i++) { edge[chars[i] - 'a' ] = i; } int idx = 0 ; int last = -1 ; for (int i = 0 ; i < chars.length; i++) { idx = Math.max(idx,edge[chars[i] - 'a' ]); if (i == idx) { list.add(i - last); last = i; } } return list; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 class Solution { public int [][] findPartitions(String s) { List<Integer> temp = new ArrayList <>(); int [][] hash = new int [26 ][2 ]; for (int i = 0 ; i < s.length(); i++) { char c = s.charAt(i); if (hash[c - 'a' ][0 ] == 0 ) hash[c - 'a' ][0 ] = i; hash[c - 'a' ][1 ] = i; hash[s.charAt(0 ) - 'a' ][0 ] = 0 ; } List<List<Integer>> h = new LinkedList <>(); for (int i = 0 ; i < 26 ; i++) { temp.clear(); temp.add(hash[i][0 ]); temp.add(hash[i][1 ]); h.add(new ArrayList <>(temp)); } int [][] res = new int [h.size()][2 ]; for (int i = 0 ; i < h.size(); i++) { List<Integer> list = h.get(i); res[i][0 ] = list.get(0 ); res[i][1 ] = list.get(1 ); } return res; } public List<Integer> partitionLabels (String s) { int [][] partitions = findPartitions(s); List<Integer> res = new ArrayList <>(); Arrays.sort(partitions, (o1, o2) -> Integer.compare(o1[0 ], o2[0 ])); int right = partitions[0 ][1 ]; int left = 0 ; for (int i = 0 ; i < partitions.length; i++) { if (partitions[i][0 ] > right) { res.add(right - left + 1 ); left = partitions[i][0 ]; } right = Math.max(right, partitions[i][1 ]); } res.add(right - left + 1 ); return res; } }
56. 合并区间※ 建议 :
题目链接: https://leetcode.cn/problems/merge-intervals/ 文章讲解: https://programmercarl.com/0056.%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4.html 视频讲解:
题目分析
方案一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public int [][] merge(int [][] intervals) { List<Integer[]> resultList = new ArrayList <>(); Arrays.sort(intervals, (o1, o2) -> o1[0 ] == o2[0 ] ? o1[1 ] - o2[1 ] : o1[0 ] - o2[0 ]); int indexStart = intervals[0 ][0 ], indexEnd = intervals[0 ][1 ]; for (int i = 1 ; i < intervals.length; i++) { if (indexEnd < intervals[i][0 ]) { resultList.add(new Integer []{indexStart, indexEnd}); indexStart = intervals[i][0 ]; indexEnd = intervals[i][1 ]; } else { indexEnd = Math.max(indexEnd, intervals[i][1 ]); } } resultList.add(new Integer []{indexStart, indexEnd}); int [][] result = new int [resultList.size()][2 ]; for (int i = 0 ; i < result.length; i++) { result[i][0 ] = resultList.get(i)[0 ]; result[i][1 ] = resultList.get(i)[1 ]; } return result; } }
结果 解答成功: 执行耗时:8 ms,击败了34.20% 的Java用户 内存消耗:44.6 MB,击败了26.85% 的Java用户
分析 时间复杂度: O( n logn )
空间复杂度: O( n )
代码随想录 https://programmercarl.com/0056.%E5%90%88%E5%B9%B6%E5%8C%BA%E9%97%B4.html
思路 本题的本质其实还是判断重叠区间问题。
大家如果认真做题的话,话发现和我们刚刚讲过的 452. 用最少数量的箭引爆气球 和 435. 无重叠区间 都是一个套路。
这几道题都是判断区间重叠,区别就是判断区间重叠后的逻辑,本题是判断区间重贴后要进行区间合并。
所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。
按照左边界从小到大排序之后,如果 intervals[i][0] <= intervals[i - 1][1]
即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=)
这么说有点抽象,看图:(注意图中区间都是按照左边界排序之后了 )
知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢?
其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。
C++代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : vector<vector<int >> merge (vector<vector<int >>& intervals) { vector<vector<int >> result; if (intervals.size () == 0 ) return result; sort (intervals.begin (), intervals.end (), [](const vector<int >& a, const vector<int >& b){return a[0 ] < b[0 ];}); result.push_back (intervals[0 ]); for (int i = 1 ; i < intervals.size (); i++) { if (result.back ()[1 ] >= intervals[i][0 ]) { result.back ()[1 ] = max (result.back ()[1 ], intervals[i][1 ]); } else { result.push_back (intervals[i]); } } return result; } };
时间复杂度: O(nlogn)
空间复杂度: O(logn),排序需要的空间开销
代码实现 # 快速排序 及其时间复杂度和空间复杂度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution { public int [][] merge(int [][] intervals) { List<int []> res = new LinkedList <>(); Arrays.sort(intervals, (x, y) -> Integer.compare(x[0 ], y[0 ])); int start = intervals[0 ][0 ]; int rightmostRightBound = intervals[0 ][1 ]; for (int i = 1 ; i < intervals.length; i++) { if (intervals[i][0 ] > rightmostRightBound) { res.add(new int []{start, rightmostRightBound}); start = intervals[i][0 ]; rightmostRightBound = intervals[i][1 ]; } else { rightmostRightBound = Math.max(rightmostRightBound, intervals[i][1 ]); } } res.add(new int []{start, rightmostRightBound}); return res.toArray(new int [res.size()][]); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int [][] merge(int [][] intervals) { LinkedList<int []> res = new LinkedList <>(); Arrays.sort(intervals, (o1, o2) -> Integer.compare(o1[0 ], o2[0 ])); res.add(intervals[0 ]); for (int i = 1 ; i < intervals.length; i++) { if (intervals[i][0 ] <= res.getLast()[1 ]) { int start = res.getLast()[0 ]; int end = Math.max(intervals[i][1 ], res.getLast()[1 ]); res.removeLast(); res.add(new int []{start, end}); } else { res.add(intervals[i]); } } return res.toArray(new int [res.size()][]); } }