Leetcode刷题报告1——哈希表
文章目录
- 说明
- [1. 两数之和](https://leetcode.cn/problems/two-sum/)
- 题干
- 题解
- [49. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/)
- 题干
- 题解
- [128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/)
- 题干
- 题解
说明
从今天开始,我们开始力扣刷题,每天计划完成3道题目,作为我算法数据结构的进一步学习。我决定先完成热题100这个题单,它按照算法/数据结构分成了若干部分。每部分会用若干天完成,完成后我会写一篇博客,记录一下我的题解和感悟吧。
本章的主题是哈希表,总共是1、49、128三道题。第一次写刷题博客,就都先贴出来吧。
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)
的算法吗?
题解
题目本身很简单,没什么理解上的难点。显然暴力枚举的时间复杂度是平方级,不满足进阶要求。想要有常数级时间复杂度,就要用哈希表。
c++11标准库 <unordered_set>和 <unordered_map>定义了用哈希表实现的key-value结构和集合结构。我们直接使用即可。
#include <bits/stdc++.h>class Solution
{
public:std::vector<int> twoSum(std::vector<int>& nums, int target){int n = nums.size();std::unordered_map<int, int> hash_table; //以数值为索引,保存数组下标的值for (int i = 0; i < n; i++){auto it = hash_table.find(target - nums[i]);if (it != hash_table.end()){return { i, it->second };}hash_table[nums[i]] = i;}return {};}
};int main() {int n;std::cin >> n;std::vector<int>nums;for (int i = 0;i < n;i++) {int t;std::cin >> t;nums.push_back(t);}int target;std::cin >> target;Solution mySolution;std::vector<int> ans = mySolution.twoSum(nums, target);for (int i : ans) {std::cout << i << " ";}
}
说明:大部分的oj都是acm输入输出,我之前在洛谷刷题,也习惯于acm输入输出,所以这里我给出了完整的main,包含了输入输出过程。提交答案到力扣的时候,只需要提交上面的类即可。
这道题我新学/复习的知识点:
-
左值引用,引用传值:形参带引用符号
&
,实参不需要,直接就可以修改实参的值。相比于地址传值(指针),优点是使用起来更方便,无需取地址和解引用。缺点是没有空引用,而且引用必须在声明的同时赋值,赋值后无法更改。
-
auto关键字作为变量声明:
使用占位符类型声明的变量的类型从其初始化器推导而来。此用法在变量的初始化声明中是允许的。
占位符类型只能作为声明说明符序列中的声明说明符之一出现,或者作为指定替换此类声明说明符的类型的尾随返回类型中的类型说明符之一出现。在这种情况下,声明必须声明至少一个变量,并且每个变量都必须具有非空初始化器。
简单来说就是根据后续内容推测前面的变量类型。这道题auto 代指unorder_map的迭代器,类型就是pair<key,value>,当然也可能是end()迭代器。
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]
仅包含小写字母
题解
其实看到这道题后,我脑子里就有一个模糊的思路:遍历每个字符串,看看字符串是否能匹配上若干个字符,如果能匹配上,放到一组中。如果不能,单独再开一组,顺带确定新的一组的标签。
我第一次写的代码没有想好怎么把哈希表和二维数组互转。这也是我认为的这道题的难点。另外就是匹配过程不太清晰,标签应该是char还是string也没弄明白。最后还是看了官方的题解。
#include <bits/stdc++.h>class Solution {
public:std::vector<std::vector<std::string>> groupAnagrams(std::vector<std::string>& strs) {std::unordered_map<std::string, std::vector<std::string>> map;for (std::string& str : strs) {std::string key = str;sort(key.begin(), key.end());map[key].emplace_back(str);}std::vector<std::vector<std::string>> ans;for (auto it = map.begin(); it != map.end(); it++) {ans.emplace_back(it->second);}return ans;}
};int main() {int length;std::cin >> length;std::vector<std::string> stringNums;for (int i = 0;i < length;i++) {std::string s;std::cin >> s;stringNums.push_back(s);}Solution mySolution;auto ans = mySolution.groupAnagrams(stringNums);for (int i = 0;i < ans.size();i++) {std::vector<std::string> t = ans[i];for (int j = 0;j < t.size();j++) {std::cout << t[j] << " ";}std::cout << std::endl;}
}
我用的是题解排序法,对于每个待匹配字符串,通过std::sort()生成它的标签,标签作为哈希表的key,若干个string形成的vector-string 作为value,形成kv关系。排序法好写,容量小的时候时间复杂度略高。
这道题我最大的收获:
- 一是如何确定key的思路:排序or计数,最终目的是生成唯一的key。
- 二是学习了emplace_back方法:之前我只知道push_back,或者insert。这种方法可以就地构造元素,不需要临时元素,直接生成在容器内,一般情况下推荐使用。
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
题解
这道题我一开始想到的是按范围枚举,而且没有用set,贴出错误代码:
class Solution {
public:int longestConsecutive(std::vector<int>& nums) {int minNum = INT32_MAX;int maxNum = INT32_MIN;std::unordered_map<int, int> map;for (int i = 0;i < nums.size();i++) {minNum = std::min(minNum, nums[i]);maxNum = std::max(maxNum, nums[i]);map[nums[i]] = 1;}int ans = 0, cnt = 0;for (int i = minNum;i <= maxNum;i++) {if (map.find(i) != map.end()) {cnt++;ans = std::max(ans, cnt);}else {cnt = 0;}}return ans;}
};
这种方法平均也能达到常数级别,但是如果上下限太大,也就是n太大,就会超时。最终这个方法能通过80%左右的测试点。
第一个优化是改用unorder_set,还有就是重新整理思路,确保只让每个元素进一次查找(find/count)。
最终正确代码是:
#include <bits/stdc++.h>class Solution {
public:int longestConsecutive(std::vector<int>& nums) {std::unordered_set<int> numSet;for (const int& num : nums) {numSet.insert(num);}int ans = 0;for (const int& num : numSet) {if (!numSet.count(num - 1)) {int numCnt = num;int ansCnt = 0;while (numSet.count(numCnt)) {numCnt++;ansCnt++;}ans = std::max(ans, ansCnt);}}return ans;}
};int main() {int size;std::cin >> size;std::vector<int> nums;for (int i = 0;i < size;i++) {int t;std::cin >> t;nums.push_back(t);}Solution mySolution;int ans = mySolution.longestConsecutive(nums);std::cout << ans;
}
同样先插入元素到容器,然后查找,区别在于最终方法确保了每个元素仅仅进行一次查找。
如何实现?首先一个数字被放到一个序列的前提是,这个数字的上一个数字就在序列里,此时计数+1,序列继续延伸。否则,以这个数字为起点,开辟一个新的序列。每当一个序列延申完毕,更新答案。
这样,我们确保了每个数组中的元素,都进行并且仅进行一次查找。而且,非数组元素不会进行查找,这样不仅是常数级时间复杂度,n还比较小,能ac。
这道题我学到的知识点:
unorder_set和count方法。之前不太了解u_set,只知道是没有重复元素的集合,底层是哈希表,查找时间复杂度是o1。但是具体的查找库函数不太了解,今天算是补上了。