提示:DDU,供自己复习使用。欢迎大家前来讨论~
哈希表
学习
一、哈希表的基础理论
1. 哈希表的使用
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法
哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
2. 哈希碰撞:拉链法和线性探测
3. 常见的三种哈希结构
- 数组
- set集合
- map映射
二、题目
题目一:242.有效的字母异位词
思路:
定义一个记录26个字母的数组,先遍历第一个字符串,进行字母个数的累加,再遍历第二字符串,进行字母个数的缩减。
最后判断,如果每一个字符的在数组中对应的个数。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0}; // 使用初始化列表,数组的每个元素都将被初始化为 0
for (int i = 0; i < s.size(); i++) {
record[s[i] - 'a']++;
}
for (int j = 0; j < t.size(); j++) {
record[t[j] - 'a']--;
}
for (int i = 0; i < 26; i++) { //这里要遍历26个字母
if (record[i] != 0)
return false;
}
return true;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
题目二:349.两个数组的交集
如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
所以这个题目,在没有给定限制条件的情况下,使用set是最好的。
直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。这个耗时在数据量大的情况下,差距也很明显。
三种常用的数据结构:
- std::set
- std::multiset
- std::unordered_set
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
1.使用set:
这个一次性记住有点难。
class Solution {
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 (int num : nums2) {
// find这个成员函数,找到了返回指向该元素的迭代器,如果没找到就会返回指向end的迭代器
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num); //每次insert和find一个数值,需要进行一个哈希运算,并开辟新的空间。
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
- 时间复杂度: O(n + m) m 是最后要把 set转成vector
- 空间复杂度: O(n)
2.使用数组:
力扣改了 题目描述 和 后台测试数据,增添了 数值范围:
- 1 <= nums1.length, nums2.length <= 1000
- 0 <= nums1[i], nums2[i] <= 1000 就可以 使用数组来做哈希表了, 因为数组中数值都是 1000以内的。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
int hash[1005] = {0}; // 默认数值为0
for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
hash[num] = 1;
}
for (int num : nums2) { // nums2中出现话,result记录
if (hash[num] == 1) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
- 时间复杂度: O(m + n)
- 空间复杂度: O(n)
题目三:202.快乐数
使用哈希表进行寻找,计算各位数字的平方和,为了防止元素重复,使用unordered_set, 如果有重复,说明无限循环了,return false 结束。
反之,继续循环求平方和,直至sum = 1。
- 取数值的每一位数字。求和(单独写一个函数,自己掌握一下。)
- 判断是不是快乐数
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;
while (1) {
int sum = getSum(n);
if (sum == 1)
return true;
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
- 时间复杂度: O(logn),对于一个数字n,其位数是O(log n)
- 空间复杂度: O(logn)
题目三:1.两数之和
本题有四个重点:
- 为什么会想到用哈希表:找一个元素是否出现过,或者元素是否在集合中出现过。
- 哈希表为什么用map:map可以用来记录key ,value对。
- 本题map是用来存什么的:存放遍历过的元素。
- map中的key和value用来存什么的:key是用来存值,value用来存下标。 {key:数据元素,value:数组元素对应的下标}
- 为什么使用unordered_map: key不需要有序,此时使用unordere_map效率更高。查找和增删效率均为O(1).
代码细节:
在C++中的映射容器(如 std::map
或 std::unordered_map
)中,每个元素是一个键值对,其中包含两个部分:
first
:表示键(key),它是唯一的,用于在映射中标识元素。second
:表示值(value),与键相关联的数据。
通过 iter->first
来访问键部分,通过 iter->second
来访问值部分。这里的 ->
运算符是用于访问指针指向对象的成员的。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map; //用来记录遍历过的元素
for (int i = 0; i < nums.size(); i++) {
auto iter = map.find(target - nums[i]); //找到和为target的另一个元素
if (iter != map.end()) {
return {iter->second, i};
}
map.insert(pair<int, int>(nums[i], i));//map的添加pair<type,type>(,)
}
return {};
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(n)
总结
-
学习使用了哈希表三种结构的用法
- 数组:以其快速访问的特性而著称,适用于存储固定大小的数据集合。
- set:一个不允许重复元素的集合,保证了元素的唯一性。
- map:一种键值对集合,允许我们通过键快速访问对应的值。
-
哈希表的基本操作,
-
find()
:用于查找特定元素是否存在于哈希表中,并返回指向该元素的迭代器。 -
end()
:返回一个指向哈希表末尾的迭代器,常用于遍历操作的终止条件。 -
insert()
:用于向哈希表中添加新的元素。对于map
,使用insert()
函数时,通常需要提供一个pair
类型的参数,例如map.insert(std::make_pair<key_type, value_type>(key, value))
。 -
auto
是 C++ 中的类型推导关键字,它允许编译器根据初始化表达式自动确定变量的类型
-
今天的题目基本都是使用了两个集合或者数组,进行对比和对应的操作。