nSum问题
nSum问题就是给定一个整数数组 nums
和一个
target
,要求找到 n
个nums
中的元素,使得这n
个元素的和等于
target
,要求返回所有可能的不重复的情况。
【2Sum】问题是这类的基本情况,将数组排序后使用双指针即可解决问题。对于\(n >
2\) 的情况,尤其是n
比较大时,直接解决是比较困难的,可以采用==递归==解决。下面给出解决【nSum】问题的代码。
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 vector<vector<int >> nSum (vector<int >& nums, int start, int k, int target) { int n = nums.size (); vector<vector<int >> res; if (k == 2 ){ int i = start; int j = n - 1 ; while (i < j){ int left = nums[i]; int right = nums[j]; int sum = left + right; if (sum == target){ res.push ({left, right}); while (i < j && nums[i] == left) i++; while (i < j && nums[j] == right) j--; } else if (sum > target){ while (i < j && nums[j] == right) j--; } else { while (i < j && nums[i] == left) i++; } } } else { int i = start; while (i < n){ int val = nums[i]; int sub_target = target - val; vector<vector<int >> sub_res = nSum (nums, i+1 , k-1 , sub_target); for (vector<int > sub_res_i : sub_res){ sub_res_i.push_back (val); res.push_back (sub_res_i); } while (i < n && nums[i] == val) i++; } } return res; }
区间问题
区间问题就是给定一些区间,然后需要求解这些区间的交集、或者合并区间等问题
解决这类问题最核心的思想就是==排序==+==讨论==。
针对不同的问题,需要进行不同的排序,可能是对起点排序,也可能是对终点排序,有可能是升序,也有可能是降序。
1 2 3 4 5 6 7 static bool cmp (vector<int >& a, vector<int >& b) { if (a[0 ] != b[0 ]) return a[0 ] < b[0 ]; else return a[1 ] > b[1 ]; }sort (nums.begin (), nums.end (), cmp);
题目列表:
452.
用最少数量的箭引爆气球
435.
无重叠区间
1288.
删除被覆盖区间
56.
合并区间
986.
区间列表的交集
1024.
视频拼接
一道别致的区间问题!这道题在对区间进行排序之后的操作更为复杂一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int videoStitching (vector<vector<int >>& clips, int time) { sort (clips.begin (), clips.end (), cmp); int n = clips.size (); int i = 0 ; int end = 0 ; int cnt = 0 ; while (i < n){ int max = -1 ; while (i < n && clips[i][0 ] <= end){ if (clips[i][1 ] > max) max = clips[i][1 ]; i++; } if (max == -1 ) return -1 ; end = max; cnt ++; if (end >= time) return cnt; } return -1 ; }
快速选择算法
215.
数组中的第K个最大元素
快速排序算法
快排(quick-sort)是一种经典的排序算法,和归并排序(merge-sort)一样都是
divide-and-conquer 类型的算法,平均时间复杂度为 \(O(NlogN)\)
【思路】
快排的思路是,要对nums[lo...hi]
排序,先找到一个分界点pivot
,然后使得nums[lo...pivot-1]
都小于nums[pivot]
,nums[pivot+1...hi]
都大于nums[pivot]
,然后递归地在nums[lo...pivot-1]
和nums[pivot+1...hi]
上执行相同的操作。
【实现过程】
首先可以轻易写出下面两个函数,最主要的难点是实现partition
函数。
1 2 3 4 5 6 7 8 9 10 void quick_sort (vector<int >& nums) { quick_sort_helper (nums, 0 , nums.size () - 1 ); }void quick_sort_helper (vector<int >& nums, int i, int j) { if (i >= j) return ; int pivot = partition (nums, i, j); quick_sort_helper (nums, i, pivot-1 ); quick_sort_helper (nums, pivot+1 , j); }
partition
函数所做的工作就是,对一个数组nums[lo...hi]
,返回一个下标pivot
,使得数组nums[lo...pivot-1]
部分的元素全部都小于nums[pivot]
,数组nums[pivot+1...hi]
部分的元素全部都大于nums[pivot]
。
首先任意选取一个值作为pivot,比如选择最后一个元素`
smaller_idx
变量是用来记录所有<=nums[pivot]
的元素的下标
遍历数组,当遇到<=nums[pivot]
的元素时,就要把它放在smaller_idx
指定的位置上(for循环中if块中的内容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int partition (vector<int >& nums, int lo, int hi) { int pivot = nums[hi]; int smaller_idx = lo-1 ; for (int i = lo; i <= hi; i++){ if (nums[i] <= pivot){ smaller_idx ++; swap (nums, smaller_idx, i); } } return smaller_idx; }void swap (vector<int >& nums, int i, int j) { int tmp = nums[i]; nums[i] = nums[j]; nums[j] = tmp; }
quick-sort
上面的partition
函数返回的pivot
其实就是数组的第pivot
大元素,而问题需要的是第k
大元素,所以可以借用【二分搜索】思想进行求解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int findKthLargest (vector<int >& nums, int k) { int lo = 0 ; int hi = nums.size () - 1 ; int idx = nums.size () - k; while (lo <= hi){ int pi = partition (nums, lo, hi); if (idx > pi){ lo = pi+1 ; } else if (idx < pi){ hi = pi-1 ; } else { return nums[pi]; } } return -1 ; }
二叉堆
这道题最简单的还是采用二叉堆(优先队列)的解法
1 2 3 4 5 6 7 8 9 10 int findKthLargest (vector<int >& nums, int k) { priority_queue<int , vector<int >, std::greater<int >> pq; for (int i : nums){ pq.push (i); if (pq.size () > k){ pq.pop (); } } return pq.top (); }
priority_queue
数据结构自定义比较如何书写?
1、首先优先队列默认情况下是使用less比较,即大顶堆(最大元素作为堆顶出现)。如果想要改成小顶堆,需要使用greater比较。
2、如果要自定义优先队列的比较,可以在一个结构体 里==重载()运算符==
1 2 3 4 5 struct cmp { bool operator () (vector<int >& a, vector<int >& b) { } };
3、或者可以重载复合数据类型的比较运算符(改变原本less和greater中比较运算符的含义)
sort函数中的自定义比较应该如何书写?
1、默认情况下按照升序对容器内元素进行排序。
2、一般情况下只要重写一个比较函数传入sort函数即可
1 2 3 4 5 bool cmp (vector<int >& a, vector<int >& b) { }sort (container.begin (), container.end (), cmp);
但是如果要在类中使用,自定义的比较函数一定得是静态成员函数或者全局函数
因为std::sort
函数是全局的,不能依赖于只有创建实例后才能使用的普通成员函数
运算优先级:分治算法
241.
为运算表达式设计优先级
使用递归思想解决问题,一定要从整体出发考虑问题的结构,而不能局限于某个细节。将大问题不断分解成规模更小的子问题,当规模小到一定程度时就可以轻易求解,然后再将小规模问题的答案合并,得到最终问题的结果,这就是分治的思想。
这道题也是一样,给定一个表达式字符串,要求出该表达式所有可能的结果。无论如何改变运算符优先级,或者说增加括号,本质上表达式的计算都可以回到【操作数1
操作符
操作数2】这种形式上,只是这个操作数可以又是一个表达式,以此来增加复杂度。
如何分解复杂度呢?可以选定一个操作符,然后递归处理两边的表达式。
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 vector<int > diffWaysToCompute (string expression) { vector<int > res; int n = expression.size (); for (int i = 0 ; i < n; i++){ char c = expression[i]; if (c == '+' || c == '-' || c == '*' ){ vector<int > left = diffWaysToCompute (expression.substr (0 , i)); vector<int > right = diffWaysToCompute (expression.substr (i+1 , n - i)); for (int op1 : left){ for (int op2 : right){ switch (c){ case '+' : res.push_back (op1+op2); break ; case '-' : res.push_back (op1-op2); break ; case '*' : res.push_back (op1*op2); break ; } } } } } if (res.empty ()){ int val = 0 ; for (int i = 0 ; i < n; i ++){ val = 10 * val + expression[i] - '0' ; } res.push_back (val); } return res; }
贪心算法
加油站
134.
加油站
看到问题的一个反应就是把两个数组变成一个数组diff
,diff[i]
表示在i
加油站加完油开到i+1
加油站的油量变化。那么要找到那个点,应该尽可能的让车在刚开始一段时间油量尽可能多,这样才能保证最后可以走完一圈。可以将diff
的值标在图上,连起来之后最低点的下一个点就应该是出发的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int canCompleteCircuit (vector<int >& gas, vector<int >& cost) { int n = gas.size (); vector<int > diff; int sum = 0 ; for (int i = 0 ; i < n; i++){ int onediff = gas[i] - cost[i]; diff.push_back (onediff); sum += onediff; } if (sum < 0 ) return -1 ; int total = 0 ; int res = 0 ; int min = 10000 ; for (int i = 0 ; i < n; i ++){ total += diff[i]; if (total < min){ min = total; res = i; } } return (res+1 )%n; }
换一个思路,这道题更为普遍的想法应该是用一个双重循环去枚举所有的情况。可以增加一个trick
:当发现从i
开到j
不能到达时,那么i
到j
中间的任何一个点出发都行不通 。这样就避免了很多重复的情况,复杂度可以达到线性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int canCompleteCircuit (vector<int >& gas, vector<int >& cost) { int n = gas.size (); vector<int > diff; int sum = 0 ; for (int i = 0 ; i < n; i++){ int onediff = gas[i] - cost[i]; diff.push_back (onediff); sum += onediff; } if (sum < 0 ) return -1 ; for (int i = 0 ; i < n; i++){ if (diff[i] < 0 ) continue ; int j = (i+1 )%n; int total = diff[i]; for (; j != i; j = (j+1 )%n){ total += diff[j]; if (total < 0 ) break ; } if (j == i) return i; i = (j + n - 1 ) % n; } return -1 ; }
分割数组为连续子序列
659.
分割数组为连续子序列
贪心算法:要想满足上述条件,应该使得每个子序列尽可能地长,即尽可能不要建造新的子序列 。
所以当遍历到nums
中x
时,判断是否有以x-1
结尾的子序列,如果有:将x
加在子序列的后面;如果没有:看x+1
和x+2
这两个数字是否还有,如果还有,则建造一个以x+2
结尾的子序列,如果没有,则返回false
。
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 bool isPossible (vector<int >& nums) { unordered_map<int , int > cnt; unordered_map<int , int > endx; for (int num : nums) cnt[num] ++; for (int x : nums) { if (cnt[x] > 0 ) { if (endx[x-1 ] > 0 ) { endx[x-1 ] --; endx[x] ++; cnt[x] --; } else { if (cnt[x+1 ] > 0 && cnt[x+2 ] > 0 ) { endx[x+2 ]++; cnt[x]--; cnt[x+1 ]--; cnt[x+2 ]--; } else { return false ; } } } } return true ; }
计算器
224.
基本计算器
227.
基本计算器 II
给定一个字符串,包含数字、四则运算符、括号以及空格,要求计算该表达式的值
可以将这个问题进行分解,比如:如何将字符串中的数提取出来、如何处理四则运算以及如何处理括号。
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 int calculate (string s) { stack<int > stk; int n = s.size (); int i = 0 ; int val = 0 ; char sign = '+' ; while (i < n){ if (s[i] >= '0' && s[i] <= '9' ){ val = 10 * val + (s[i] - '0' ); } if (s[i] == '+' || s[i] == '-' || s[i] == '*' || s[i] == '/' || i == n-1 ){ switch (s[i]){ case '+' : stk.push (val); break ; case '-' : stk.push (-1 * val); break ; case '*' : val = stk.top () * val; stk.pop (); stk.push (val); break ; case '/' : val = stk.top () / val; stk.pop (); stk.push (val); break ; } val = 0 ; sign = s[i]; } if (s[i] == '(' ){ int start = i + 1 ; int cnt = 1 ; while (cnt != 0 ){ i ++; if (s[i] == '(' ) cnt ++; if (s[i] == ')' ) cnt --; } val = calculate (s.substr (start, i - start)); } i ++; } int res = 0 ; while (!stk.empty ()){ res += stk.top (); stk.pop (); } return }
完美矩形
391.
完美矩形
精确覆盖 的意思是:这些小矩形可以恰好组成一个大矩形,中间不能有重叠,也不能有空隙。
首先,很明显可以从面积 出发。找到组成的大矩形的顶点,算出大矩形的面积,如果小矩形面积之和不等于大矩形面积的话,肯定实现不了。
其次,尽管算出来面积是相等的,也不能保证就可以恰好组成一个大矩形。可以从顶点 出发,如果可以恰好围成一个大矩形,那么在所有小矩形的顶点中,有且只有4个顶点只出现一次(围成大矩形的四个顶点/角),其他顶点一定只出现2次或者4次。
由下图可以看出,只有当某个顶点只出现奇数次才会作为围成图形的角 。很明显,要想恰好围成大矩形,必须有其只有4个顶点符合情况一,且不能出现情况三。
图片来源:《labuladong算法秘籍》
注意:
找到的大矩形顶点只是理论上的:就是说给定这些矩形,所可以围成的大矩形的顶点理论上是这个值。
考虑顶点的时候,除了判断只有4个顶点出现1次,还需要判断这4个顶点是否就是理论上的那4个顶点。
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 typedef pair<int , int > p;bool isRectangleCover (vector<vector<int >>& rectangles) { map<p, int > points; long long X1 = INT_MAX, Y1 = INT_MAX, X2 = INT_MIN, Y2 = INT_MIN; long long areaSum = 0 ; for (vector<int > rectangle : rectangles) { long long x1 = rectangle[0 ], y1 = rectangle[1 ], x2 = rectangle[2 ], y2 = rectangle[3 ]; X1 = min (X1, x1); Y1 = min (Y1, y1); X2 = max (X2, x2); Y2 = max (Y2, y2); areaSum += (x2 - x1) * (y2 - y1); p p1 = {x1, y1}, p2 = {x1, y2}, p3 = {x2, y1}, p4 = {x2, y2}; points[p1]++;points[p2]++;points[p3]++;points[p4]++; } long long bigArea = (X2 - X1) * (Y2 - Y1); if (areaSum != bigArea) return false ; int v_cnt = 0 ; for (const auto & p0 : points){ if (p0.second % 2 == 1 ) v_cnt++; } if (v_cnt != 4 ) return false ; p p1 = {X1, Y1}, p2 = {X1, Y2}, p3 = {X2, Y1}, p4 = {X2, Y2}; return points[p1]==1 && points[p2]==1 && points[p3]==1 && points[p4]==1 ; }
这里使用的是map
而不是一般的unordered_map
,原因是在unordered_map
是使用hash
来计算key
,但是hash
不能处理pair
类型数据,所以不能在unordered_map
中使用pair
数据类型。
参考文章