哈希表理论基础:
哈希表:
数组就是一张哈希表,所以可以借用数组来解释哈希表。哈希表使用关键码来访问元素,在数组中,关键码就是数组下标,可以通过下标直接访问数组中的元素。
哈希表解决的问题:一般都是用来快速判断一个元素是否出现在集合中
具体措施可以理解为:首先将待查找元素映射为哈希表上的索引,然后通过查询索引下标从而知道元素是否出现在集合中。并且此过程中耗费的时间非常短。
哈希函数:
哈希函数用来将待查找的元素映射为哈希表上的索引。以查找学生姓名是否出现在学校中作为例子。
哈希函数(hashFunction)通过hashCode将名字转化为数值。一般hashCode通过特定编!码方式,将待查找元素转化为索引数值。
为了保证映射得到的索引数值都位于哈希表上,所以会对hashCode得到的数值进行一个取模(求余)操作,从而确保学生姓名一定可以映射到哈希表上。
当学生数量 > 哈希表大小时,会出现不同的学生名字同时映射到哈希表同一个索引下标的位置(事实上,学生数量 < 哈希表大小时也可能会出现,这取决于hashCode)。这时出现的不同的学生名字映射到哈希表同一个索引下标的现象,叫做哈希碰撞。
哈希碰撞
比如说下面这种情况,小王和小明都映射到了索引为1的位置
一般来说,哈希碰撞的解决方法有两种,拉链法和线性探测法。
拉链法
不改变小王和小明映射的索引数值,在发生冲突的位置1,将小王和小明存储在链表中,从而我们可以通过首先计算出索引,再通过索引处的链表找到小王和小明。
拉链法的关键在于设置tablesize即哈希表的大小,保证既不会因为数组空置而浪费大量内存空间,也不会因为链表过长导致需要耗费大量时间用于查找。
线性探测法:
线性探测法主要依靠哈希表中的空位来解决问题,因此需要tablesize > datasize(数据规模)。
总结
当需要快速判断一个元素是否出现在集合中时,哈希法是一个比较好的实现方法。
哈希表是通过牺牲空间来实现时间上的提高。因为使用了额外的数组,set(集合)或者map(映射)来存放数据,从而实现了快速查找。
LeetCode 242.有效的字母异位词:
看到题目后的思路:
- 使用数组作为hash表
hash表应当能将相同的字符映射到同样的位置,因此数组下标只与字符有关。而题目给出字符串中只包含小写字母。因此我们采用大小为26的数组作为hash表(26个字母),下标0~26分别对应’a’ ~ ‘z’,而小写字母的ASCII码是连续的,因此将小写字母映射成为索引的方式为 x - ‘a’。x为小写字母的代指。
那么具体的实现思路为:首先遍历s,将数组下标为s[i] - 'a’对应的值 + 1。然后遍历 t,下标为t[i] - 'a’对应的值 - 1。最后判断hash数组中是否存在非0元素,如果存在,有字符串多出或者少了字母。
class Solution(object):
def isAnagram(self, s, t):
"""
:type s: str
:type t: str
:rtype: bool
"""
record = [0] * 26 # hash表
for i in s:
record[ord(i) - ord("a")] += 1 # python需要使用ord得到单个字符的ASCII码
for i in t:
record[ord(i) - ord("a")] -= 1
# 最后判断数组中是否存在非0元素
for apl in record:
if apl != 0:
return False
return True
- 使用字典作为hash表
因为将元素加入哈希表的同时需要统计出现的次数。如果直接用如下语句,会出现报错KeyError,因为 += 需要record[i]存在才能进行运算。而这个可以采用defaultdict()函数解决
record = dict()
for i in s:
record[i] += 1
defaultdict是collections模块中的一个函数,可以用来避免python中访问字典dict中不存在的key时出现的’KeyError’异常。
1)defaultdict的具体介绍
defaultdict是数据类型dict的一个子类,区别在于重写了missing(key)和增加了一个可写的对象变量default_factory
# 语法
collection.defaultdict([default_factory[, ...]])
# default_factory使用defaultdict传入的第一个参数进行初始化,其用于missing()方法中
missing(key)
① 如果default_factory为None,报出KeyError异常
② 如果default_factory不为None,会给不存在的key提供一个默认值,并将该值插入字典中。
③ (用法可以理解为根据给定的内容自动构造出一个相关的字典)
# 构造字典
from collections import defaultdict
① 字典的键对应的值为列表
list1 = [('a', 1), ('b', 2), ('c', 3), ('b', 3), ('c', 7)]
list_dict = defaultdict(list) #对于不存在的key,默认值为空列表
for key, value in list1:
# 首先对于不存在的key,创建了一个空list,然后对这个list添加了元素value。
list_dict[key].append(value)
print(list1_dict.items())
""" 打印结果
dict_items([('a', [1]), ('b', [2, 3]), ('c', [3, 7])])
"""
② 字典的值为int (可以用来计数)
str1 = "atee"
str1_dict = defaultdict(int)
for i in str1:
str1_dict[i] += 1
print(str1_dict.items())
"""
dict_items([('e', 1), ('a', 1), ('t', 1)])
"""
items函数解释链接
使用defaultdict()的方法:
首先使用该方法对两个字符串构造对应的字典,然后比较两个字典是否相等
class Solution(object):
def isAnagram(self, s, t):
from collections import defaultdict
s_dict = defaultdict(int)
t_dict = defaultdict(int)
for i in s:
s_dict[i] += 1
for i in t:
t_dict[i] += 1
return s_dict == t_dict
实现过程中遇到的问题:
只想到了 t 能不能匹配 s 的情况,没有想到 s 可能比 t 的字母异位体多了字母。
LeetCode 349.两个数组的交集:
看到题目的思路:
题目需要判断一个元素是否在数组中出现过,因此采用哈希表的方法。
- 使用数组作为哈希表
因为题目限定了数组元素的范围为0 ~ 1000,因此可以采用大小为1001的数组作为哈希表,数组元素的值直接对应为哈希表的索引即数组下标。
题目要求返回的列表中不出现重复的元素,因此在遍历nums2时,若元素在哈希表中,将其加入result后将哈希表中对应的值设置为0
class Solution(object):
def intersection(self, nums1, nums2):
"""
:type nums1: List[int]
:type nums2: List[int]
:rtype: List[int]
"""
hash_num1 = [0] * 1001
result = []
for num in nums1:
hash_num1[num] += 1
for num in nums2:
if hash_num1[num] != 0:
result.append(num)
hash_num1[num] = 0 # 将hash表中对应的值设为0,避免result中出现重复
return result
- 使用字典作为哈希表
根据nums1的内容初始化一个哈希表,为了避免返回列表中包含重复元素,解决办法有两个:
1)result初始化为set集合,返回时转换为list类型
2)result初始化为list,对nums2进行遍历时,若一个元素在哈希表中,将其加入result后从哈希表删除
# 采用集合
class Solution(object):
def intersection(self, nums1, nums2)
from collections import defaultdict
hash_num1 = defaultdict(int)
for num in nums1:
hash_num1[num] += 1
result = set()
for num in nums2:
if num in hash_num1:
result.add(num)
return list(result)
实现过程中遇到的问题:
怎么解决返回的列表中出现的重复元素:
1)在将元素加入result后将其在哈希表中删除
2)对两个数组都得到一个哈希表,判断相同下标处的值是否均不为0
LeetCode 202.快乐数:
看到题目后的想法:
题目的主要思路就是不断求当前数的每个位置上数字的平方和,并判断是否为1或陷入循环。
- 使用哈希表判断是否陷入循环
判断是否陷入循环也就是判断当前数是否在之前出现过可以用哈希表,此处使用set集合作为哈希表存储之前出现过的数。
class Solution(object):
def isHappy(self, n):
"""
:type n: int
:rtype: bool
"""
cycle = set() # 记录之前出现过的元素
while(1):
if n == 1: # 是快乐数
return True
elif n in cycle:
return False # 陷入循环
else:
cycle.add(n)
n = self.get_num(n)
def get_num(self, n):
re = 0
while n != 0:
n, r = divmod(n, 10)
re += r ** 2
return re
- 使用快慢指针
快慢指针初始相同,快指针走两步,慢指针走一步,如果存在循环,那么快慢指针一定能相遇。
1)快慢指针的实现1:值得注意的是:循环的判断条件为fast != 1且self.get_num(fast) != 1,后面的条件判断是必须的。因为可能存在fast != 1,但是self.get_num(fast) = 1,如果不加的话,while循环中就会出现fast = 1, slow = 1,从而返回False的错误
2)快慢指针实现2:因为不管输入的整数是否为快乐数,最后都会陷入循环,那么通过判断fast = slow时的值来判断是否为快乐数
# 快慢指针的实现1
class Solution(object):
def isHappy(self, n):
fast = slow = n
while fast != 1 and self.get_num(fast) != 1:
fast = self.get_num(self.get_num(fast))
slow = self.get_num(slow)
if fast == slow: # 陷入了循环
return False
return True
# 快慢指针的实现二
def isHappy(self, n):
fast = self.get_num(self.get_num(n))
slow = self.get_num(n)
while fast != slow:
fast = self.get_num(self.get_num(fast))
slow = self.get_num(slow)
if fast == 1:
return True
else:
return False
实现过程中存在的问题:
使用快慢指针实现判断出是否陷入了循环时,while的条件应当是判断 fast 和 fast 的下一个数是否为1
LeetCode 1. 两数之和:
看到题目后的想法:
- 采用哈希表
1)可以采用哈希表的原因:
能使用哈希表原因在于,每次确定一个元素current后,找到第二个元素target - current可以是向前找,也可以是向后找。
如果是向后找,那么就是暴力求解,时间复杂度为O(n^2);如果是向前找,那么就是找target - curren在前面是否出现过,从而联想到可以使用哈希表的方式。
2)采用字典作为哈希表
因为需要返回元素的下标,因此哈希表需要记录元素的值和下标,同时需要根据元素的值找到下标。因此将元素加入字典中时,key为元素的值,value为元素的下标
class Solution(object):
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
record = dict()
for i in range(len(nums)):
# 如果在字典中有匹配的key,返回下标
if target - nums[i] in record:
return [i, record[target - nums[i]]]
# 否则将当前元素加入字典中
record[nums[i]] = i
return []
- 双指针
① 首先对数组进行排序,采用一对指针left和right分别指向排序后数组的开始和结束。
② 若[left] + [right] < target,left向后移动
③ 若[left] + [right] > target,right向前移动
④ 直到找到加和为target的两个元素,随后遍历一遍原数组,找到对应的元素的下标并返回
class Solution(object):
def twoSum(self, nums, target):
nums_sorted = sorted(nums)
left, right = 0, len(nums) - 1
while left < right:
nsum = nums_sorted[left] + nums_sorted[right]
if nsum == target:
left_index = nums.index(nums_sorted[left])
right_index = nums.index(nums_sorted[right])
# list.index是从前向后找,下标从0开始,因此对于相同元素,找到的下标是相同的
if left_index == right_index:
# 从left_index + 1向后找到[right]的正确下标
right_index = left_index + 1 + nums[left_index + 1:].index(nums_sorted[right])
return [left_index, right_index]
elif nsum < target:
left += 1
else:
right -= 1
return []
实现过程中遇到的问题:
最开始只想到了确定一个元素后,找到另一个元素可以向后找,没有想到可以向前找,从而没有想到哈希表的解法。
学习收获:
① 对于什么时候运用哈希表有了更加确定的认识,当需要判断一个元素是否在一个部分中出现过时,采用哈希表
② 对于具体哈希表的实现:在python中,可以使用数组、字典和集合作为哈希表。