leetcode hot100刷题(1)
1. 两数之和
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
进阶:你可以想出一个时间复杂度小于 O(n2)
的算法吗?
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hashtable;
for (int i = 0; i < nums.size(); ++i) {
auto it = hashtable.find(target - nums[i]);
if (it != hashtable.end()) {
return {it->second, i};
}
hashtable[nums[i]] = i;
}
return {};
}
};
注意:如果不采用这种一般将数据压入hashtable一边判断而是采用先将所有数据全部压入hashtable后再一个给判断过不了样例2
49. 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 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]
仅包含小写字母
方法一:排序
时间复杂度:$O(nklogk)$,其中 n 是 strs 中的字符串的数量,k是 strs 中的字符串的的最大长度。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>> hashtable;
vector<string> tmp(strs);
for(int i=0;i<strs.size();i++) sort(tmp[i].begin(),tmp[i].end());
for(int i=0;i<strs.size();i++)
hashtable[tmp[i]].push_back(strs[i]);
vector<vector<string>> res;
for(auto it:hashtable) res.push_back(it.second);
return res;
}
};
注意:下面代码只会对tmp的副本排序,所以tmp[i]仍然没有被排序,需要将for(string s:tmp) sort(s.begin(),s.end());
改成for(string& s:tmp) sort(s.begin(),s.end());
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string,vector<string>> hashtable;
vector<string> tmp(strs);
for(string s:tmp) sort(s.begin(),s.end());
for(int i=0;i<strs.size();i++)
hashtable[tmp[i]].push_back(strs[i]);
vector<vector<string>> res;
for(auto it:hashtable) res.push_back(it.second);
return res;
}
};
他山之石
由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键。
由于字符串只包含小写字母,因此对于每个字符串,可以使用长度为 26 的数组记录每个字母出现的次数。需要注意的是,在使用数组作为哈希表的键时,不同语言的支持程度不同,因此不同语言的实现方式也不同。
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
// 自定义对 array<int, 26> 类型的哈希函数
auto arrayHash = [fn = hash<int>{}] (const array<int, 26>& arr) -> size_t {
return accumulate(arr.begin(), arr.end(), 0u, [&](size_t acc, int num) {
return (acc << 1) ^ fn(num);
});
};
unordered_map<array<int, 26>, vector<string>, decltype(arrayHash)> mp(0, arrayHash);
for (string& str: strs) {
array<int, 26> counts{};
int length = str.length();
for (int i = 0; i < length; ++i) {
counts[str[i] - 'a'] ++;
}
mp[counts].emplace_back(str);
}
vector<vector<string>> ans;
for (auto it = mp.begin(); it != mp.end(); ++it) {
ans.emplace_back(it->second);
}
return ans;
}
};
语法学习
std::array
是 C++ 标准库提供的静态数组,长度固定,但有更多功能,如边界检查。
在 C++11
之后,vector
容器中添加了新的方法:emplace_back()
,和 push_back()
一样的是都是在容器末尾添加一个新的元素进去,不同的是 emplace_back()
在效率上相比较于 push_back()
有了一定的提升。
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<pair<int, string>> vec;
// 使用 push_back 需要显式构造对象
vec.push_back(make_pair(1, "Apple"));
// 使用 emplace_back 直接构造对象
vec.emplace_back(2, "Banana");
for (const auto& p : vec) {
cout << p.first << " - " << p.second << endl;
}
return 0;
}
C++中push_back和emplace_back的区别 - 知乎
自定义hash函数
在C++中如何对自定义类型做hash操作_c++自定义哈希函数-CSDN博客
128. 最长连续序列
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
示例 3:
输入:nums = [1,0,1,2]
输出:3
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
set<int> s(nums.begin(),nums.end());
nums.clear();
for(int num:s) nums.push_back(num);
int res=(nums.size()!=0);
int l=0,r=1;
int n=nums.size();
while(r<n){
while(r<n&&nums[r]-nums[r-1]==1){
r++;
}
res=max(res,r-l);
l=r;r=r+1;
}
return res;
}
};
他山之石
本题不能排序,因为排序的时间复杂度是 $O(n\log n)$,不符合题目 $O(n)$ 的要求。
核心思路:
对于 nums
中的元素 x
,以 x
为起点,不断查找下一个数 x+1, x+2, ...
是否在 nums
中,并统计序列的长度。
关键优化(使时间复杂度达到 $O(n)$):
-
使用哈希集合
- 将
nums
中的数存入哈希集合,这样可以 $O(1)$ 判断数字是否存在。
- 将
-
避免重复计算
- 如果
x-1
存在于哈希集合中,则不以x
为起点。 - 这样可以保证我们只会从连续序列的第一个数开始计算,避免大量重复计算。
- 例如
nums = [3,2,4,5]
:- 从
3
开始,我们可以找到3,4,5
这个连续序列。 - 从
2
开始,我们可以找到2,3,4,5
,一定比从3
开始的更长,因此3
不是一个合适的起点。
- 从
- 如果
细节优化:
- 遍历哈希集合,而不是
nums
本身!- 例如
nums = [1,1,1,...,1,2,3,4,5,...]
(前一半都是1
)。 - 遍历
nums
会导致每个1
都执行 $O(n)$ 的循环,总复杂度变为 $O(n^2)$,从而超时。
- 例如
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
int res=0;
unordered_set<int> hashtable(nums.begin(),nums.end());
for(int x:hashtable){
if(hashtable.contains(x-1)) continue;
//x是序列的起点
int y=x+1;
while(hashtable.contains(y)) y++;
//循环结束后,y-1是最后一个在连续数
res=max(res,y-x);
}
return res;
}
};
语法学习
从C++11到C++23(六)C++20利用contains查询map是否存在某个键_c++ map contains-CSDN博客
C++20新增了std::map::contains
可以直接查找键是否存在,返回值类型为bool型
#include <iostream>
#include <string>
#include <map>
int main()
{
std::map<int, std::string> example = {{1, "One"}, {2, "Two"},
{3, "Three"}, {42, "Don\'t Panic!!!"}};
if(example.contains(42)) {
std::cout << "Found\n";
} else {
std::cout << "Not found\n";
}
}
283. 移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
提示:
1 <= nums.length <= 104
-231 <= nums[i] <= 231 - 1
进阶:你能尽量减少完成的操作次数吗?
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int i=0,j=0;
int n=nums.size();
while(i<n&&j<n){
while(n>i&&nums[i]) i++;
j=i+1;
while(n>j&&!nums[j]) j++;
if(n>i,n>j) swap(nums[i],nums[j]);
i++;
}
}
};
他山之石
📌 核心思路
我们将 0
视作 空位,把所有 非零元素 依次移动到数组左侧的空位,同时 保持原有顺序。
示例
cpp复制编辑输入: nums = [0, 1, 0, 3, 12]
输出: [1, 3, 12, 0, 0]
💡 关键点
- 维护一个指针
i0
,指向最左侧的 空位(即0
)。 - 遍历数组
- 遇到非零元素 → 交换
nums[i]
和nums[i0]
,然后i0++
。 - 遇到 0 → 不操作,继续遍历。
- 遇到非零元素 → 交换
📜 代码
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int i0 = 0; // 记录最左侧空位
for (int& x : nums) { //
if (x) { // 只有非零元素才交换
swap(x, nums[i0]);
i0++; // 更新最左侧空位
}
}
}
};
📊 复杂度分析
- 时间复杂度:O(n),遍历
nums
一次,每个元素最多交换一次。 - 空间复杂度:O(1),原地修改,没有额外空间消耗。
🧐 运行过程解析
示例
输入: nums = [0, 1, 0, 3, 12]
i |
i0 |
nums[i] |
交换后 nums |
---|---|---|---|
0 | 0 | 0 | 不操作 |
1 | 0 | 1 | [1, 0, 0, 3, 12] |
2 | 1 | 0 | 不操作 |
3 | 1 | 3 | [1, 3, 0, 0, 12] |
4 | 2 | 12 | [1, 3, 12, 0, 0] |
最终输出:
[1, 3, 12, 0, 0]
💬 可能的疑问
❓ Q1: 为什么可以i0=0
?
- 实际上
i0
会自动找到第一个为0的元素
-
例如
nums = [1, 2, 3, 4,0,12]
- 发现
1
,交换nums[0]
和nums[0]
→[1, 2, 3, 4,0,12]
,i0=1
- 发现
2
,交换nums[1]
和nums[1]
→[1, 2, 3, 4,0,12]
,i0=2
- 发现
3
,交换nums[2]
和nums[2]
→[1, 2, 3, 4,0,12]
,i0=3
- 发现
4
,交换nums[3]
和nums[3]
→[1, 2, 3, 4,0,12]
,i0=4
- 发现
0
,交换nums[4]
和nums[4]
→[1, 2, 3, 4,0,12]
,i0=4
- 发现
12
,交换nums[5]
和nums[4]
→[1, 2, 3, 4,12,0]
,i0=5
- 发现