本文图源:代码随想录link
今日任务 :
- 哈希表理论基础
- 242.有效的字母异位词
-
- 两个数组的交集
-
- 快乐数
-
- 两数之和
当遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法
哈希表理论基础
Hash Table,也称为散列表。
哈希表是根据关键码的值而直接进行访问的数据结构。
简单举例:数组,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射为哈希表上的索引,映射函数为hash function ,也就是哈希函数。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于 哈希表的大小,也就是大于tableSize,怎么办呢?
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就保证了学生姓名一定可以映射到哈希表上。
此时问题是如果学生的数量大于哈希表的大小怎么办,就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置。
接下来哈希碰撞登场
一般哈希碰撞有两种解决方法, 拉链法和线性探测法
拉链法
小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样就可以通过索引找到小李和小王
数据规模是dataSize, 哈希表的大小为tableSize
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
** 线性探测法**
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据。如图所示:
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
下图为C++中的集合和映射的底层实现和优劣表现
题外话:因为随想录主要以C++为主,所以我都是看C++的code来写python的,一个没学过C++的人,现在除了一些基本规则不知道以外,我竟然能看懂C++的code了,这对我来说是本次学习意想不到的收获!
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
242.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1: 输入: s = “anagram”, t = “nagaram” 输出: true
示例 2: 输入: s = “rat”, t = “car” 输出: false
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/valid-anagram
- 法一:哈希表法
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
record = [0]*26 # 创建一个空列表,用来存储26个字母出现的次数
# 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
for i in range(len(s)):
# 作差这里很妙,正好对应新列表的下标,0~25
record[ord(s[i]) - ord("a")] += 1
for i in range(len(t)):
record[ord(t[i]) - ord("a")] -= 1
for i in range(26):
# record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
if record[i] != 0:
return False
return True
- 法二:使用 collections.defaultdict 类
可参考https://blog.csdn.net/sGDUTBMW/article/details/108962367
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
from collections import defaultdict
# int()没有参数返回值为0,也就是说这里的s_dict的key对应的value默认值为0
s_dict = defaultdict(int)
t_dict = defaultdict(int)
for x in s:
s_dict[x] += 1
for x in t:
t_dict[x] += 1
return s_dict == t_dict
如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费!
349. 两个数组的交集
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
法一:自己做的
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
result = []
for i in nums1:
if i in nums2:
result.append(i)
result = list(set(result))
return result
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2))
202. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/happy-number
class Solution:
def isHappy(self, n: int) -> bool:
def get_sum(n): # 求和函数
sum = 0
while n:
sum += (n % 10)**2 # n%10 n%100...来拆分一个整数的各个位置上的数
n //= 10 # 比如111,111%10=1,111//10=11,11%10=1,11//10=1,1%10=1,1//10=0,结束循环
return sum
res = set() # 定义一个空集合,用集合就是检测下面的循环有没有进入死循环
while True:
n = get_sum(n)
if n == 1:
return True
elif n in res: # 如果中间结果重复出现,就为死循环,return False
return False
else:
res.add(n)
1. 两数之和
回归leetcode第一题!
map中的存储结构为 {key:数据元素,value:数组元素对应的下标}
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
dic = {} # 创建一个空字典,用来记录遍历过的值和对应下标,key=num,value=index
for idx, num in enumerate(nums): # 枚举法的好处,不用再根据下标取值
d = target - num # 求与num匹配的值,也就是d+num=target
if d in dic: # 判断d是否在dic字典里,在就返回当前num的下标和d对应的下标
return [idx, dic[d]]
else: # 不在,就将当前遍历到的num和对应下标记录在dic里
dic[num] = idx