Before Coding
06进入到哈希表的学习中,哈希表是查找方法中的一种,在查找过程中的时间复杂度为 O ( 1 ) O(1) O(1),当然相比于链表中的查找是通过时间换空间。初次接触哈希表可以通过懒猫老师的视频快速了解哈希表。
关于基本的查找方法如下图所示:
哈希表理论部分
可参考我的笔记,这里补充一些关于代码随想录的内容:
- Q:对于哈希表常用的三种结构
-
数组
-
set 集合
-
map 映射
在C++中set和map分别提供以下三种数据结构:集合 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率 std::set 红黑树 有序 否 否 O ( l o g n ) O(logn) O(logn) O ( l o g n ) O(logn) O(logn) std::multiset 红黑树 有序 是 否 O ( l o g n ) O(logn) O(logn) O ( l o g n ) O(logn) O(logn) std::unordered_set 哈希表 无序 否 否 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1) -
Q:set集合的底层实现及性质
std::unordered_set
底层实现为哈希表,而std::set
和std::multiset
底层为红黑树,红黑树是一种二平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整颗树的错乱,所以只能删除和增加。映射 底层实现 是否有序 数值是否可以重复 能否更改数值 查询效率 增删效率 std::map 红黑树 key有序 key不可重复 key不可修改 O ( l o g n ) O(logn) O(logn) O ( l o g n ) O(logn) O(logn) std::multimap 红黑树 key有序 key可重复 key不可修改 O ( l o g n ) O(logn) O(logn) O ( l o g n ) O(logn) O(logn) std::unordered_map 哈希表 key无序 key不可重复 key不可修改 O ( 1 ) O(1) O(1) O ( 1 ) O(1) O(1) -
Q:map映射的底层实现及性质
std::unordered_map
底层实现为哈希表,而std::map
和std::multimap
底层为红黑树,所以key值是有序的,但key不可以修改,改动key值会导致整颗树的错乱,所以只能删除和增加。 -
Q:为什么优先选择unordered_set作为哈希表,什么情况下用multiset和set
std::unordered_set
的查询和增删效率是最优的( O ( 1 ) O(1) O(1)),如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。 -
Q:相比于set,map的(key,value)数据结构优点:
map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。 -
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
-
Q:为什么set,multiset,map,multimap底层是红黑树却可以作为哈希表?
虽然std::set
、std::multiset
的底层实现是红黑树,不是哈希表,std::set
、std::multiset
使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
-
Let’s Code
242.有效的字母异位词
通过数组构建哈希表,英文字母是26个,通过将字母映射到26个连续的空间中即可完成哈希表,数组内存就是连续的。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {};
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 (auto character : record) {
if (character != 0) return false;
}
return true;
}
};
349.两个数组的交集
建议:本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。
-
关于set的说明:C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和set封装了二叉树等,在封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。
-
关于set,必须说明的是set 关联式容器。 set作为一个容器也是用来存储同一数据类型的数据类型,并且能从一个数据集合中取出数据,在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。应该注意的是set中数元素的值不能直接被改变。C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树,所以被STL选择作为了关联容器的内部结构。
-
关于set的几个问题:
- Q:为何map和set的插入删除效率比用其他序列容器高?
对于关联容器来说,不需要做内存拷贝和内存移动,set容器内所有元素都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针指向其他节点也OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。
A
/\
C D
/\ /\
EF GH - Q:为何每次insert之后,以前保存的iterator不会失效?
iterator这里就相当于指向节点的指针,内存没有变,指向内存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。 - Q:当数据元素增多时,set的插入和搜索速度变化如何?
在set中查找是使用二分查找,也就是说,如果有16个元素,最多需要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。你明白这个道理后,就可以安心往里面放入元素了。
- Q:为何map和set的插入删除效率比用其他序列容器高?
-
关于set的使用方法:
方法 作用 begin() 返回set容器的第一个元素 end() 返回set容器的最后一个元素 clear() 删除set容器中的所有的元素 empty() 判断set容器是否为空 max_size() 返回set容器可能包含的元素最大个数 size() 返回当前set容器中的元素个数 rbegin 返回的值和end()相同 rend() 返回的值和rbegin()相同
题目描述
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序。
题解思路
显然使用哈希查找,最好构建哈希表的时候能够去除重复的元素。
这里使用unordered_set,因为unordered_set在构建时不用额外耗时排序。
我的代码:
相比于代码随想录本方法在最后少用了一次循环,通过set的erase方法来实现在查找的循环中边找边加入输出result。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector<int> inter;
set<int> set1(nums1.begin(), nums1.end());
// for (auto i : nums1) {
// set1.insert(i);
// }
for (auto mem : nums2) {
if (set1.find(mem) != set1.end()) {
inter.push_back(mem);
set1.erase(mem);
}
}
return inter;
}
};
代码随想录代码
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
202.快乐数
题目描述:
编写一个算法来判断一个数 n 是不是快乐数。
题解思路
主要是观察到是否为快乐数的条件:即按照题目的方式进行运算后是否会陷入循环。
我的代码
class Solution {
public:
bool isHappy(int n) {
// set<int> nums;
unordered_set<int> nums;
while (n != 1) {
nums.insert(n);
n = calculateNum(n);
if (nums.find(n) != nums.end()) return false;
}
return true;
}
// 按照快乐数的运算法则计算
int calculateNum (int n) {
int calNum = 0;
while (n != 0) {
calNum += (n % 10) * (n % 10);
n /= 10;
}
return calNum;
}
};
1.两数之和
题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
题解思路
关于这道题,很容易想到双指针法,但是题目要求返回满足的元素下标,这也注定我们不能通过提前排序后使用双指针,因为这样会改变下标。
因此使用map构造哈希表,通过value来存储数组下标。
我的代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> maps;
for (int i = 0; i < nums.size(); ++i) {
if (maps.find(target - nums[i]) != maps.end()) {
return {i, maps.find(target - nums[i])->second};
}
maps.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
Q
Q:对于哈希表常用的三种结构
Q:set集合的底层实现及性质
Q:map映射的底层实现及性质
Q:为什么优先选择unordered_set作为哈希表,什么情况下用multiset和set
Q:相比于set,map的(key,value)数据结构优点
Q:为什么set,multiset,map,multimap底层是红黑树却可以作为哈希表?
Q:为何map和set的插入删除效率比用其他序列容器高?
Q:为何每次insert之后,以前保存的iterator不会失效?
Q:当数据元素增多时,set的插入和搜索速度变化如何?