中级算法(7.22-8.21)
数组和字符串
三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = []
输出:[]
示例 3:
输入:nums = [0]
输出:[]
提示:
0 <= nums.length <= 3000
-105 <= nums[i] <= 105
链接:https://leetcode.cn/leetbook/read/top-interview-questions-medium/xvpj16/
我的解法
企图用哈希解法未遂
去重的过程不好处理,有很多小细节
标答
思路:排序+双指针
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums)
{
int size = nums.size();
if (size < 3) return {}; // 特判
vector<vector<int> >res; // 保存结果(所有不重复的三元组)
std::sort(nums.begin(), nums.end());// 排序(默认递增)
for (int i = 0; i < size; i++) // 固定第一个数,转化为求两数之和
{
if (nums[i] > 0) return res; // 第一个数大于 0,后面都是递增正数,不可能相加为零了
// 去重:如果此数已经选取过,跳过
if (i > 0 && nums[i] == nums[i-1]) continue;
// 双指针在nums[i]后面的区间中寻找和为0-nums[i]的另外两个数
int left = i + 1;
int right = size - 1;
while (left < right)
{
if (nums[left] + nums[right] > -nums[i])
right--; // 两数之和太大,右指针左移
else if (nums[left] + nums[right] < -nums[i])
left++; // 两数之和太小,左指针右移
else
{
// 找到一个和为零的三元组,添加到结果中,左右指针内缩,继续寻找
res.push_back(vector<int>{nums[i], nums[left], nums[right]});
left++;
right--;
// 去重:第二个数和第三个数也不重复选取
// 例如:[-4,1,1,1,2,3,3,3], i=0, left=1, right=5
while (left < right && nums[left] == nums[left-1]) left++;
while (left < right && nums[right] == nums[right+1]) right--;
}
}
}
return res;
}
};
根据标答自己写的代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
int n=nums.size();
for(int i=0;i<n;i++)
{
cout<<i<<endl;
if(i>=1&&nums[i]==nums[i-1])
{
continue;
}
int target=-nums[i];
int l=i+1,r=n-1;
while(l<r)
{
if(nums[l]+nums[r]<target)
{
while(l<n-1&&nums[l]==nums[l+1])
l++;
l++;
}
else if(nums[l]+nums[r]>target)
{
while(r>0&&nums[r]==nums[r-1])
r--;
r--;
}
else
{
vector<int> row;
row.push_back(nums[i]);
row.push_back(nums[l]);
row.push_back(nums[r]);
ans.push_back(row);
while(l<n-1&&nums[l]==nums[l+1])
l++;
l++;
while(r>0&&nums[r]==nums[r-1])
r--;
r--;
}
}
}
return ans;
}
};
疑惑解答
1.为什么双指针的单向运行不会使正确答案跳过?
思路:证明每一个正确答案都会被发现
![img](https://i-blog.csdnimg.cn/blog_migrate/4f4de344cda2370c402c924ca0848f81.png)
2.为什么第一层循环可以跳过重复的元素而不会导致漏解
![image-20220722175653073](https://i-blog.csdnimg.cn/blog_migrate/e886d9534a2d21bdb33decfe02e346ee.png)
由于i<j,选j后面双指针的范围是选i的子集,也就是说[s[j],▯,▯]的解一定在[s[i],▯,▯]里
矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
示例 2:
输入:matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]]
输出:[[0,0,0,0],[0,4,5,0],[0,3,1,0]]
提示:
m == matrix.length
n == matrix[0].length
1 <= m, n <= 200
-2^31 <= matrix[i][j] <= 2^31 - 1
进阶:
一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个仅使用常量空间的解决方案吗?
链接:https://leetcode.cn/leetbook/read/top-interview-questions-medium/xvmy42/
我的解法
没有想到比O(m+n)更好的算法了
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m=matrix.size(),n=matrix[0].size();
vector<int> row(n,0);
vector<int> col(m,0);
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(matrix[i][j]==0)
{
col[i]=1;
row[j]=1;
}
}
}
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(col[i]==1||row[j]==1)
{
matrix[i][j]=0;
}
}
}
}
};
官解
一、使用两个标记变量
我们可以用矩阵的第一行和第一列代替方法一中的两个标记数组,以达到 O(1)的额外空间。但这样会导致原数组的第一行和第一列被修改,无法记录它们是否原本包含 0。因此我们需要额外使用两个标记变量分别记录第一行和第一列是否原本包含 0。
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int m = matrix.size();
int n = matrix[0].size();
int flag_col0 = false, flag_row0 = false;
for (int i = 0; i < m; i++) {
if (!matrix[i][0]) {
flag_col0 = true;
}
}
for (int j = 0; j < n; j++) {
if (!matrix[0][j]) {
flag_row0 = true;
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (!matrix[i][j]) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (!matrix[i][0] || !matrix[0][j]) {
matrix[i][j] = 0;
}
}
}
if (flag_col0) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
if (flag_row0) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
}
};
二、使用一个变量标记
我们可以对方法二进一步优化,只使用一个标记变量记录第一列是否原本存在 0。这样,第一列的第一个元素即可以标记第一行是否出现 0。
但为了防止每一列的第一个元素被提前更新,我们需要从最后一行开始,倒序地处理矩阵元素。
官解虽短但不好理解,自己写了一个
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
bool col0=true;
int m=matrix.size(),n=matrix[0].size();
for(int i=0;i<m;i++)
{
if(matrix[i][0]==0)
{
col0=false;
break;
}
}
for(int j=0;j<n;j++)
{
if(matrix[0][j]==0)
{
matrix[0][0]=0;
break;
}
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(matrix[i][j]==0)
{
matrix[i][0]=0;
matrix[0][j]=0;
}
}
}
for(int i=1;i<m;i++)
{
for(int j=1;j<n;j++)
{
if(matrix[i][0]==0||matrix[0][j]==0)
{
matrix[i][j]=0;
}
}
}
if (!matrix[0][0]) //注意:如果先判断col0,matrix[0][0]处的数据会被覆盖
{
for (int j = 0; j < n; j++)
{
matrix[0][j] = 0;
}
}
if (!col0)
{
for (int i = 0; i < m; i++)
{
matrix[i][0] = 0;
}
}
}
};
总结思考
1、当利用常数空间时,想想是不是可以把结构塞到原有结构中
2、正确性证明:
关键:覆盖了的本来也需要改,没覆盖的正好不需要改
[1…n] [1…n]:算法正确性同利用O(m+n)空间算法,唯一要说明的是若本来第一行、第一列为0,则[1…n] [1…n]也要修改(有点外力指定那种意思)
第一行(列):若被上一过程[1…n] [1…n]修改成0了,说明它本来也要被改成0,修改正确!若没有被修改则说明当前列(行)没有0,唯一被修改的可能就是因为第一行内部本来就有的0;若row0标记true,则修改为0,正确!若row0标记false,则不修改,正确!
字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
示例 1:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
示例 2:
输入: strs = [""]
输出: [[""]]
示例 3:
输入: strs = ["a"]
输出: [["a"]]
提示:
- 1 <= strs.length <= 104
- 0 <= strs[i].length <= 100
- strs[i] 仅包含小写字母
我的解法
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> ans;
vector<string> reorder(strs);
int n=strs.size();
for(int i=0;i<n;i++)
{
sort(reorder[i].begin(),reorder[i].end());
}
quicksort(strs,reorder,0,n-1);
for(int i=0;i<n;i++)
{
if(i!=0&&reorder[i-1]==reorder[i])
{
ans[ans.size()-1].push_back(strs[i]);
}
else
{
vector<string> temp;
temp.push_back(strs[i]);
ans.push_back(temp);
}
}
return ans;
}
void quicksort(vector<string>& strs,vector<string>& reorder,int l,int r)
{
if(l>=r)
{
return;
}
int pos=partition(strs,reorder,l,r);
quicksort(strs,reorder,l,pos-1);
quicksort(strs,reorder,pos+1,r);
}
int partition(vector<string>& strs,vector<string>& reorder,int l,int r)
{
string pivot=reorder[l];
int j=l;
for(int i=l+1;i<=r;i++)
{
if(reorder[i]<pivot)
{
swap(reorder[i],reorder[++j]);
swap(strs[i],strs[j]);
}
}
swap(reorder[l],reorder[j]);
swap(strs[l],strs[j]);
return j;
}
};
官解
两个字符串互为字母异位词,当且仅当两个字符串包含的字母相同。同一组字母异位词中的字符串具备相同点,可以使用相同点作为一组字母异位词的标志,使用哈希表存储每一组字母异位词,哈希表的键为一组字母异位词的标志,哈希表的值为一组字母异位词列表。
遍历每个字符串,对于每个字符串,得到该字符串所在的一组字母异位词的标志,将当前字符串加入该组字母异位词的列表中。遍历全部字符串之后,哈希表中的每个键值对即为一组字母异位词。
以下的两种方法分别使用排序和计数作为哈希表的键。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
for (string& str: strs) {
string key = str;
sort(key.begin(), key.end());
mp[key].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
auto arrayHash = [fn = hash<int>{}](const array<int, 26>& arr)->size_t {
/*
* accumulate()中的第四个参数项要传入一个二元操作(BinaryOperation)规则,告诉它如何将当前元素与累积量做操作
* 它隐式地调用(size_t)acc和(int)num这两个量,默认情况下做简单的相加运算。
*/
return accumulate(arr.begin(), arr.end(), (size_t)0, [&](size_t acc, int num) {