C++ : 力扣_Top(42-56)
文章目录
42、接雨水(困难)
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例: 输入: [0,1,0,2,1,0,1,3,2,1,2,1] 输出: 6
// 动态规划法
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size(); // n 容器长度
if(n==0||n==1) return 0;
int left_max[n]; left_max[0] = height[0]; // 存放当前下标左侧的最高高度
int right_max[n]; right_max[n-1] = height[n-1]; // 存放当前下标右侧的最高高度
int max = 0;
int sum = 0;
for(int i=1; i<n; ++i){ // 先将left_max数组赋值
if(max<height[i-1]){
max = height[i-1];
}
left_max[i] = max;
}
max = 0;
for(int i=n-2; i>=0; --i){ // 再将right_max数组赋值
if(max<height[i+1]){
max = height[i+1];
}
right_max[i] = max;
}
for(int i=1; i<n-1; ++i){ // 计算除了左右两侧之外所有节点头顶上的水体积
int v = my_min(left_max[i], right_max[i]) - height[i];
if(v>0){ // 如果当前节点的左右最高点的较低值大于该节点高度
sum += v;
}
}
return sum;
}
int my_min(int a, int b){
return a < b ? a : b;
}
};
// 双指针法(无额外内存使用)
class Solution {
public:
int trap(vector<int>& height) {
vector<int>::size_type n = height.size();
if(n==0||n==1) return 0;
size_t left = 1; // 定义左指针
size_t right = n-2; // 定义右指针
size_t sum = 0;
int left_max = height[0], right_max = height[n-1]; // 定义左右两堵墙
while(left <= right){ // 两个指针向中间移动遍历
if(left_max<right_max){ // 1、右边的墙更高,计算左指针这边的容量
if(height[left]<left_max){ // 如果有存水条件出现
sum += left_max - height[left];
}
left_max = max(left_max, height[left]); // 更新左侧墙
++left;
}
else{ // 2、左边的墙更高,计算右指针这里的容量
if(height[right]<right_max){ // 如果有存水条件出现
sum += right_max - height[right];
}
right_max = max(right_max, height[right]); // 更新右侧墙
--right;
}
}
return sum;
}
};
思路:首先需要明确,该题的阶梯思路较多,比如一行一行计算体积、一列一列计算体积,但计算复杂度都较高;比较好的思路是:一列一列计算体积,当前节点处可以接到的水的体积等于它左侧和右侧的最高值的较小值,再减去该节点的高度;但如果每遍历到一个节点都要向左向右循环查找最大值,计算复杂度为O(n^2); 故可利用动态规划思想:提前建立两个数组,分别保存当前节点i的左侧最大值节点和右侧最大值节点;但空间复杂度为O(n);更好的方法是双指针法:定义左右两侧的指针,向中间移动;移动哪一方根据设定的左右最高墙的较低值来确定,如果 left_max > right_max,则左边的最高值高,则右侧只要是低于right_max的地方肯定能存水,而且存水量由right_max决定;则更新右边的指针,如果出现了存水条件(height[right]<right_max),则计算当前指针处的存水体积;左右来回移动,累积结果;
44、通配符匹配(困难)
给定一个字符串 (s) 和一个字符模式 § ,实现一个支持 ‘?’ 和 ‘’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
'’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
输入:
s = “aa”
p = “a”
输出: false
输入:
s = “aa”
p = “*”
输出: true
输入:
s = “cb”
p = “?a”
输出: false
输入:
s = “adceb”
p = “ab”
输出: true
输入:
s = “acdcb”
p = “a*c?b”
输入: false
// 动态规划法
class Solution {
public:
bool isMatch(string s, string p) {
// 为空串的判定融合在bp的初始化中,故不用另外判定
int slen = s.size(), plen = p.size();
bool bp[slen+1][plen+1]; // 动态数组,表示s中前i个和p中前j个是否匹配
bp[0][0] = true; // 当s和p都为空,则匹配
for(int i=1; i<=slen; ++i){ // 当p为空,而s不为空时,肯定不匹配
bp[i][0] = false;
}
for(int j=1; j<=plen; ++j){
bp[0][j] = (bp[0][j-1] && p[j-1] == '*'); // 当s为空,p有值但只可以有任意的'*';
}
for(int i=1; i<=slen; ++i){ // 循环遍历bp,开始匹配
for(int j=1; j<=plen; ++j){ // i为s下标,j为p下标
if(s[i-1]==p[j-1]||p[j-1]=='?'){ // 当当前字符上两者相等或p[j]为'?'
bp[i][j] = bp[i-1][j-1];
}
else if(p[j-1]=='*'){ // 如果p[j]为'*',则当前有两种情况:
// 前者表示*为空串,可以忽略 || 后者表示*匹配上了当前的一个字符
bp[i][j] = bp[i-1][j] || bp[i][j-1];
}
else{ // 如果都不匹配
bp[i][j] = false;
}
}
}
return bp[slen][plen];
}
};
// 双指针贪心算法
class Solution {
public:
bool isMatch(string s, string p) {
int i_tmp = -1, j_tmp = -1; // 匹配回溯标志
int i = 0, j = 0; // s和p的下标
while(i<s.size()){
if(s[i]==p[j]||p[j]=='?'){ // 当前匹配
++i; ++j;
}
else if(p[j]=='*'){ // 遇到*号,记录如果之后序列匹配不成功时,i和j需要回溯到的位置
i_tmp = i; j_tmp = j; //记录星号
++j; //记录星号 并且j移到下一位 准备下个循环s[i]和p[j]的匹配
}
else if(i_tmp>=0){ // 当前不匹配,但是i_tmp>=0,说明之前有*,回溯回*号处
// 发现字符不匹配且没有星号出现 但是istar>0 说明可能是*匹配的字符数量不对,这时回溯
i = i_tmp; j = j_tmp + 1;
++i; ++i_tmp;
// i回溯到istar+1
// 因为上次从s串istar开始对*的尝试匹配已经被证明是不成功的(不然不会落入此分支)
// 所以需要从istar+1再开始试 同时inc istar 更新回溯位置
// j回溯到jstar+1
// 重新使用p串*后的部分开始对s串istar(这个istar在上一行已经inc过了)位置及之后字符的匹配
}
else{ // 再有其他情况,直接返回错误
return false;
}
} // 当s中字符匹配完,p中字符不能有除星号以外字符
while(j<p.size()&&p[j]=='*') ++j; // 清除掉p尾端的*号
return j == p.size();
}
};
思路:正则表达式匹配问题一般都较为复杂;提供了两种做法;首先是动态规划法,建立一个bp(s.size()+1,p.size()+1)的状态矩阵,存放当前s的前i个字符和p的前j个字符匹不匹配的结果;初始化bp边缘后推导状态转移方程:当当前s和p的下标处相等时或p中为’?‘时,匹配当前字符,bp[i][j]=bp[i-1][j-1]; 当p中为’'时,则分两种情况:bp[i][j] = bp[i-1][j] || bp[i][j-1];前者表示当前匹配了空串,后者表示匹配上了当前的一个字符,并把回溯到之前;除了上述情况外,其他情况bp节点都为false;求出bp矩阵的右下角结果即为答案,注意bp矩阵的下标问题;
另一种方法是双指针贪心算法,利用*处的标记实现匹配不成功后的匹配,没有使用额外空间,但比较难想,具体思路见注释;建议主要掌握动态规划方法;
46、全排列(中等)
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
class Solution {
vector<vector<int> > result;
public:
vector<vector<int>> permute(vector<int>& nums) {
if(nums.empty()) return result;
FindAllPermutation(nums, 0, nums.size()-1);
return result;
} // now是当前处理到的下标,len是数组总长度
void FindAllPermutation(vector<int>& nums, int now, int len){
if(now==len){
result.push_back(nums);
return;
}
for(int i=now; i<=len; ++i){ // 核心部分
swap(nums[now],nums[i]); // 直接在nums上处理就行,交换now和now之后的每个值;
FindAllPermutation(nums, now+1, len);
swap(nums[now],nums[i]); // 交换回来
}
}
};
思路:典型的全排列问题,记住处理的经典方法和代码即可,每次递归处理当前的一个元素。数组的一个全排列为:第一个元素+后面的元素 和 后面的元素+第一个元素;
48、旋转图像(中等)
给定一个 n × n 的二维矩阵表示一个图像。将图像顺时针旋转 90 度。
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
给定 matrix =
[1,2,3],
[4,5,6],
[7,8,9]
原地旋转输入矩阵,使其变为:
[7,4,1],
[8,5,2],
[9,6,3]
// 自外而内旋转法
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
if(!matrix.empty()&&matrix.size()==matrix[0].size()){
int size = matrix.size();
int temp, temp_i;
for(int i=0; i<size/2; ++i){ // 每圈循环
for(int j=i; j<size-i-1; ++j){ // 每圈的点循环,当前点坐标i,j
temp = matrix[i][j]; // 倒序的循环赋值过程,下标细节需要多注意!
matrix[i][j] = matrix[size-1-j][i];
matrix[size-1-j][i] = matrix[size-1-i][size-1-j];
matrix[size-1-i][size-1-j] = matrix[j][size-1-i];
matrix[j][size-1-i] = temp;
}
}
}
}
};
思路:上面的方法是按照每圈、每四个点一组进行旋转的方法,每四个旋转的点可以连成一个正方体,其坐标分别为比如: 01 14 43 30 01 … 旋转时尽量倒序赋值,即01->temp,30->01,43->30,14->43,temp->14,这样不用建立过多额外的临时变量;另外一个更好的方法是先将矩阵进行转置,然后再将每一行进行反转,即可求得旋转矩阵,这种方法非常简单,如下:
// 两次旋转法
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
if(!matrix.empty()&&matrix.size()==matrix[0].size()){
int size = matrix.size();
for(int i=0; i<size; ++i){ // 转置
for(int j=i; j<size; ++j){
swap(matrix[i][j], matrix[j][i]);
}
}
for(int i=0; i<size; ++i){ // 对每行进行逆序
reverse(matrix[i].begin(), matrix[i].end());
}
}
}
};
49、字母异位词分组(中等)
给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
输入: [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”],
输出:
[“ate”,“eat”,“tea”],
[“nat”,“tan”],
[“bat”]
说明:所有输入均为小写字母。不考虑答案输出的顺序。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string> > result;
if(strs.empty()) return result;
unordered_map<string, int> m; // unordered_map 哈希表
// key为排序后的字符串,value为这个组合在result中的下标;
string temp;
int num = 0; // 用来当做result中的下标(第几个组合)
for(int i=0; i<strs.size(); ++i){
temp = strs[i];
sort(temp.begin(),temp.end()); // 排序当前字符串
if(m.find(temp)==m.end()){ // 哈希表中还没有这个字符组合
m.insert(pair<string,int>(temp,num++)); // 插入这个组合,result下标加1
result.push_back( {strs[i]} ); // result中插入一个vactor<string>
}
else{ // 哈希表中已有这个组合,m[temp]表示value,即result中的下标
result[ m[temp] ].push_back(strs[i]);
}
}
return result;
}
};
思路:一道不太难但做起来很麻烦的题,上述是比较巧妙的做法,首先判断两个字符是不是字母异位词的直接方法就是排序,然后判断是否相等;但排序和判断是否相等都需要额外的计算时间;这里利用unordered_map<string,int>巧妙地将两个string的比较部分换为在map中的key查找操作;并且map中的value用来存储当前的是第几个组合。方法过程:遍历strs,对每一个string进行排序,然后再map中寻找这个组合,如果没找到,就将其加入map,并把value设定为自增量0,1,2,3…如果找到了这个组合,就将其按照在map中对应组合的value当做result中的下标,push_back到相应的vector中;利用了一个map将查找组合和下标操作进行了巧妙融合,使算法只需要遍历一遍就能解决问题;
50、Pow(x,n)(中等)
实现 pow(x, n) ,即计算 x 的 n 次幂函数。
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231,231−1]
输入: 2.00000, 10
输出: 1024.00000
输入: 2.10000, 3
输出: 9.26100
输入: 2.00000, -2
输出: 0.25000
解释: 2^-2 = 1/2^2 = 1/4 = 0.25
class Solution {
bool InvalidInput = false;
public:
double myPow(double x, int n) {
if(x>100||x<-100){ // x越界时
InvalidInput = true;
return 0;
} // 先不管n的正负,最后根据n的正负选择返回值
if(n<0) return 1/myPow2(x, n); // 指数为负的情况
else return myPow2(x, n); // 指数为正的情况
}
double myPow2(double x, int n){
if(n==0) return 1;
if(n==1||n==-1) return x;
// 不能写 n = -n; 小心当n=INT_MIN时,会有越界!!
// 注意不能写成下面这种形式!!相当于还是算了两次递归!!
// double re = myPow(x, n/2) * myPow(x, n/2);
double re = myPow2(x, n/2); // 二分递归求解
if(n%2==0) return re * re; // 为偶数
else return re * re * x; // 为奇数
}
};
// 将递归改为循环,但稍微难以理解
class Solution {
bool InvalidInput = false;
public:
double myPow(double x, int n) {
if(x<-100||x>100){
InvalidInput = true;
return 0;
}
if(n==0) return 1;
double temp = x, result = 1;
for(int i=n; i>=1||i<=-1; i/=2){ // 同时处理指数为正或指数为负
if(i%2==1||i%2==-1){ // 只有在i为奇数时更新result的值,原理类似于二进制规则
result = result * temp; // 也可以根据规律理解
}
temp = temp * temp;
}
if(n>0) return result;
else return 1/result;
}
};
// 一个非常棒的递归写法
class Solution {
public:
double myPow(double x, int n) {
if(n==0) return 1;
if(n==1) return x;
if(n==-1) return 1/x; // 将指数为负的情况放在递归终止条件中处理
double result = myPow(x, n/2);
double left = myPow(x, n%2); // n如果为奇数,则还需要乘上一个x
return result * result * left;
}
};
思路:二分快速幂的思想,n为偶数时,x^n = (x^n/2) × (xn/2),n为奇数时,xn = (x^n/2) × (x^n/2) × x; 递归停止的条件是 n==1 时;其中该题有非常注意的点,比如递归代码不可以写成 double re = myPow(x, n/2) * myPow(x, n/2); 这相当于还是算了两次递归,计算复杂度没有减少;其次需要注意指数为正、指数为负的情况,其中不能把负数n变成正数求解,因为可能越界。好方法是将指数的正负暂时忽略,在最后返回时判断是返回结果还是他的倒数;
53、最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.empty()) return 0;
// temp表示以当前节点为结尾的子串的最大和
int result = INT_MIN, temp = 0;
for(int i=0; i<nums.size(); ++i){
temp += nums[i];
result = result > temp ? result : temp;
if(temp<0){ // 贪心思路,前面子串的最大和都小于零,说明这一段都没用处
temp = 0;
}
}
return result;
}
};
思路:与剑指offer中的那道题一致i,使用贪心思想,一次遍历完成;用一个变量temp表示以当前节点为结尾的子串的最大和,当前面子串的最大和都小于零,说明这一段都没用处;
54、螺旋矩阵(中等)
给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
输入:[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ] ]
输出: [1,2,3,6,9,8,7,4,5]
输入:[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12] ]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> result;
if(!matrix.empty()){
int up = 0, down = matrix.size()-1; // 定义上下左边界
int left = 0, right = matrix[0].size()-1; // 定义左右边界
while(true){
for(int i=left;i<=right;++i) result.push_back( matrix[up][i] );
if(++up>down) break;
for(int i=up;i<=down;++i) result.push_back( matrix[i][right] );
if(--right<left) break;
for(int i=right;i>=left;--i) result.push_back( matrix[down][i] );
if(--down<up) break;
for(int i=down;i>=up;--i) result.push_back( matrix[i][left] );
if(++left>right) break;
}
}
return result;
}
};
思路:通过设定四个边界数值来使问题简单化:首先设定上下左右边界;其次向右移动到最右,此时第一行因为已经使用过了,可以将其从图中删去,体现在代码中就是重新定义上边界;判断若重新定义后,上下边界交错,表明螺旋矩阵遍历结束,跳出循环,返回答案;若上下边界不交错,则遍历还未结束,接着向下向左向上移动,操作过程与第一,二步同理;不断循环以上步骤,直到某两条边界交错,跳出循环,返回答案.
55、跳跃游戏(中等)
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
输入: [2,3,1,1,4]
输出: true
解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。
输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。
class Solution {
public:
bool canJump(vector<int>& nums){
int maxlen = 0; // 当前能跳到的最远下标
for(int i=0; i<nums.size(); ++i){
if(i>maxlen) return false; // 当前遍历的下标超过了之前能跳到的最远下标处
maxlen = nums[i]+i > maxlen ? nums[i]+i : maxlen; // 在当前节点更新maxlen
if(maxlen>=nums.size()) return true;
}
return true;
}
};
思路:一道比较巧妙的题,最开始的想法是利用一个队列存储每个节点能跳到的节点,然后从队列中依次判断,但每次判断都要循环这个下标处值的大小次,会超时;巧妙的办法是设定一个当前能够跳到的最远下标变量,然后只循环一次数组,唯一可以使数组达不到最后节点的情况就是当前遍历的节点下标超过了设定的之前节点能够达到的最远下标,具体见程序;
56、合并区间(中等)
给出一个区间的集合,请合并所有重叠的区间。
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
// 双指针法
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int> > result;
vector<int>::size_type size = intervals.size();
if(size==0||size==1) return intervals; // 当原矩阵为空或只有一个值,直接返回原矩阵
vector<int>::size_type left=0, right=1; // 定义双指针,left指向前者,right指向后者
sort(intervals.begin(), intervals.end(), // 对interval按每个vector的第一个元素排序
[](vector<int>& a,vector<int>& b){return a[0]<b[0];} ); // 此处lambda表达式也可以不写
while(right<size){
if(intervals[left][1]<intervals[right][0]){ // 如[1,3][4,5]
result.push_back(intervals[left]); // 推入[1,3]
left = right; continue; // 将left移动到right的位置
}
if(intervals[left][1]>=intervals[right][0]){ // 如[1,3][2,2]或[1,3][2,4]
if(intervals[left][1]>=intervals[right][1]){ // 如[1,3][2,2]
++right; // 直接忽略[2,2],right右移
}
else{ // 如[1,3][2,4], 变为[1,4][2,4],然后忽略[2,4],right指针右移
intervals[left][1] = intervals[right][1]; // [1,3][3,4]->[1,4][3,4]
++right; // 右指针继续移动,左指针不变
}
if(right>=size){ // 当right到头时,把最后一个left推入结果中
result.push_back(intervals[left]);
}
}
}
return result;
}
};
思路:一道比较麻烦的题,没有太多技巧,关键在于分清情况;首先为了简化问题,可以先将原矩阵按照每个vector中的第一个元素进行排序,这样就不用考虑每个区间的左侧部分了,然后定义双指针,再区分为三种情况: left和right分别指向[1,3][4,5]时,直接将left指向的部分推入结果,然后将left移到right的位置;或者是[1,3][2,2],前者包含了后者,则直接忽略后者即可,将right后移;或者[1,3][2,4],这时将left指向的区间进行扩容,变成[1,4],然后忽略[2,4],将right后移;