LeetCode最近一次更新中更新了大量高质量的LeetBook,做起来很舒心,于是打算开一些博文来记录我刷Book的经历。
集合set的使用
t1两个数组的交集
求两个集合的交集,重复元素计算一次。
思路很简单,C++里set元素具有唯一性,所以用两个set给两个数组去重再求交集即可。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int>set1;
set<int>set2;
vector<int> ans;
for(int i = 0; i < nums1.size(); i++)
set1.insert(nums1[i]);
for(int i = 0; i < nums2.size(); i++)
set2.insert(nums2[i]);
set_intersection(set1.begin(), set1.end(), set2.begin(), set2.end(), back_inserter(ans));
return ans;
}
};
t2快乐数
题目定义了一种数,当且仅当一个数进行一系列逐位平方和计算后能得到1时,它被称为快乐数。一个数如果不是快乐数,他一定无法通过逐位计算平方和最后得到1.
根据题目定义去计算平方和,把每一次的答案扔进一个set里,每次计算完后查询当前值是否之前计算得到过,如果是,那么可以直接return false。循环边界是当前数字为1.
class Solution {
public:
bool isHappy(int n) {
set<int>ss;
while(1)
{
if(n == 1)
return true;
int res = 0;
while(n > 0)
{
int tmp = n % 10;
res += tmp * tmp;
n /= 10;
if(ss.count(res))
return false;
ss.insert(res);
n = res;
}
}
}
};
t3存在重复元素
给定一个序列,判断它内部是否存在重复的元素。
没什么难度,最水的一道,用一个set来存序列,每次计算判断当前元素是否在之前出现过就行。
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int>ss;
int n = nums.size();
for(int i = 0; i < n; i++)
{
if(ss.count(nums[i])) return true;
ss.insert(nums[i]);
}
return false;
}
};
字典dict的使用
t1两个数组的交集II
求两个数组的交集,重复元素多次计算。
跟set里的那道题很像,但这次用map来辅助判断出现次数。
用map记录第一个数组中出现过的每个元素的次数,再去遍历第二个数组。每次遍历判断当前数字在第一个数组中是否出现过,如果是就把当前数字添加到答案,并减去一个次数。若次数耗尽就在map中移除这个数字。
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
vector<int>ans;
map<int, int>mp;
for(auto el: nums1)
mp[el]++;
for(auto el: nums2)
{
if(mp.count(el))
{
ans.push_back(el);
mp[el]--;
if(!mp[el])
mp.erase(el);
}
}
return ans;
}
};
t2有效的字母异位词
给出两个单词,判断它们是否互为异位词。异位词是指两个单词组成字母完全相同,只是排布顺序不同。
跟上一题有异曲同工的地方。用一个map记录第一个单词中所有字母出现的次数,再去遍历第二个单词,出现一个相同字母就消耗一个次数,没有次数就在map中移除这个单词。最后判断匹配成功的次数是否同时等于两个单词的长度。
class Solution {
public:
bool isAnagram(string s, string t) {
map<char, int>mp;
for(int i = 0; i < s.length(); i++)
mp[s[i]]++;
int cmp = 0;
for(int i = 0; i < t.length(); i++)
{
if(mp.count(t[i]))
{
cmp++;
mp[t[i]]--;
if(!mp[t[i]])
mp.erase(t[i]);
}
}
return (cmp == s.length()) && (s.length() == t.length());
}
};
t3同构字符串
给出两个字符串,判断它们是否是同构地。同构字符串是指两个字符串的组成形式一致,如“egg”与“pdd”都是“122”形式。
有两种做法。第一种是用map保存当前字母在单词中第一次出现的下标位置,然后创造一个变型串,在变型串中把原串的每个字母替换成map中保存的下标,即将“pdd”转化成“122”,最后比较两个单词的变型串,以此为依据来得到答案。
class Solution {
public:
bool isIsomorphic(string s, string t) {
set<char>ss;
map<char, int>mp;
string s1, t1;
int key;
if(s.length() != t.length()) return false;
ss.clear();
mp.clear();
key = 1;
for(int i = 0; i < s.length(); i++)
{
if(!ss.count(s[i]))
{
mp[s[i]] = key;
ss.insert(s[i]);
key++;
}
s1 += to_string(mp[s[i]]);
}
ss.clear();
mp.clear();
key = 1;
for(int i = 0; i < t.length(); i++)
{
if(!ss.count(t[i]))
{
mp[t[i]] = key;
ss.insert(t[i]);
key++;
}
t1 += to_string(mp[t[i]]);
}
//cout << s1 << endl << t1 << endl;
return t1 == s1;
}
};
这个代码中的set其实可以不用,map本身也可以起到判断存在的作用。
第二种做法是在学习题解中学会的,很简洁巧妙,思路也是同源地。对于两个互为同构串的单词,他们的形态是一致地,那么对于两个同构串中位于同一个下标上的每个字母,它们上一次出现的下标也一定是一致地。根据这个点就可以得到第二种代码:
class Solution {
public:
bool isIsomorphic(string s, string t) {
map<char, int>mp1;
map<char, int>mp2;
for(int i = 0; i < s.length(); i++)
{
if(mp1[s[i]] != mp2[t[i]]) return false;
mp1[s[i]] = i;
mp2[t[i]] = i;
}
return true;
}
};
两个map分别保存两个单词中每一位字母上一次出现的位置。这个做法相当巧妙。
t4根据字符出现频率排序
给定一个字符串,根据字母出现的频率重新给单词排序。相同字母要出现在一起,如“aaccca”不可重排为“acacac”,而应该排成“aaaccc”或“cccaaa”。
用一个map存单词中每个字母出现的次数,再对map以次数为关键字排序,从头取出每个字母组成一个新的单词即可。
class Solution {
public:
static bool cmp(const pair<char, int>& a, const pair<char, int>& b)
{
return a.second > b.second;
}
string frequencySort(string s) {
string ans = "";
map<char, int>mp;
for(auto ch: s) mp[ch]++;
vector<pair<char, int>>arr(mp.begin(), mp.end());
sort(arr.begin(), arr.end(), cmp);
for(auto el: arr)
while(el.second--)
ans += el.first;
return ans;
}
};
查找表与求和问题
t1两数之和
给定一个数组与一个目标值,在数组中找到两个数使得他们之和等于目标值,返回这两个数的下标。
用map保存每个数的下标,遍历时检测(目标值-当前数)是否存在在数组中,若存在则可以返回答案。
保存时选择下标+1作为数据,防止处理下标为0的情况。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> ans;
unordered_map<int, int>mp;
for(int i = 0; i < nums.size(); i++)
{
//防止自己加自己
if(mp[target - nums[i]] && mp[target-nums[i]] != i + 1)
{
ans.push_back(i);
ans.push_back(mp[target-nums[i]]-1);
return ans;
}
mp[nums[i]] = i + 1;
}
return ans;
}
};
t2三数之和
给定一个数组,找出所有和为零的三元组。
这题给超时超傻了,最后看了题解。先讲我自己的做法吧,313个cases过了311个,要不是无序集合不能存tuple估计我就过了。只能说这道题确实不太适合用hash来做。
目标是寻找符合要求的三元组,显然直接三重循环是会爆t的,所以选择定一动二,即使用两重循环枚举第一个与第二个数,在内重循环计算0-(i+j)。这样可以吧时间复杂度降到O(n²)。
用一个map来记录数组中出现过的数、一个set来给三元组们去重。每次计算时检测0-(i+j)是否在数组中,若在且该三元组并未在之前出现过,那么将它加入答案。
值得注意的是,C++中{-1,0,1}与{-1,1,0}被视为两个不同的元组,即使他们组成成员是相同的。所以为了确保同一个元组在作为答案时存在唯一性,需要用一个方法来保证每个元组被计算得到时是有序的,这样才能起到去重作用。
//311/313 cases passed
class Solution {
public:
//元组排序方法
inline static tuple<int, int, int> tSort(tuple<int,int,int>tt)
{
int a = get<0>(tt), b = get<1>(tt), c = get<2>(tt);
int maxn = max(a, max(b, c));
int mini = min(a, min(b, c));
int mid = (a + b + c) - (maxn + mini);
return {mini, mid, maxn};
}
vector<vector<int>> threeSum(vector<int>& nums) {
set<tuple<int,int,int>> mys;
vector<vector<int>> ans;
const int n = nums.size();
for(int i = 0; i < n; i++)
{
unordered_map<int, int> mp;
for(int j = i+1; j < n; j++)
{
int tmp = 0 - (nums[i] + nums[j]);
tuple<int, int, int> tt(nums[i], nums[j], tmp);
tt = tSort(tt);
if((mp[tmp]) && (!mys.count(tt)))
{
//printf("%d,%d,%d\n", get<0>(tt), get<1>(tt), get<2>(tt));
ans.push_back({get<0>(tt), get<1>(tt), get<2>(tt)});
mys.insert(tt);
mp[tmp]--;
}
else mp[nums[j]]++;
}
}
return ans;
}
};
这样做几乎快通过了,但还是差两个点,于是用了法2。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>>ans;
const int n = nums.size();
sort(nums.begin(), nums.end());
for(int i = 0; i < n; i++)
{
if(nums[i] > 0) continue;
if(i > 0 && nums[i] == nums[i-1]) continue;
unordered_set<int>mys;
for(int j = i+1; j < n; j++)
{
if(j > i+2 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2]) continue;
int tmp = 0 - (nums[i] + nums[j]);
if(mys.count(tmp))
{
ans.push_back({nums[i], nums[j], tmp});
mys.erase(tmp);
}
else mys.insert(nums[j]);
}
}
return ans;
}
};
t3四数之和
思路跟二三数之和完全一致,但数据规模的提升使得C++语言下不再适合使用hash来解答,不符合我专题训练的目的,遂跳之。事实上二三四数之和用排序+双指针都能获得较之hash法近十倍优秀的效率,实际运用时并不推荐用hash做此类题目,在此仅作练习目的。
灵活选择键值
t1四数相加 II
给定四个数组,每个数组中选择一个元素,统计元素和为零的四元组个数。
思路也很简单。因为有四个成员,显然不可能直接枚举,所以定二动二。
用两重循环枚举Ai+Bj,把它存入map,然后再用另外的两重循环枚举0-Ci-Dj是否在map中,若是则累加器累加上其出现的次数,最后就是答案。
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
int ans = 0;
map<int, int>mp;
for(int i = 0; i < A.size(); i++)
for(int j = 0; j < B.size(); j++)
mp[A[i] + B[j]]++;
for(int i = 0; i < C.size(); i++)
for(int j = 0; j < D.size(); j++)
if(mp[0 - C[i] - D[j]])
ans += mp[0 - C[i] - D[j]];
return ans;
}
};
t2字母异位词分组
给出一系列单词,把同属于一类异位词的单词分到一组。异位词是指字母组成相同,但位置不一定相同的一系列单词。
这里的键值设置有一些巧妙。用一个map,键设置为string,值为string的vector。
每次读入一个string,将它排序。根据定义,异位词的组成字母一定是一样的,所以同属一类的异位词在排序后一定都是同一个单词,然后将这同一类的单词压入当前键值所映射的vector中。最后在挨个把每个vector压入答案中即可。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
map<string, vector<string>> mp;
vector<vector<string>> ans;
for(string s: strs)
{
string tmp = s;
sort(tmp.begin(), tmp.end());
mp[tmp].push_back(s);
}
for(auto el: mp)
ans.push_back(el.second);
return ans;
}
};
t3回旋镖的数量
给出若干个二维点,定义一个三元组为回旋镖,当且仅当组成它的三个点中有一个点到另外两个点的距离相等,找出这组二维点中回旋镖的数量。
定一动一。对于每个点i,枚举计算每个点j到它的距离,以这个距离为键,以其值为累加器计算距离为|(i,j)|的线段的个数,统计这个个数即为答案。
class Solution {
public:
int numberOfBoomerangs(vector<vector<int>>& points) {
int ans = 0;
const int n = points.size();
for(int i = 0; i < n; i++)
{
map<int, int> mp;
for(int j = 0; j < n; j++)
{
int dx = points[i][0] - points[j][0];
int dy = points[i][1] - points[j][1];
mp[dx * dx + dy * dy]++;
}
for(auto el: mp)
ans += el.second * (el.second - 1);
}
return ans;
}
};
t4直线上最多的点数
给定一系列二维点,求最多有多少个共线的点。
这是一道hard,但其实没那么难,因为题设有个隐藏设定:平行也算作共线。
所以我们可以直接用斜率做map的键,值做累加器来统计同在一条直线上的点。
具体来讲就是两重循环定一动一去枚举所有的点,以斜率为键来记录同在一条直线上的点的个数,最后统计最大值就可以了。
需要注意的点有两个。第一个是这题有一个数据存在精度问题,所以不能用double做键表示斜率,我采用的是string;第二个是处理原点的问题,我选择的是单独用一个累加变量,检测到0直接++然后continue就行了。
class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int ans = 0;
const int n = points.size();
if(n < 3) return n;
for(int i = 0; i < n; i++)
{
int cnt = 0;
int curMax = 0;
map<string, int>mp;
for(int j = i+1; j < n; j++)
{
if(points[i][0] == points[j][0]
&& points[i][1] == points[j][1])
{
cnt++;
continue;
}
int dx = points[j][0] - points[i][0];
int dy = points[j][1] - points[i][1];
int tmp = gcd(dx, dy);
string key = to_string(dx / tmp) + "/" + to_string(dy / tmp);
//cout << key << endl;
mp[key]++;
curMax = max(curMax, mp[key]);
}
ans = max(ans, curMax + cnt + 1);
}
return ans;
}
private:
int gcd(int a, int b)
{
if (b == 0)
return a;
return gcd(b, a % b);
}
};
查找表和滑动窗口
t1存在重复元素II
给定一个数组,找出其中两个下标之差不超过k并且值相等的元素。
这题时间卡的很死,O(n²)必超,所以采用滑动窗口。
用一个set记录当前存在的元素,每次遍历检测当前元素是否存在。为了切合题意,维护set的size为k,即当set.size()>k时,erase掉nums[i-k]就行了。
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_set<int> mys;
const int n = nums.size();
for(int i = 0; i < n; i++)
{
if(mys.count(nums[i])) return true;
mys.insert(nums[i]);
if(mys.size() > k) mys.erase(nums[i - k]);
}
return false;
}
};
t2存在重复元素 III
跟上一题很像,但这题要找的是满足一个大小区间的数。
思路也很清晰,在上一题滑动窗口的基础上思考题设要求:
“使得 nums [i] 和 nums [j] 的差的绝对值小于等于 t” 其实就是指“nums[i]-t<=nums[j]<=nums[i]+t”。可以利用set的有序性,二分查找第一个大于等于nums[i]-t的值,再判断它是否小于等于nums[i]+t即可。
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int k, int t) {
set<long long> mys;
for(int i = 0; i < nums.size(); i++)
{
auto el = mys.lower_bound((long long)nums[i] - t);
if(el != mys.end() && (*el) <= (long long)nums[i] + t)
return true;
mys.insert(nums[i]);
if(mys.size() > k)
mys.erase(nums[i-k]);
}
return false;
}
};