理论基础
hash table(哈希表、散列表)
-
用来**快速判断某个元素是否在集合里的时候,用,**查找效率O(1)牺牲空间换时间
-
元素映射到哈希表index需要哈希函数,hash function
-
如果映射出现冲突则会发生哈希碰撞,hash collisions,一般采用拉链法或线性探测法
- 拉链法,冲突位置用链表,将冲突元素存储其中,避免一次申请过大内存
- 需要table size大于dataSize,冲突的话接着往下找合适位置
常见三种结构
- 数组
- set(集合)
| set | 底层实现 | 有序否 | 数值是否可重复 | 能否改值 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
| std::set | 红黑树 | 有 | 否 | 否 | O(longN) | O(longN) |
| std::multiset | 红黑树 | 有 | 是 | 否 | O(longN) | O(longN) |
| std::unordered_set | 哈希表 | 否 | 否 | 否 | O(1) | O(1) |
- 红黑树是一种平衡二叉搜索树,所以key值有序,但key不可改,改动会导致整个树错乱
- 需要用集合时,优先unordered_set,查找增删最优,如果有序则set,如果还要重复数据就multiset
- map(映射)
| map | 底层实现 | key有序否 | key是否可重复 | key能否改值 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
| std::map | 红黑树 | 有 | 否 | 否 | O(longN) | O(longN) |
| std::multimap | 红黑树 | 有 | 是 | 否 | O(longN) | O(longN) |
| std::unordered_map | 哈希表 | 否 | 否 | 否 | O(1) | O(1) |
- map是key value对,对key有限制,value没有,因为key的存储方式采用红黑树
- 虽然set multiset底层实现是红黑树,不是哈希,而且用红黑树来索引和存储,但是我们使用的方式仍然是key value,所以还是称哈希法,map也是这个原理
心得
- 上手就是map,而且对unordered_map基本操作不熟悉
- 应该针对不同情况具体分析,某些优化的点进行思考,如为什么用一个数组即可,如全是小写字母暗示数据量少使用数组
题解
- 当前数据量少,统计数组一个即可,同时需要注意需要赋初始值,最好以后写代码都显式赋值,养成好习惯,否则很有可能用的垃圾值
class Solution {
public:
bool isAnagram(string s, string t) {
int hash[26] = {0}; // 由于小写仅26个即可,定义一个即可,新增++,减--即可
for (int i = 0; i < s.size(); i++){
hash[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++){
hash[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++){
if (hash[i] != 0){
return false;
}
}
return true;
}
};
心得
- 看到交集想到的是,交集和剩余元素组成原数组,类似set感觉,但是其实2个块间的set,并没有什么用,导致误导,应该仔细审题,尤其是记录类,尽量往哈希上靠
- 同时想用set发现容器基本操作不熟,需要加强基本知识学习
题解
- 本题原来数组大小未做限制,而且强调返回结果唯一,结果考虑set,重点考虑set的思路
- 注意for循环的不需要索引的简介写法和遍历的写法find,以及容器的插入操作和容器间的相互转换
- 限制大小有限制也可以用数组做
// 使用set
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;
unordered_set<int> nums1_set(nums1.begin(), nums1.end());
for (int num : nums2){
if (nums1_set.find(num) != nums1_set.end()){
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
// 使用数组
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result;
int hash[1001] = {0};
for (int num : nums1){
hash[num] = 1;
}
for (int num : nums2){
if (hash[num] == 1){
result.insert(num);
}
}
return vector<int>(result.begin(), result.end());
}
};
心得
- 看到题目想的是结果是1xxx0的形式是快乐数,然后往勾股定理那边偏,但是忽略无限循环的含义
- 还有没有完全理解题意,对于无限循环有误解,其实只要找到重复的第一个无限循环的数字,其他位置会按照之前的结果依次开始
- 涉及到求和一定要注意初始化,否则可能会存在起始值不对的问题
题解
- 两个结果一个是1应该是无限循环,对于结果1的情况,仔细处理好取每位值求平方,无非取余 除法运算
- 对于循环,由于长度待定,且需要查询才能知道是否重复,所以用哈希记录,出现循环立刻返回false
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;
}
}
};
心得
- 知道用hash但是 不会写iter,包括返回两个元素的自动转换效果,基本语法不熟练,即使知道算法无从下手
题解
- 为什么用哈希,需要记录下来之前的值和索引,反复查询,涉及此哈希最好,同时保存数值和索引因此用map
- 为什么用key是值而不是索引,因为查询的就是key
- 注意auto 返回的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]);
if (iter != map.end()) {
return {iter->second, i};
}
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};