哈希表理论基础
文章讲解:https://programmercarl.com/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
哈希表是根据关键码的值而直接进行访问的数据结构。
index=hashFunction(name)
如果两个内容都映射到了同一个索引下标,就是哈希碰撞。一般哈希碰撞有两种解决方法, 拉链法和线性探测法。拉链法就是直接外挂在上面,线性探测法是依靠哈希表中的空位来解决碰撞问题。
常见的三种哈希结构是数组、set(集合)和map(映射)。
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
242. 有效的字母异位词
看起来挺简单的,我的第一反应是用字典存储每个字母出现的次数,然后对比两个字典是不是相同。。但是听起来好像有点傻,而且这也不是哈希表?如果用哈希表的话,是不是字母填在哈希表上,次数挂在上面?
直接看题解了:https://www.bilibili.com/video/BV1YG411p7BA/?vd_source=40781b3117fa72cc67da258629107b62
这里又提到暴力解法了,我发现每次提到暴力,都先要确定是几组for循环,我好像每次都有点写不清楚暴力的细节,以后要加强这方面练习!
题解里讲到是要定义一个长度为26的数组,然后统计第一个字符串中字母出现的频率,减去第二个字符串中字母出现的频率。这里还有一个之前不知道的知识点,就是a-z的ASCII码是连续的,所以可以用数字减ord(a),ord()
是一个 Python 内置函数,它用于返回一个字符的 ASCII 值或 Unicode 值(如果字符是 Unicode 的话)。这样操作,但是总感觉还有点魔幻,直接让我写代码我不敢写啊!去看答案了。文字答案在这:https://programmercarl.com/0242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC
代码部分在这里,ord(i)-ord("a")就是字母相对于a的偏移量,然后字母在s字符串出现一次就在该索引上+1,字母在t字符串上出现一次就在该索引上-1,只有所有元素全为0才是字母异位词。原理还是很简单的~
class Solution(object):
def isAnagram(self, s, t):
"""
:type s: str
:type t: str
:rtype: bool
"""
record = [0]*26
for i in s: #对于s的每个字母
record[ord(i)-ord("a")] +=1
for i in t:
record[ord(i)-ord("a")] -=1
for i in range(26): # 这里遍历整个字母表,保证稳妥
if record[i]!=0:
return False
return True
349. 两个数组的交集
这个题一眼看过去好像和上一道题有点类似,但是由于不计顺序,所以用列表好像不行。于是我去看了题解:
视频版:
https://www.bilibili.com/video/BV1ba411S7wu/?vd_source=40781b3117fa72cc67da258629107b62
我发现,这涉及一种哈希结构unordered_set,和我一开始想的一样。这种数据结构的底层是哈希值。我们其实先遍历num1放到哈希表里,然后遍历num2,看里面每个元素是否出现在哈希表里,如果出现过,就把这个元素放到result里。
这道题我们用set解决,我感觉距离写代码主要的困难在于,我好像不知道用python怎么写unordered_set,所以还是直接看解法了。我发现这个里面,table相当于哈希表,是用来给元素计数的,用的是字典格式,遍历nums1
,对于每个元素num
,如果它已经存在于table
中,则将其对应的值加1;如果不存在,则将其初始值设为0。res相当于上一段说到的result。然后对我来说比较难懂的一个点是table.get(num,0)为什么要加1,问了Claude,他说num如果在table里就会返回当前已经出现过的次数,+1就是把这一次也加上。
class Solution(object):
def intersection(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
:rtype: List[int]
"""
table={}
for num in nums1: # 第一个循环,统计num1中每个元素出现的次数
table[num]=table.get(num,0)+1
res = set()
for num in nums2:
if num in table: # 检查一个键是否在字典中,另一种方式就是get函数
res.add(num)
# del table(num) # 确保只出现一次,但是我发现这一行注释掉也没问题
return list(res)
还有一种数组解法。建立一个长度为1001的哈希数组,然后把num1的所有数字出现次数设定为1,再遍历num2,判断它有没有在这个哈希数组出现过。后来我看1001是因为力扣给了num的范围<1000。
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
count1 = [0]*1001
count2 = [0]*1001
result = []
for i in range(len(nums1)): # 给num1和num2分别一个循环,用户计数
count1[nums1[i]]+=1
for j in range(len(nums2)):
count2[nums2[j]]+=1
for k in range(1001):
if count1[k]*count2[k]>0: # 在count1和count2都出现了就记录在result里
result.append(k)
return result
还有一个很绝的,也是我一开始想到的解法哈哈哈,用到set用&取交集就行了。
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2))
202. 快乐数
这个题一眼看过去好像有点复杂,但我看题目介绍里说这道题和上一道差不多,而且我连视频题解都妹找到,有点疑惑。
文字教程:https://programmercarl.com/0202.%E5%BF%AB%E4%B9%90%E6%95%B0.html
原来,题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
于是去看源码了,自己解读一下:
class Solution(object):
def isHappy(self, n):
"""
:type n: int
:rtype: bool
"""
record = set()
while True: # while True是为了检测无限循环的情况,因为不知道循环几次才得到1,或者永远得不到1
n=self.get_sum(n)
if n ==1:
return True
if n in record: # 如果数字重复出现了,可以预计就会产生无限循环
return False
else:
record.add(n) # 出现过的数字都在集合里记录下来
def get_sum(self, n):
new_num = 0
# 从num最低一位开始循环,假设num是134,第一轮n和r就是13和4,第二轮是1和3,第三轮是0和1。以此类推,所以r是我们要平方加和的数
while n: # 这里判断的标准是n不为0
n, r = divmod(n, 10) #表达除法,输入是被除数和除数,输出是商和余数
new_num += r**2 # 把每一位平方加和
return new_num
然后又看到了第二种解法,这里是先设定一个while无限循环,看数字在record里有没有出现过(也对,出现过就可以直接排除了),再看平方和是不是等于1。然后这里平方和是通过字符串转换实现的。同理,这里record也可以直接换成数组,也成立。
class Solution:
def isHappy(self, n: int) -> bool:
# 创建一个集合用于记录出现过的数字
record = set()
# 使用while循环,当n不在record集合中时执行循环体
while n not in record:
# 将当前的n加入record集合
record.add(n)
# 初始化new_num为0,用于存储各位数字平方和
new_num = 0
# 将n转换为字符串,方便遍历每一位数字
n_str = str(n)
# 遍历n_str的每一个字符(数字)
for i in n_str:
# 将当前数字转换为整数,计算平方后累加到new_num
new_num += int(i) ** 2
# 如果new_num等于1,说明n是快乐数,返回True
if new_num == 1: return True
# 否则,将n更新为new_num,进入下一次循环
else: n = new_num
# 如果退出while循环,说明出现了循环,n不是快乐数,返回False
return False
后面居然还有三种解法,累了……不看了先
1.两数之和
这个题第一眼看过去好像暴力解法写两个循环遍历就行?
力扣原题:https://leetcode.cn/problems/two-sum/
视频讲解:https://www.bilibili.com/video/BV1aT41177mK/很神奇,老师说这道题是用map来解决的,和我想的好像不太一样。老师说,当需要判断一个元素是否出现过时,第一反应应该是哈希表。这个题在遍历的时候,需要判断一个元素是否之前遍历过(比如遇到了2,就想找到7是否遍历过)。选择哈希表结构方面,由于需要同时存储数组和数组下标,可以用map来存放。
听了一下解题思路感觉挺简单的,后面部分没听完就写出来了!
class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
record = dict() # 我发现我的第一反应是{},以后要记得用dict()
for index, value in enumerate(nums): #index和value一起遍历要用enumerate
if target-value in record:
return [record[target-value],index]
record[value]=index
return [] # 如果没有找到就返回空列表
感觉应该要总结一下,用数组和/set/map做哈希映射的适用范围和具体情况。
文字讲解:
https://programmercarl.com/0001.%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C.html