1.哈希表基础 Hash Table
1.映射
映射的五大核心方法
1.1 Hash Table 的定义
哈希表是根据关键码的值而直接进行访问的数据结构。哈希表也可以被称作是散列表
哈希表的底层实现是一个数组,哈希表是一个特殊的数组
1.1.1 Hash Tabel 的映射
在一个字典中,有了 Key 和 value 的值可以形成一个映射,可以先考虑一种有限制的设置,在这个设置中有 n 个元组,想要将这 n 个映射,就要定义一个比元组个数还要大的映射表用来表示这个映射,如下图所示:
在上图的情况下,将键值 k 对应的值映射在索引值也为 k 的位置上。但是这样存储的缺点就是:①需要定义的 N 要大于 n ,造成空间资源的浪费 ②在这种情况下想完成 O(1) 复杂度的搜索就要求键值必须是整数③有多个键值映射到相同的索引
当有多个键值映射到相同索引时就需要用桶数组
1.1.2 哈希函数定义
哈希函数 h 的目标是把每个键 k 映射到 [0,N-1] 区间内的整数,其中 N 是哈希表桶数组的容量而不用键 k 做索引,最后会在桶A[h(k)] 中存储元组(k,v),哈希函数由两个部分组成:①哈希码 ②压缩函数
1.1.3 发生冲突
如果有两个或者更多的键具有相同的哈希值,那么这两个不同的元组会映射到同一个桶中,在这种情况下就是发生了一次冲突
1.2 Hash Table 的使用场景
1.Python 中 Dict,Set 的构建就是用的数据结构 Hash Table(散列表) ,因为 Hash Table 可以实现映射
2.hash table 还可以快速判断一个元素是否出现在集合里
枚举方法时间复杂度:O(N)
哈希表时间复杂度:O(1)
因为哈希表有一个哈希函数,哈希函数可以将所有的元素都映射到一张表中,表明该元素存储在内存中的什么位置。在找某一个元素的时候根据哈希函数直接找到关键词在哈希表中的位置就可以一步定位到内存
1.3 Hash Table 会出现的问题
1.3.1index > hash table size
解决方法:最后会做一个取模的操作
1.3.2哈希碰撞
多个元素的索引都映射到了相同的位置
1.4 哈希碰撞的解决方法
一般有两种解决方法:拉链法和线性探测法
1.4.1拉链法
如果发生了冲突,冲突的元素都放在链表中,然后通过索引找到想要找的对象
拉链法应该选择适当的哈希表的大小,这样可以避免后面接的链表过长而导致查找时间也变长
1.4.2 线性探测法
线性探测法一定要保证哈希表的表长发大于等于数据的个数,这样才能保证当发生冲突的时候可以有多余的空位存放冲突元素
1.5 Hash Table 的实现方式
1.数组:判断某元素是否存在并计数,要知道要保存的数据的长度
2.set :判断某元素是否存在,
3.Dict:判断某元素是否存在并记录元素位置
1.6 关于容器常用的几种函数
1.6.1 关联性容器的常用函数
找到排序容器的最大值和最小值,这里得到的是 iterator ,要想取到值还要进行 * 处理
(1)最小值
set.begin();
(2)最大值
set.rbegin();
(3)寻找某个值
最后返回的是 iterator
st.find(nums[i])
1.7 xxx 之和总结
对于题目:454. 四数相加 II(哈希) 、18. 四数之和(双指针) ,322. 零钱兑换(动归) 及其衍生题目来说,这几个题目都会给一个或者多个数组,求组成 target 的几个元素
虽然目的都是与求组成 target 的子元素有关,但是这三个题目用了三种不同的算法,那么下面就是整理应该用什么算法解决该问题:
1.结果是否可以重复
结果可重复 — HashMap:
如果允许重复,那题目一般会给多个数组,比如两数之和,和四数之和,那么这时就用 Hash ,因为对于 Hash 来说他的 key 值不可能存放组成结果这么麻烦的东西,他的关键是搜索,所以如果是多个数组且可以重复就用 Hash
结果不可重复 — 双指针:
如果去重则先将数组排序然后再用双指针会更好,因为要排序还要考虑到排序的时间复杂度,所以一般不会给多个数组,一般是一个
2.是否是最优子问题
最优子问题,求最小 xxx 或者最大 xxx 用动态规划
2.LeecCode 相关题目
常用数据结构:
- 数组
- Set(集合)
- map(映射)
- set 和 map 的区别在于:map 是有 key 值的,set 没有
- set 的 value 值和 map 的 key 值是使用相应数据结构存储的,所以他们都不可以修改,只能增加和删除
- 和底层使用哈希表和使用红黑树的区别就在于哈希表是无序的,红黑树是有序的
- 能不使用 set 就不适用 set :因为 set 占的空间比数组大,并且速度比数组慢,因为要将每个值都映射到 hashmap 上
关于 key 值的查找,count 经常用于查找 map 或者 set 中该 key 值的元素有几个。但是 map,set key 值都是不能重复的,所以 count 就用于查找该值是否在 map 中
2.1_242 有效的字母异位词
2.1.1 算法描述
异构词:只需要关注有哪些字母出现过,这些字母出现了几次
2.1.2 Python&C++ 代码实现
1.Python 代码实现
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
# define hash table(list--Dict)
count_s = [0]*26
for c in s:
count_s[ord(c)-ord('a')]+=1
for c in t:
count_s[ord(c)-ord('a')]-=1
for i in range(len(count_s)):
if count_s[i]!=0:
return False
return True
2.C++ 代码实现
class Solution {
public:
bool isAnagram(string s, string t) {
int count[26]={0}; // 数组即可
for(char c:s){
count[c-'a']++;
}
for(char c:t){
count[c-'a']--;
}
// 判断是否每个数都为 0
for(int c:count){
if(c!=0) return false;
}
return true;
}
};
2.1.3时空复杂度
时间复杂度:O(N)
空间复杂度:O(1) 因为 Dict 是一个常数
2.1.4相关技巧
字母个数计数器:
count_s = [0]*26
for c in s:
count_s[ord(c)-ord('a')]+=1 # get the ASCII to index
2.2_349两个数组的交集
2.2.1 算法描述
输出结果答案中的值是唯一的,并且可以不考虑输出结果的顺序
首先可以如上一道题,使用数组做哈希的题目,实现一个计数器。
但是因为并不知道计数器中需要保存多少个数值,如果数值之间跨度比较大,数值比较少那么就会造成一种浪费
所以这里使用 set 最为计数器的存储结构
2.2.2 python&C++ 代码
1.C++ 代码实现
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int>set1(nums1.begin(),nums1.end());
set<int>res;
for(int num:nums2){
if(set1.find(num)!=set1.end()) res.insert(num);
}
return vector<int>(res.begin(),res.end());
}
};
易错点:
2.Python 代码实现
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
res_set = set()
set1 = set(nums1)
for i in nums2:
if i in set1:
res_set.add(i)
return list(res_set)
2.2.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(N)
2.3_202 快乐数
2.3.1 算法描述
这道题的关键在于两个地方:
①什么时候停止循环
这里肯定不能用 for 循环,因为不知道会判断几次,只能用 while
停止 while 循环条件:当 sums 重复出现的时候就要停止循环,所以要用 set 进行保存
②求各个位数之和
这个总结在本题技巧部分
2.3.2 C++&Python 代码实现
1.C++ 代码实现
class Solution {
public:
int getpos(int a){
int sums = 0;
while(a!=0){
sums+=(a%10)*(a%10);
a/=10;
}
return sums;
}
bool isHappy(int n) {
int val = getpos(n);
unordered_set<int>res;
while(1){
int val = getpos(n);
if(val==1) return true;
if(res.find(val)!=res.end()){ // val 在 set 中
return false;
}else{
res.insert(val);
n = val;
continue;
}
}
}
};
2.Python 代码实现
class Solution:
def isHappy(self, n: int) -> bool:
set_sums = set()
while 1:
sums = self.getSum(n)
if sums == 1:
return True
# if sums in set ,return False
elif sums in set_sums:
return False
else:
set_sums.add(sums)
n = sums
# get the every element of n
def getSum(self, n):
sums = 0
while n > 0:
sums += (n%10) * (n%10)
n //= 10
return sums
2.3.3 时空复杂度
时间复杂度:O()
2.3.4 技巧:得到一个数的每一位上的值
res = list()
def getSum(n):
sums = 0
while n > 0:
res.append(n%10)
n //= 10
2.4_1 两数之和
2.4.1 算法描述
1.C++ 算法描述
因为有 index 的参与所以需要保存数组下标。
然后判断 target-nums[i] 是否也保存在 map ,如果保存了就返回其 index ,如果没有找到就接着往下找
2.Python 算法描述
一共有三种数据结构可以选择:数组,Set,Dict
本题要是用的数据结构是 Dict
①不知道保存数据的长短,所以不能使用数组
②既要保存 value ,又要保存 index 所以不能用 Set
③所以选择 Dict
2.4.2 C++&Python 代码实现
1.C++ map 代码实现
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
// 定义一个 map
unordered_map<int,int> mp; // (value index)
// 遍历 nums
for(int i =0;i<nums.size();i++){
// 判断其被减数是否在 map 中
auto iter = mp.find(target-nums[i]);
if(iter!=mp.end()){
return {iter->second,i};
}
// 不在的话就要插入
mp.insert(pair<int,int>(nums[i],i));
}
return vector<int>(-1,-1);
}
};
2.Python 代码实现
方法1:Dict
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
dict_nums = defaultdict(int)
res = []
for i,v in enumerate(nums):
dict_nums[v] = i # key-->value in nums ; value-->index in nums
for i in range(len(nums)):
element = target-nums[i] # get the value
j = dict_nums[element] # find the whether in dict or not
if j and j!=i:
return [i,j]
方法2:双指针
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)):
sums = 0
for j in range(i+1,len(nums),1):
if((nums[i]+nums[j])==target):
return [i,j]
return None
2.4.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(N)
2.5_454四数相加2(media)
2.5.1 算法描述
1.C++ 算法描述
本题要求出现的次数所以在设置 Map 时设置的 Map 的值就不能是 index 。而是记录某种组合出现的次数
本来应该是有一个 四重 for 循环去类似于排列的方法进行一一计算,但是可以使用 Map 的方式减少 for 循环的次数
将 nums1 和 nums2 两两组合计算,这样就是一个双重 for 循环。
因为这里允许重复,所以比方说如果 n1+n2 == a 出现了 5 次,但是 n3+n4 只出现1 次,最后的结果还是 5 ,不是 1 。所以在求次数的时候可以看到,加的是总的出现次数
count+=map[0-(c+d)];
2.Python 算法描述
本来想用 list 实现,但是发现 list 只能统计一个数是否出现过,无法判断出现次数
要求次数,并且不知道要保存多少个数值。所以使用 Dict 的数据结构实现 HashTable
这里两两为一个大组进行判断,这样就可以转换成两个大组之间的计算
nums1,nums2 并成一个大组;nums3,nums4并成一个大组
Dict 中:key --> 两个元素相加的值 value:前面相加的结果一共出现几次
本题和下面的四数之和不太一样,这个题只需要返回次数即可,不需要知道具体的值,所以是可以重复的
2.5.2 C++&Python 代码实现
1.C++ 代码实现
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
// 1. 定义一个 map ,key 值是 a+b 有可能出现的所有值
std::unordered_map<int,int> map ; // key:累加和 ; value: 该值出现的次数
int count = 0;
// 将 AB 的累加放到第一个数组
for(int a:nums1){
for(int b:nums2){
map[a+b]++;
}
}
// 减去 CD 的 for 循环
for(int c:nums3){
for(int d:nums4){
if(map.find(0-(c+d))!=map.end()){ // 可以进行相减
count+=map[0-(c+d)];
}
}
}
return count;
}
};
2.Python 代码实现
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
hashmap = defaultdict(int) # 定义一个 map ,key-->可能出现的两数之和 value-->两数之和出现的次数,两两进行组合
for n1 in nums1: # 两两组合
for n2 in nums2:
if n1+n2 in hashmap:
hashmap[n1+n2] +=1
else:
hashmap[n1+n2] =1
count = 0
for n3 in nums3:
for n4 in nums4:
key = -n3-n4 # 取负
if key in hashmap:
count+=hashmap[key] # 可以是重复的,value 有几组就算几组
return count
2.5.3 时空复杂度
时间复杂度:O(N²)
空间复杂度:O(N)
2.6_383赎金信
2.6.1 算法描述
本题有一个将 ransomNote 的每个字符从 magazine 中的查找操作,所以想到使用 hash table 。
实现 hash table 有三种方法:①数组 ②set ③Dict
因为这里需要对字母出现的个数进行记录并且字母的个数有限,所以使用数组的方式对 字母和每个字母出现的个数进行记录
2.6.2 C++ & Python 代码实现
1.C++ 代码实现
数组实现字母的 map
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
// 出现的次数还要匹配
// magazine 的范围更广
int res[26] = {0};
// 将 mag 的字符放到 map 中
for(char c:magazine){
res[c-'a']++;
}
// 遍历 ran
for(char c : ransomNote){
if(--res[c-'a']<0){ // 用完了
return false;
}else continue;
}
return true;
}
};
方法1:list 实现
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
# record the str lenth of the letter
m_list = [0]*26
for c in (magazine):
m_list[ord(c)-ord('a')]+=1
for c in (ransomNote):
letter_index = ord(c)-ord('a')
m_list[letter_index]-=1
if m_list[letter_index]<0:
return False
return True
方法2:Dict 实现
class Solution:
def canConstruct(self, ransomNote: str, magazine: str) -> bool:
zimu_dict = defaultdict(int)
for c in magazine:
if c in zimu_dict:
zimu_dict[c]+=1
else:
zimu_dict[c]=1
for c in ransomNote:
if c in zimu_dict:
zimu_dict[c]-=1
if zimu_dict[c]<0:
return False
else:
return False
return True
2.6.3 时空复杂度
时间复杂度: O(N)
空间复杂度:O(N)
3.其他题目
3.1_49字母异位词分组
哈希表
3.1.1 算法描述
异构词是具有相同字母构成但是字母顺序不同的单词。在往常使用 hash 对 a 单词中字母出现的次数进行映射。
这里使用的是对单词 a 的结构当做 value 值进行映射,如果结构一样就保存到 res 中。避免了一个个数字母个数的麻烦
3.1.2 C++ 代码实现
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>>res; // 存放结果的数据结构
unordered_map<string,vector<string>> sig; // 对单词进行记录
for(int i =0;i<strs.size();i++){
string s = strs[i];
sort(s.begin(),s.end()); // 对一个单词进行排序
sig[s].push_back
(strs[i]); // 将具有相同结构的字符串当做 key 值放入
}
for(auto x:sig){x
res.push_back(x.second); // 将所有的 key 值放入
}
return res;
}
};
3.1.3 时空复杂度
3.2_128最长连续序列
3.2.1 算法描述
朴素的做法是:枚举nums中的每一个数x,并以x起点,在nums数组中查询后面连续的数 x + 1,x + 2,,,x + cur 是否存在。
假设查询到了 x + cur ,那么以 x 为起点的最长连续子序列长度即为 cur 。
向 1 ,100 ,200 都是起点
但是如果 x-1 也存在数组中那么对于 x 来说,x 肯定不是起点,所以如果 x 有 x-1 存在于数组中的话那 x 可以直接跳过,子算起点的子序列长度
因为要判断 x+1 ,x+2 …是否存在于数组中,这里使用 hash 的方式是快,并且在插入时最节约的
3.2.2 代码实现
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
// 使用 unordered_set 来解决
unordered_set<int> st(nums.begin(),nums.end());
int res = 0;
for(int i = 0;i<nums.size();i++){
if(!st.count(nums[i]-1)){
// 以该值为起点时的最长递增序列
int cur = 1;
while(st.count(nums[i]+cur)){
cur++;
}
res = max(res,cur);
}
}
return res;
}
};
3.2.3 时空复杂度
时间复杂度:O(N)
空间复杂度:O(N)
3.3_41. 缺失的第一个正数
3.3.1 算法描述
1.情况 1
本题是使用原地 hash 的算法做到时间复杂度为 O(N) 空间复杂度为 O(1)
在一般情况下排序的时间复杂度都是 O(nlogn) 但是有在一种情况下可以将时间复杂度降为 O(N),如:
对 [0,1,2,5,3,4] 使用 O(1) 进行排序
这种方法的大体思路就是将 index = i 的位置放置 i 这个值,这里使用一个 for 循环遍历整个数组
S1:看 nums[0] 是 0 不–> 是–> 不做任何操作
S2:看 nums[1],nums[2] 都是 1,2 所以不做任何操作
S3:看 nums[3] 是 3 不,不是–> nums[3] 是 5 ,所以物归原主,将 nums[3] 的 5 放在 nums[nums[3]] 的位置
S4:看 nums[4] 是 4 不,不是–> 将 nums[4] 放在它本应该在的位置 nums[nums[4]] 上
S5:。。。。。。
也就是在 for 循环遍历的时候首先判断 nums[i] 上是 i 嘛,如果不是那么就要将 nums[i] 上的值放在它本身的位置上也就是 nums[nums[i]] 上
2.情况 2:循环交换
但是我们并不能保证本题给的数组就是 [1,n] 所有数都存在,但是方法不变
[2,4,3,5,2] 这个题是没有 1 的而且还存在重复的数
排序过程是:
[2,4,3,5,2]–>[4,2,3,5,2]–>[5,2,3,4,2]–>[2,2,3,4,5]
为了防止两个 [2,2] 会一直循环,也就是形成了一个环路,也就是代表了 nums[i] 该放的位置和 nums[i] 是一样的
nums[i] != nums[nums[i]]
注意本题最后得到的 nums 如果不是 情况 1,那么我们得到的 nums 不一定是有序的,它只能看出 index = i 时 i 是否在那个位置上,如果那 i 这个值不在 index = i 上,说明 i 是确实的,因为我们是根据 index 定位值的,而且本题求最小正数,缺失的 i 就是最后答案
通过上面的描述我们所要做的就是一直交换,直到将 nums[i] 放在正确的 nums[nums[i]] 中,但不是所有的值都能交换,可以交换的情况如下
1.同理 nums[i]>n 的时候也是没有办法交换的,就像上面例子中的 100 一样
2.nums[i] 是负数,因为这里要将 nums[i] 放到 index 为 i 的地方,这里 nums[i] 都是负数了他也没有地方放,所以就不用交换
3.i 位置上的值不为 i
4.永久循环,就像上面的例子其中 [2,2] 这两个数会不断的交换,永无止境
每一次 while 循环都会有一个元素被放到正确的位置上
[0,1,2,5,3,4]
当 for 循环遍历到 5 时发现 i!=nums[i],然后交换位置
3.3.2 代码实现
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
// 每一次将一个数放在合适的位置
nums.insert(nums.begin(),0);
int n = nums.size();
for(int i = 0;i<nums.size();i++){
// 判断该数是否可以放在合适的位置或者其他的数是否可以放在他的位置
while(i!=nums[i]&&nums[i]>=0&&nums[i]<n&&nums[i]!=nums[nums[i]]) {
swap(nums[i],nums[nums[i]]);
}
}
// 从前向后遍历
for(int i = 1;i<n;i++) if(i!=nums[i]) return i;
return n;
}
};
2111

被折叠的 条评论
为什么被折叠?



