1. 1-两数之和
哈希做法,key是nums[i],value是i。遍历nums,找是否有target-nums[i],如果说明找到结果,直接打包返回就行。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map;
vector<int> result;
for(int i=0; i<nums.size(); i++) //构建哈希表
map.insert(make_pair(nums[i], i));
for(int i=0; i<nums.size(); i++){
auto it = map.find(target-nums[i]); //找另一个数
if(it != map.end() && it->second != i){ //注意不能是自己
result.emplace_back(i);
result.emplace_back(it->second); //找到就打包返回
break;
}
}
return result;
}
};
2. 49-字母异位词分组
哈希表,先求得每个string得hash,然后再构建一个<hash, vector>得map,这样就把相同hash(异位词)分组了
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> result;
map<map<char, int>, vector<string>> res;
vector<map<char, int>> temps;
for(const auto& str:strs){ //求每个string得hash
map<char, int> temp;
for(const auto& s:str){
if(temp.find(s) != temp.end()){
temp[s]++;
}else{
temp.insert(make_pair(s,1));
}
}
temps.emplace_back(temp);
}
for(int i=0; i<strs.size(); i++){ //找hash对应的vector
if(res.find(temps[i]) != res.end()){ //如果有,追加string
res[temps[i]].emplace_back(strs[i]);
}else{ //如果没有,新建一个
vector<string> vec;
vec.emplace_back(strs[i]);
res.insert(make_pair(temps[i], vec));
}
}
for(const auto& re:res){ //res中就是已经分组好的
result.emplace_back(re.second);
}
return result;
}
};
3. 128-最长连续序列
这道题存在一个nums中找目标元素的技巧,利用map的find。其次就是,找连续,每个元素的+1在,那就while就行,需要剪枝,我们是从小开始判断的,例如,1、2、3.那么,从1判断到3.因此如果一个元素的-1在数组中,就说明他已经在前一轮判断中了,可以直接continue
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int result_key = 0;
map<int, int> temps;
for(auto num:nums){ //以便快速查找,构建map
if(temps.find(num) == temps.end()){
temps.insert(make_pair(num, 1));
}else{
continue;
}
}
for(auto temp:temps){
int index = 0;
if(temps.find(temp.first-1) != temps.end()) //说明已经在前一轮判断中了,连续,因此continue
continue;
while(temps.find(temp.first+index) != temps.end()){ //开始判断连续
// res.emplace_back(nums[i]+index);
index++;
}
result_key = result_key>index ?result_key:index;
}
return result_key;
}
};
4. 283-移动零
双指针问题,快指针遍历,慢指针表示该存储的位置,快指针元素只要不是0,就赋值给慢指针。最后快指针结束,再把慢指针后面的位置补0就好。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int low = 0;
int fast = 0;
while(fast<nums.size()){
if(nums[fast] !=0){
nums[low++] = nums[fast]; //移动到前面(low)
}
fast++;
}
while(low<nums.size()){ //最后的补0;
nums[low++] = 0;
}
}
};
5. 11-盛最多水的容器
双指针问题,左右指针位置代表容器两边,往中间遍历,不断更新容积。首先对于容器高,肯定是两个指针中高度较小的是h。其次就是更新,优先动高度小的,因为要找容积大的
class Solution {
public:
int maxArea(vector<int>& height) {
int i = 0;
int j = height.size()-1;
int result = 0;
while(i<=j){
int h = min(height[i], height[j]);//小的是h
int temp = (j-i)*h; //计算容积
result = max(result, temp);
if(height[i] > height[j]) //更新小的,为了找大的
j--;
else
i++;
}
return result;
}
};
补充1- 721账户合并
这道题第一眼以为很简单,但按照暴力思路做,其中至少有4层for循环,夹杂各种if判断,妥妥ss代码,并且大概率会超时,因此果断放弃。
本题关键点在于,怎么判断两个邮箱是否相同,即一个邮箱是否在另一个用户邮箱出现过,出现过,就说明这两个邮箱组同属于一个人,就要合并,如果没有出现过,哪怕两个账户组名称一样,那也是两个邮箱组。
进一步剖析,有一堆邮箱,若干个邮箱是一个组,某个邮箱会在两个组内出现(这道题好处就是整体处理不用管名称,邮箱就是唯一决定因素,也就是说不存在相同邮箱不同名称的情况,因此只要有两个相同邮箱,就合并),此时需要将这两个邮箱组合并,嗯?这不就是并查集么
- 首先,先做准备工作,对于每个邮箱先映射为int,整体操作都用int,相同邮箱就是一个int;再邮箱映射到账户名称,为了最后返回时找到邮箱组对应的名称(题目要求的格式,第一个是名称,后面是邮箱)
- 第二步,就是重点,对于一个邮箱组,合并其中的所有邮箱,并查集的合并操作。如果有一个邮箱在两个邮箱组,那这两个邮箱组也会合并在一起。此时已经完成任务了,集合内的每个团就是一个要返回的邮箱组,剩下就是处理返回值就好
- 第三步就是将同一个根节点的邮箱(同一个邮箱组)放在一起,在利用一个map<int, vector>,key就用该邮箱组的根节点就好。之前保存过邮箱到int的映射关系,这一步就是find每个int,都会返回根节点,然后把email插入到对应key(根节点int)的value中即可。这要就得到结果的每个邮箱组的vector了
- 遍历第三步得到的map,每个value就是一个邮箱组,然后首部加入邮箱组对应的名称就好,随便用一个邮箱就行,第一步得到过邮箱->名称。就大功告成
class Solution {
public:
class UnionFind {
private:
vector<int> p;
public:
UnionFind(int n) : p(n) {
iota(p.begin(), p.end(), 0);
}
int find(int x) {
return p[x] == x ? x : p[x] = find(p[x]);
}
void unite(int x, int y) {
p[find(x)] = find(y);
}
};
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
map<string, int> emailToIndex;
unordered_map<string, string> emailToName;
for (auto& account : accounts) {
for (int i = 1; i < account.size(); ++i) {
string& email = account[i];
if (!emailToIndex.contains(email)) {
emailToIndex[email] = emailToIndex.size(); //邮箱->账户id的哈希
emailToName[email] = account[0]; //邮箱->账户的哈希
}
}
}
UnionFind uf(emailToIndex.size());
for (auto& account : accounts) {
for (int i = 2; i < account.size(); ++i) {
//账户id放入集合:每个账户的邮箱都跟第一个邮箱合并
uf.unite(emailToIndex[account[1]], emailToIndex[account[i]]);
}
}
unordered_map<int, vector<string>> indexToEmails;
for (auto& [email, index] : emailToIndex) {
indexToEmails[uf.find(index)].emplace_back(email); //将同属于一个集合的邮箱放在一起
}
vector<vector<string>> ans;
for (auto& [_, emails] : indexToEmails) {
vector<string> account;
account.emplace_back(emailToName[emails[0]]); //先根据第一个邮箱把对应账户加进去
for (auto& email : emails) { //再一次加入邮箱即可。
account.emplace_back(email);
}
ans.emplace_back(account);
}
return ans;
}
};
6 15-三数之和
典型双指针应用,第一个数就是一个for循环的i,然后双指针分别代表大于i的最小值和最大值。利用while改变两个指针的位置就行。重点在于有可能有重复的值,需要剪枝,第一个是i重复,需要continue。第二是两个指针++或者–值相同,那就利用while直到++或者–到不重复的值;代码很直观,记得还有个变种,三数之和最接近于target。一样的道理。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
for(int i=0;i<nums.size();i++){
if(nums[i] > 0)
return result;
if(i>0 && nums[i] == nums[i-1])
continue;
int left = i+1;
int right = nums.size()-1;
while(right>left){
if(nums[i]+nums[left]+nums[right] > 0)
right--;
else if(nums[i]+nums[left]+nums[right] < 0)
left++;
else{
result.emplace_back(vector<int> {nums[i], nums[left], nums[right]});
while(right>left && nums[left] == nums[left+1])
left++;
while(right>left && nums[right] == nums[right-1])
right--;
right--;
left++;
}
}
}
return result;
}
};
7. 42 接雨水
这道题解法很多,首先单调栈解法:
能接雨水,就是有一个大小大的三元素。从左往右,栈元素递减(底部往上),这样每次遍历一个新元素,如果大于栈顶元素,那就满足大小大,就需要计算面积。如果小于,那就push。
class Solution {
public:
int trap(vector<int>& height) {
if(height.size() <= 2) return 0;
stack<int> st;
int result = 0;
st.push(0);
for(int i=1; i<height.size(); i++){
if(height[i]<height[st.top()]) //单调递减
st.push(i);
else if (height[i] == height[st.top()]){ //值相同,忽略不计
st.pop();
st.push(i);
}else{
while(!st.empty() && height[i]>height[st.top()]){
int mid = st.top(); //这是底部
st.pop();
if(!st.empty()){
int h = min(height[i], height[st.top()]) - height[mid];
result += (i-st.top()-1)*h;
}
}
st.push(i);
}
}
return result;
}
};
8. 3 无重复字符的最长字串
典型的滑动窗口题目,从左往右遍历,窗口右边不断更新(往右),直到遇到重复字符。那就下一趟for,窗口需要右移动(弹出左边元素)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_set<char> check; //记录char是否出现(无重复)
int n = s.size();
int result = 0, right=-1;
for(int i=0; i<n; i++){
if(i != 0){
check.erase(s[i-1]); //滑动窗口,每次窗口往右,因此左边的需要erase
}
while(right+1 < n && !check.count(s[right+1])){ //逐步增加窗口右边边界,直到重复
check.insert(s[right+1]);
right++;
}
result = max(result, right-i+1); //更新result
}
return result;
}
};
9. 438 找到字符串中的所有字母异位词
滑动窗口记录两个map,一个是目标的map,另一个查询的滑动窗口map,从左往右更新,从0开始,如果相同,就说明找到了起始位置,push(i)即可。更新滑动窗口,将i弹出,把右边界i+p.size()加入。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int num_s=s.size(), num_p=p.size();
vector<int> result;
vector<int> sCount(26, 0);
vector<int> pCount(26, 0);
if(num_p > num_s)
return result;
for(int i=0;i<num_p;i++){ //记录长度为p的滑动窗口
sCount[s[i]-'a']++;
pCount[p[i]-'a']++;
}
// if(sCount == pCount)
// result.emplace_back(0);
for(int i=0;i<=num_s-num_p;i++){
if(sCount == pCount) //找到符合的窗口,加入i
result.emplace_back(i);
if(i==num_s-num_p){ //已经到最后一个位置了
break;
}
--sCount[s[i]-'a']; //弹出左边
++sCount[s[i+num_p]-'a']; //加入右边
}
return result;
}
};
10. 560 和为K的子数组
这道题是我感觉目前最难的理解的一个,暴力很简单,但数组过长应该会超时,因此需要遍历每个子数组的值。
因此这道题方法是利用前缀和。这样想,假如现在位置前缀和为b,之前某个位置前缀和为a,如果,b-a=target,是不是就说明,存在一个连续数组,就是这两个位置之间的元素之和为target,满足条件。等式变化一下,a=b-target。因此我们每次得到一个前缀和x,都在map中找找是不是有个x-target。有,就说明存在一个子数组。
反正第二次做,依旧想不来,研究题解半天才慢慢理解意思。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1; //不是很理解
int count = 0, pre = 0;
for (auto& x:nums) {
pre += x;
if (mp.find(pre - k) != mp.end()) { //找到目标
count += mp[pre - k];
}
mp[pre]++;
}
return count;
}
};