哈希表理论基础:
哈希表:是一种根据关键码的值而进行直接访问的数据结构,例如数组就是一张哈希表,其中关键码就是数组的索引下标,通过下标可以直接访问数组中的元素,如下图:
哈希表的作用:一般用来快速判断一个元素是否出现在集合里,时间复杂度为O(1)
哈希函数:
例如查询一个学生名字,是否在这个学校里,哈希函数(hash function) 把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道结果。
哈希函数如下图所示,通过hashCode把名字转化为数值,一半hashCode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于哈希表的大小了,也就是大于tableSize了,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会再次对数值做一个取模的操作,这样我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
这就是哈希碰撞。
如下图所示,小李和小王都被映射到了索引下标1的位置,这一现象叫做哈希碰撞
一半哈希碰撞有两种解决办法,拉链法和线性探测法。
拉链法:
对于发生碰撞的小李和小王,发生冲突的元素都被存储在链表中,这样我们就可以通过索引找到小李和小王了
(数据规模是dataSize, 哈希表的大小为tableSize)
拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间
线性探测法:
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
常见的三种哈希结构:
当我们想使用哈希法来解决问题的时候一般用三种数据结构:数组,set(集合),map(映射)
题目链接:242. 有效的字母异位词 - 力扣(LeetCode)
思路:
首先是暴力解法,两层for循环,同时要记录字符是否重复出现,时间复杂度为O(n²)
更优的方式,数组就是一个简单的哈希表,而且在此题中只有小写字符,那么可以定义一个数组,来记录字符串s中字符出现的次数,只需定义record大小为26即可,初始化为0,因为字符a到字符z的ASCⅡ码也是26个连续的数值。
在遍历字符串s时,用s[i]的ASCⅡ码减去字符“a”的ASCⅡ码,即得到 [0, 25]之间的一个整数,再让record对应位置+1;遍历字符串t时同理record对应位置-1,最后判断得到的record数组中有无非零元素即可完成。
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
record = [0] * 26
for i in s:
# 并不需要记住字符a的ASCⅡ码,只要求出相对数值就可以了
record[ord(i) - ord("a")] += 1 # ord(i)为求字符i的Unicode编码
for i in t:
record[ord(i) - ord("a")] -= 1
for i in range(26):
if record[i] != 0:
# record数组如果有不为0的元素,则一定有一个多了或少了字母
return False
return True
题目链接:349. 两个数组的交集 - 力扣(LeetCode)
思路:
注意题目特别要求输出结果中的每个元素都是唯一的,那么就要求输出的结果是去重的,同时可以不考虑输出结果的顺序,这道题暴力解法时间复杂度为O(n²)
在C++中可以使用set结构来解决问题,在python中则是使用字典。
首先定义字典val_dict来存放nums1中出现的数值,并完成了去重处理,再判断是否有既在val_dict中出现又在nums2中出现的数值,存入数组ans中,最后返回ans
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
val_dict = {}
ans = []
for num in nums1:
val_dict[num] = 1
for num in nums2:
if num in val_dict.keys() and val_dict[num] == 1:
ans.append(num)
val_dict[num] = 0
return ans
思路:
题目中提到可能会无限循环,也就是求和过程中中间值eql会重复出现,遇到这种需要快速判断一个元素是否在集合里,就要考虑哈希法,判断eql是否重复出现,若是则返回false,不是则继续计算。
# 版本一
class Solution:
def isHappy(self, n: int) -> bool:
eql = 0
val = {}
while(n > 1):
if n in val.keys() and val[n] == 1: # 如果等于中间结果说明陷入循环返回false
return False
val[n] = 1 # 记录中间结果
eql = (n % 10) ** 2
while(n > 1):
n = n // 10
eql += (n % 10) ** 2
n = eql
return True
# 版本二__代码随想录
class Solution:
def isHappy(self, n: int) -> bool:
def calculate_happy(num):
sum_ = 0
# 从个位开始依次取,平方求和
while num:
sum_ += (num % 10) ** 2
num = num // 10
return sum_
# 记录中间结果
record = set()
while True:
n = calculate_happy(n)
if n == 1:
return True
# 如果中间结果重复出现,说明陷入死循环了,该数不是快乐数
if n in record:
return False
else:
record.add(n)
思路:
首先暴力解法为两层for循环,时间复杂度为O(n²)
在python中哈希法的使用则可以利用字典来完成,利用key来保存数值,key的值来保存索引号
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
records = dict()
for index, value in enumerate(nums):
if target - value in records: # 遍历当前元素,并在map中寻找是否有匹配的key
return [records[target- value], index]
records[value] = index # 遍历当前元素,并在map中寻找是否有匹配的key
return []
总结:
在遇到类似数值需要快速查找时,可以考虑使用哈希法,代替循环法使用可以节省很多时间