代码随想录算法训练营Day 6| 242. 有效字母异位词、349. 两个数组的交集、202. 快乐数、1. 两数之和
哈希表倒不是一种明确的容器,他更像是一种存储和处理数据的结构和思想,通过用空间换时间,通过索引的方式直接访问元素,从而大大降低了遍历容器的时间开销。所以哈希表是一种基于key - value的处理思路,在具体的实现过程中,会考虑到哈希函数、哈希碰撞(拉链法、线性探索法等等)。
今天是哈希表相关内容的第一天练习,主要在于理解思想和基本的实现思路。
一个很重要的想法是:当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法
关于哈希表的c++实现
在哈希表的实现中,用来存储数据的一般是三个数据结构:(额外的)数组、map
、set
c++中set
提供了三种类型,其底层实现方法以及优劣性如下所示:
set | 底层实现思路 | 内部是否有序 | 数值是否可以重复 | 是否能更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(log n) | O(log n) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
同理,对于map,也有类似的情况:
map | 底层实现思路 | 内部是否有序 | 数值是否可以重复 | 是否能更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可更改 | O(log n) | O(log n) |
std::multimmap | 红黑树 | key有序 | key可以重复 | key不可更改 | O(log n) | O(log n) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可更改 | O(1) | O(1) |
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set
,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set
,如果要求不仅有序还要有重复数据的话,那么就用multiset
那么再来看一下map
,在map
是一个key value 的数据结构,map
中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的
其他语言例如:java里的
HashMap
,TreeMap
都是一样的原理。可以灵活贯通
虽然std::set
、std::multiset
的底层实现是红黑树,不是哈希表,std::set
、std::multiset
使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map
也是一样的道理
242.有效字母异位词
这是一个很老的题目了,"有效的字母异位词"我认为这样理解更为直观一些:将一个单词中的所有字母打乱重组,所构成的都是有效的字母异位词,即Anagram
最基础的是暴力解法,使用两层for循环,一次用来遍历单词的每个字母,然后判断字母出现的次数,再使用一次for循环,在另一个字母中迭代判断
这样做会有很大的开销(虽然这个题评测机给的数据规模很小),而且暴力解法需要额外判断一些重复的情况,大大提高了debug难度。所以直接想想其它的办法:
-
两个词语满足anagram结构的要素总结起来就两个:单词长度相同;每个单词在单词中的出现频率相同。这个应该算是充要条件了,除非我对题意理解有误
-
在怎样具体实现上面这个判断方式呢? 我们的方法是使用一个record的数组的结构实现哈希表,只需要经过两个O(1)的遍历,第一次统计原单词中每个字母的出现次数(这样的话数组的size只需要给26,数组下表使用每个字母相对于'a'的大小就行了,甚至不需要知道 具体的ASCII Code;当然,leetcode上的题目是有所简化的,没有考虑大写字母的情况),第二次遍历第二个用于比较的单词,遍历到一个就从record中删一个。如果最后的record数组为空,那么显然这两个单词满足anagram。如果不为空,显然不满足;
代码如下:
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for(int i = 0; i < s.size(); i++){
record[s[i] - 'a']++;
}
for(int i = 0; i < t.size(); i++){
record[t[i] - 'a']--;
}
for(int i = 0; i < 26; i++){
if(record[i] != 0){
return false;
}
}
return true;
}
};
349.两个数字的交集
给定两个数组,判定他们的交集,从输入样例来看,给定的两个数组中是可以有元素的重复的,但是最后输出的交集中是没有的;
由此看来,这个题目就很满足我们哈希表的适用范围:快速判断元素是否在容器中。同时,由于最后交集的元素是不可重复的,所以在这里选用set最好不过了,而且需要注意的是:对给定的数组进行去重,是完全不影响我们后来的操作的
具体的实现思路是:首先定义一个用于存放答案的set。将其中一个给定的数组(vector)转化为set,随后使用迭代器进行遍历。如果迭代器指向的当前元素在第二个给定的数组中存在(使用set内置find进行判断),就把该元素存入答案set
代码如下:
class Solution {
public:
public:
vector<int> intersection(vector<int> & nums1, vector<int>& nums2){
unordered_set<int> result_set;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for(auto num : nums2){
if(nums_set.find(num) != nums_set.end()){
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
这道题某种程度上也是对stl set的基本操作的练习
202.快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
这道题首先需要编写一个计算各位数字平方和的小函数(需要用到一点c语言风格的对于数位的处理知识)
然后我们使用循环的方式进行判断。"is Happynum"意味着平方和是1,只要平方和是1就可以跳出循环。
"is not Happynum"意味着无限循环,怎么判断呢?只要平方和与之前计算过的某一次平方和相同,那就意味着陷入了无限循环。
所以我们在循环的过程中需要不断存储平方和的值,并且判断本次循环中的平方和是否在其中出现
"判断一个元素是否在现有容器中",很贴合哈希的使用场景,通过set就可以了
题目没有其他的难度,代码如下:
class Solution{
public:
int getSum(int n){
int sum = 0;
while(n){
sum += (n%10) * (n%10);
n /= 10;
}
return sum;
}
bool isHappy(int n){
unordered_set<int> set;
int sum = getSum(n);
while(1){
if(sum == 1){
return true;
}
if(set.find(sum) != set.end()){
return false;
}
else{
set.insert(sum);
}
sum = getSum(sum);
}
}
};
1.两数之和
这是leetcode的第一题,还是很有意思的
给定一个整数数组 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(\(n^2\)) 的算法吗?
暴力方法非常简单,使用两层for循环,但是时间复杂度是O(\(n^2\))
暴力代码如下:
class Solution{
public:
vector<int> twoSum(vector<int>& nums, int target){
vector<int> result = {0,0};
for(int i = 0; i < nums.size() - 1; i++){
for(int j = i + 1; j < nums.size() ; j++){
if(nums[i] + nums[j] == target){
result[0] = i;
result[1] = j;
return result;
}
}
}
return result;
}
};
分析一下暴力解法的不足:大量的重复判断造成了不必要的开销,由于暴力思路中是顺序遍历的,所以开销很大。所以,我们自然而然地想到:既然问题的实现思路是直观的:对于集合中的每一个元素,能否找到集合中的另一个元素使得两元素之和等于target?所以优化思路是:通过哈希表的结构,提高我们的"随机读写"效率。
具体的做法是:
- 首先使用迭代器遍历vector,对于其中的每一个元素,查询是否在我们定义的(用来存储已经访问过的元素的)map
为什么要用map呢?因为还要能够访问元素在原有vector中的下标。所以set在这里并不好用,更确切地来讲,因为我们地元素是无序的,所以使用
unordered_map
效率最高
*然后对于每一次迭代中的元素,在map中查询是否有与之配对的元素,如果有,直接返回答案(题目给出了满足要求的答案只有一对)
代码如下:
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
我觉得这个办法是有点凑巧的感觉了,hmmm……
也许不是很直观,其实只要通过迭代器将vector的内容存入map就好了,但是这里面需要返回下标,所以我尝试的时候遇到了一点困难,也许当我深入了解这些关联容器的用法之后会解决这个思路吧
下面是我尝试中失败的代码,唉,实在是对c++不了解!