写在前边的话
今天是哈希表学习的第一天,感觉哈希表的概念在脑袋里还是没能形成很好的体系,虽然对于python中的哈希结构有了解,但是却没有对这个结构做深入的学习,只是会用而已,其实也不太清楚什么时候使用这个结构会比较好以及这个结构的优点,希望通过今天的基础学习以及相关算法练习,能形成清晰的体系,要加油呀!
哈希表理论基础
什么是哈希表
也被称为散列表,用于实现关联数组或字典的抽象数据类型。
通过哈希函数将键(key)转换成一个数组的索引,从而快速存取与该键相关的值。
哈希表的组成
1. 哈希函数:一个将键映射到数组中某个位置的函数。理想的情况下,哈希函数应该将不同的键均匀分布在整个数组中,以减少键值对的冲突(即被映射到同一个位置)。
2. 数组:用来存储数据的实际数据结构。数组中的每个位置都可以存储一个键值对,其中每个位置称为一个“槽”或“桶”。
3. 冲突(哈希碰撞)解决策略:当两个或更多的键被哈希到同一个位置的时候,就会发生冲突。常见的冲突解决策略有拉链法(又叫链地址法,在每个数组位置上使用链表)和开放寻址法(如:线性探测法,在哈希表中找到下一个可用的空槽位来存储冲突元素)。
工作原理
当一个键值对要插入到哈希表中的时候:
1. 通过哈希函数计算键的哈希值
2. 将该值映射到数组的一个位置上
3. 如果该数组中的位置未被占用,将键对映的值放在这个位置;如果该位置已经被占用(哈希碰撞现象),就需要使用冲突解决策略来决定如何处理。
哈希表优点
1. 快速查找:平均情况下的查找、插入和删除操作的时间复杂度是O(1),在最坏的情况下(所有键都哈希到一个位置),时间复杂度是O(n)。
说明:哈希表的删除操作具体取决于采用了什么样的冲突解决策略。链地址法是定位到键的哈希值以后遍历链表进行删除,删除完成以后会更新元素计数;开放寻址比如线性探测,执行删除操作的时候,找到实际位置后,通常不会立即物理删除这个条目,而是将其标记为“已删 除”状态,同时会更新 哈希表中活动元素的计数,但不会改变数组的大小。 这是因为,如果物理删除一个条目,那么任何后续的搜索或插入操作都可能跳过这个位置,导致错误的结果。在线性探测中,删除一个项可能会导致之后用的搜索无法找到本应存在的项。因此, 一般是保留着位置供后续的插入操作使用。
2. 灵活的键类型:哈希表可以使用各种类型的数据作为键,只要这些类型可以通过哈希函数转换成一个整数值就可以了。
3. 动态扩容:许多哈希表的实现支持动态调整大小,可根据数据量的变化自动扩大或缩小存储空间。
哈希表缺点
1. 哈希冲突:需要额外的策略来解决冲突。
2. 内存消耗:通常需要额外的空间来存储键的哈希值和解决冲突,导致更多的内存消耗。
3. 数组扩展问题:基于数组的哈希表在创建以后难以扩展,当需要扩容时,可能需要重新哈希所有元素到新的数组,就会比较耗时。
4. 不支持有序操作:与排序的数组相比,哈希表不支持范围查询或者查找最值等有序操作。
Python中常用的哈希表
1. 字典dict
1)特性:动态键值对(可动态添加、删除键值对);不可变键(键必须是不可变类型,如:字符串、数字或元组);可变值(值可以是任何数据类型)。
2)内部结构:底层是一个哈希表。
3)冲突解决策略:使用二次探测(是一种开放寻址策略)和伪随机跳跃(跳跃的距离不是固定的二次方增长,而是基于伪随机数生成的)策略来解决冲突。
4)动态调整:python的字典在负载因子达到一定阈值时就会自动调整大小,以保持高效。
负载因子:是表中的元素数量除以哈希表大小。当负载因子过高时,冲突增加,效率就会降低。
4)效率:查找、读取、插入和删除的时间复杂度平均情况下是O(1),最坏情况下是O(n)。要是迭代的话就是O(n)了。
2. 集合set
1)特性:无序性;唯一性(集合不允许有元素重复,相同元素会被映射到相同位置);存储的是元素而不是键值对。
2)内部结构:是基于哈希表实现的,与字典解决冲突的方式一样。
3)动态调整:也支持动态调整与dict一样。
4)效率:同dict,不同的是集合有它自己的集合运算,如并集、交集等,通常具有O(min(len(s), len(t)))的时间复杂度,s、t是参与运算的集合。
Java中常用的哈希表
1. HashMap
1)特性:键值对存储,键唯一,值可以重复。键是不可变对象。
2)内部结构:基于哈希表实现。
3)冲突解决策略:拉链法(链到一个链表或树中)。
3)动态调整:支持动态调整。
4)效率:平均时间复杂度是O(1)。
2. HashSet
1)特性:使用键作为唯一标识符,值被设置成null或一个静态对象;存储元素唯一。
2)内部结构:基于哈希表实现,实际上是HashMap的一个封装。
3)冲突解决策略:拉链法(链到一个链表或树中)。
3)动态调整:支持动态调整。
4)效率:平均时间复杂度是O(1)。
什么时候用哈希法
在算法设计中,如果是快速检查某个元素是否出现过的场景,就会用到哈希法。
242. 有效的字母异位词
题目链接
题目难度
简单
看到题目的第一想法
看到题目的时候,使用python的话我的第一想法是首先检查字符串的长度是否相等来进行剪枝,然后的话我会对将字符串转换成列表并利用sorted()进行排序,然后进行比较是否相等。这样的写法平均时间复杂度是O(nlogn),空间复杂度是O(n)(由于需要额外的存储空间)。
看完代码随想录以后的总结
解题思路
1. 可以使用数组,时间复杂度和空间复杂度都较低。
2. 如果使用python字典或者java中的hashmap的话,如果字符串的长度很大的话时间和空间的复杂度都会有明显的增加。
文章讲解
视频讲解
代码编写
Python
由于今天学习的是哈希表,所以我自己就基于字典写了一种算法。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
# 如果长度不一样就返回
if len(s) != len(t):
return False
dic = {}
# 获取s中各个字母及其数量,保存在dic中
for i in s:
dic[i] = dic.get(i, 0) + 1
# 获取t中各个字母及其数量
for j in t:
if j not in dic: # 如果t中包含s中没有的字母直接返回false
return False
dic[j] -= 1 # 如果t中含有s中有的字母数量就减减,数量小于0了,就返回false
if dic[j] < 0:
return False
return True
时间复杂度:O(m+n) m, n 分别为s, t的长度。
空间复杂度:O(m)需要使用额外的字典空间存储s中的字母及其数量。
看完代码随想录看到可以使用列表
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
l = [0] * 26
ord_a = ord("a")
for i in s:
l[ord(i) - ord_a] += 1
for i in t:
l[ord(i) - ord_a] -= 1
for i in l:
if i < 0:
return False
return True
时间复杂度:O(m+n)
空间复杂度:O(1) 由于是常量大小的数组,所以空间复杂度是O(1)。
Java
使用Hashmap
class Solution {
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()){
return false;
}
HashMap<Character, Integer> map = new HashMap<>(); //注意java中键是对象,所以不能使用char,而需要使用包装类Character
for(int i=0; i<s.length(); i++){
int count = map.getOrDefault(s.charAt(i), 0) + 1;
map.put(s.charAt(i), count);
}
for(int i=0; i<t.length(); i++){
int count = map.getOrDefault(t.charAt(i), 0);
if(count == 0){
return false;
}else{
map.put(t.charAt(i), count-1);
}
}
return true;
}
}
时间复杂度:O(m+n) m, n 分别为s, t的长度。
空间复杂度:O(m)需要使用额外的map空间存储s中的字母及其数量。
使用数组
class Solution {
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()){
return false;
}
int[] l = new int[26];
for(int i=0; i < s.length(); i++){
l[s.charAt(i) - 'a']++;
}
for(int i=0; i < t.length(); i++){
l[t.charAt(i) - 'a']--;
}
for(int c: l){
if(c != 0){
return false;
}
}
return true;
}
}
时间复杂度:O(m+n)
空间复杂度:O(1) 由于是常量大小的数组,所以空间复杂度是O(1)。
349. 两个数组的交集
题目链接
题目难度
简单
看到题目的第一想法
看到这个题目python的话,我会首先将两个列表去重,然后循环其中一个集合检查元素是否在另一个集合中出现,如果出现的话就保存在新的列表中(集合+数组)。
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
snums1 = set(nums1)
snums2 = set(nums2)
res = []
for num in snums1:
if num in snums2:
res.append(num)
return res
- 时间复杂度:O(2m+n) m,n分别是两个数组的长度
- 空间复杂度:O(m+n+p),p为两个数组的交集的长度
缺点:需要对两个数组进行set操纵时间复杂度会比较高,而且两个数组都需要额外的set以后的数据空间,空间复杂度也很高。
看完代码随想录以后的总结
解题思路
1. 可以使用字典和集合的组合
2. 可以使用数组,由于题目中设置了数值范围
文章讲解
视频讲解
代码编写
python
字典+集合
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
dic = {}
for num in nums1:
dic[num] = dic.get(num, 0) + 1
res = set()
for num in nums2:
if num in dic:
res.add(num)
del dic[num]
return list(res)
- 时间复杂度:O(m+n)
- 空间复杂度:O(m+min(m, n))
使用数组
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
count1 = [0] * 1001
count2 = [0] * 1001
for num in nums1:
count1[num] += 1
for num in nums2:
count2[num] += 1
res = []
for i in range(1001):
if count1[i] * count2[i] != 0:
res.append(i)
return res
- 时间复杂度:O(m+n)
- 空间复杂度:O(min(m,n)) 最坏情况
Java
Hashset
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> nums1Set = new HashSet<>();
Set<Integer> resSet = new HashSet<>();
for(int i: nums1){
nums1Set.add(i);
}
for(int i: nums2){
if(nums1Set.contains(i)){
resSet.add(i);
}
}
int[] res = new int[resSet.size()];
int index = 0;
for(int i: resSet){
res[index++] = i;
}
return res;
}
}
- 时间复杂度:O(m+n)
- 空间复杂度:O(min(m, n)) 最坏情况
数组
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] lnums1 = new int[1001];
int[] lnums2 = new int[1001];
for(int i: nums1){
lnums1[i]++;
}
for(int i: nums2){
lnums2[i]++;
}
List<Integer> resList = new ArrayList<>();
for(int i=0; i<1001; i++){
if(lnums1[i] > 0 && lnums2[i] > 0){
resList.add(i);
}
}
int index = 0;
int[] res = new int[resList.size()];
for(int i: resList){
res[index++] = i;
}
return res;
}
}
- 时间复杂度:O(m+n) (虽然有遍历交集数量的数组,时间复杂度是O(m+n+min(m,n))由于后边的增长率在m,n很大时远小于m+n,所以整体的时间复杂度仍为O(m+n)).
- 空间复杂度: O(min(m, n)) 最坏情况
Hashmap
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
HashMap<Integer, Integer> hash = new HashMap<>();
for(int i: nums1){
int count = hash.getOrDefault(i, 0); //获取i的数量不存在赋0
hash.put(i, count+1);
}
Set<Integer> resSet = new HashSet<>();
for(int i: nums2){
if(hash.containsKey(i)){
resSet.add(i);
}
}
int[] res = new int[resSet.size()];
int index = 0;
for(int i: resSet){
res[index++] = i;
}
return res;
}
}
- 时间复杂度:O(m+n)
- 空间复杂度:O(m+min(m, n))
202. 快乐数
题目链接
题目难度
简单
看到题目的第一想法
有了前边题目的铺垫,看到这道题就想到用哈希表存储各个位数的和,然后有重复的出现则说明不是欢乐数,如果出现和为1则为快乐数。
看完代码随想录以后的总结
解题思路
1. 使用集合,利用和如果不是快乐数的话会循环出现。
文章讲解
代码编写
python
class Solution(object):
def isHappy(self, n):
"""
:type n: int
:rtype: bool
"""
res = set()
while True:
num_sum = 0
str_n = str(n)
for i in str_n:
num_sum += int(i) ** 2
if num_sum == 1:
return True
if num_sum in res:
return False
n = num_sum
res.add(n)
- 时间复杂度:O(logn) (O(logn) * O(k) 其中logn是整数的位数,k是最大迭代次数,是有限的,所以总的复杂度就可以写为O(logn))
- 空间复杂度: O(1) (空间复杂度是O(k)k是常数,所以实际上可以写成O(1))
Java
class Solution {
public boolean isHappy(int n) {
Set<Integer> hash = new HashSet<>();
String s = "";
while(true){
int sum = 0;
s = "" + n;
for(int i=0; i<s.length(); i++){
sum += (s.charAt(i) - '0')*(s.charAt(i) - '0');
}
if(hash.contains(sum)){
return false;
}
if(sum == 1){
return true;
}
n = sum;
hash.add(n);
}
}
}
- 时间复杂度 O(logn)
- 空间复杂度 O(1)
1. 两数之和
题目链接
题目难度
简单
看到题目的第一想法
由于要返回的是满足数据的下标,所以想到用python中的字典,存储出现过数据的值及索引。然后遍历数据的同时判断目标值和当前值的差值在字典中有没有出现过。
看完代码随想录以后的总结
解题思路
1. 使用哈希表(python 字典,java HashMap)
2. 存储值和下标键值对,并在哈希表中查找target-当前值的差有没有在哈希表中出现
文章讲解
视频讲解
代码编写
python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
dic = {}
for i in range(len(nums)):
if target - nums[i] not in dic:
dic[nums[i]] = i
else:
return [dic[target - nums[i]], i]
return []
- 时间复杂度:O(n)
- 空间复杂度: O(n)
Java
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> hash = new HashMap<>();
int[] res = new int[2];
for(int i=0; i<nums.length; i++){
if(hash.containsKey(target - nums[i])){
res[0] = hash.get(target - nums[i]);
res[1] = i;
}else{
hash.put(nums[i], i);
}
}
return res;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
今日总结
今天是哈希表开始的第一天,对哈希表的理论知识进行了梳理,算法题目python写着题目感觉还可以,java的话有些吃力,不太了解java哈希表的相关操作,不过四道题下来已经有很多收获了,相信接下来的训练还会加深理解和熟练度的。