提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、介绍哈希表
1.什么是哈希表?
哈希表也是一种常见的数据结构,或者说也是一种思想,之前我们所探讨的数组,链表,主要是为了存储数据,而哈希表主要是为了查询某个数据是否在某一数据结构中,重点在于查询。
2.哈希表的类别
在python中,以我现在的知识体系,我所知道的哈希表主要分为三种,列表(或者说数组),集合,字典。三者的区别呢,列表就完全可以当成数组,集合无序(这里的无序是说输出时的顺序和存储的顺序不一定一样),字典的话,分为key和value,两者一一对应。怎么说呢,这些也是python中存储数据的结构,但是如果用一些哈希算法来解释,好像就可以当成哈希表,其实我也不太清楚具体关系。
3.哈希算法
这个感觉掌握的相对扎实一些吧,哈希算法主要是为了解决数据存储时冲突的问题。就比如我们有一个数组,遍历的时间复杂度是O(n),但是,如果如果把它改造成哈希表,我们查找某一元素的时间复杂度就是O(1)。那么具体怎么改造,怎么存储呢,下面我来具体说明下。
还是用最简单的数组来举例,我们现在有一个数组,长度为8,那数组的下标就是0-6.现在我们有五个数,7,10,4,14,3。我们要把他们存到数组里,如果简单的直接存,当然可以,但是这时候比如我们要查找8,那就需要从数组下标为0的位置开始,依次向后遍历,很慢,这时候,我们就可以利用构造哈希算法来解决。最简单的哈希算法就是取模,就比如这道题,我们可以把每个数据所要存储位置的下标,用这个数对k取余得到,具体就是(比如K为7,貌似最好用小于数组长的最大质数)。7应该存储到哪呢,7%7 = 0,7就存到nums[0],10%7=3,10就存到nums[3],4%7 = 4,4就存储到nums[4],14%7=0,但是注意,nums[0]已经被前面的7给占据了,那这个时候我们怎么办呢?通常有两种方法,拉链法和开放定址法。先说比较简单的拉链法,就是这个哈希数组我们在创建的时候,他的每一个位置存储的是一个链表节点,这样我们就可以在0号位置,创建一个新节点,挂载到已占据位置节点的后边。再说开放定址法,我们先说其中最简单的线性探索法,就比如这道题,我们本来要把14存到下标为0的位置,但是0号位置被占用了,那我们就往后走以为,发现1号位置没有被占用,好,那我们就存储到一号位置,其他的开放定址法还有很多,此处就不一一赘述。然后就是存3,3%7=3,但是3号位置也已经被10占用了,根据刚才的线性探索法法,那我们应该去4号位置看看,但是4号被4占用了,那我们就接着再走,看5号位,5号位值没有被占用,那我们就存储到5号位。
存完了,但是这只是哈希算法的一半,不要忘了我们最开始的问题,怎么在O(1)的时间复杂度来查找元素,其实也差不多,相当于逆向思维。下面我依然一一概述下。查7,因为7%7=0,所以我们看看nums[0]是否为7,为7,查找成功,但是有非常重要的易错点,如果nums[0]不为7呢,难道数组里就一定没有7吗,不一定,为什么,这就要考虑到之前我们解决哈希冲突时的线性探索法。那我们就拿查3举例子,3%7=3,但是nums[3] = 10,那我们就看nums[4]有没有存数据,如果没存,那此时才能说3一定不在数组中,因为如果在的话,根据线性探索法3应该存到4号位(前提,一定没有对哈希数组进行元素删除操作),此时nums[4]也存了,为4,依然不为3,那我们就接着向后查,nums[5]为3,这时候我们成功查找到了元素3。
说的不是太清楚,总的来说我们要首先先了解列表,集合,字典的相关特性,之后理解怎么把数组改造成哈希数组,并且注意改造时的哈希冲突问题,我们是利用拉链法还是开放定址法解决。最后就是查找时一定注意,如果在原本的下标位置查找不到,那不一定不存在,有可能是因为我们哈希冲突,被散列到别的位置了,只有当按照特定散列算法查找不到的时候,才能说不存在。
二、题目分析与心得
(1).242. 有效的字母异位词 - 力扣(LeetCode)
分析:最直接的思路,直接把两个字符串排序为列表,然后比较两个列表是否相等,也就是
return sored(s) == sored(t)
但是既然我们这篇是在讲哈希表,那怎么使用哈希表来解决问题呢?首先我们要先考虑使用哪种哈希结构,列表?集合?字典?因为此题字符串的元素都是小写字母,也就是说元素的个数是固定的,所以我们使用列表(数组)比较方便。那我们怎么散列呢?可以使用每个字幕的ascll码与a的ascll码的差值来散列,字符串s对哈希表的值进行加,字符串t对哈希表的数值进行减,最后看哈希表每一个位置的值是否都是0即可。
代码实现:
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
result = [0] * 30
for i in s:
result[ord(i) - ord('a')] += 1
for i in t:
result[ord(i) - ord('a')] -= 1
for i in result:
if i != 0:
return False
return True
心得:此题是数组哈希结构的应用,后边还会接触集合,字典,主要一定要注意,哈希表最大的好处就是可以快速查询到某个数据是否被存储,还有怎么进行散列。
(2).349. 两个数组的交集 - 力扣(LeetCode)
分析:此题要抓住两个要点,问的是元素是否出现过,数组中的元素种类有限,那我们就可以使用列表来做哈希结构,具体实现方法与上一题类似,如果数组中没有给出[0,1000]的限制,那我们就只能使用字典了,在代码实现中,两个方法都会进行实现。此外,还有一点就是,因为我们要返回的是无重复的结果集,所以要使用集合,使用列表也可以,但是需要添加去重细节。
代码实现:
使用列表作为哈希表
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
result = set()
val = [0] * 1001
for i in nums1:
val[i - 0] = 1
for i in nums2:
if val[i - 0] == 1:
result.add(i)
return list(result)
使用字典作为哈希表
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
result = set()
val = {}
for i in nums1:
val[i] = 1
for i in nums2:
if i in val.keys():
result.add(i)
return list(result)
心得:注意体会什么时候用数组作为哈希表,什么时候用字典作为哈希表。当时目标元素种类有限时,可以使用数组,当目标元素种类无限或者说有限但是散列之后很分散的话,使用字典。此外,如果结果集中要求去重的话,可以使用集合,此处需要注意如果创建一个空的集合,只能使用set(),而不是{},因为{}表示的是一个空字典。
分析:此题主要明确三点问题
1.什么时候才能确定是无限循环:当在某一时刻,该数又重复出现的时候(重复出现,可以考虑哈希表)
2.用哪种哈希结构(因为在判断快乐数的时候,任何正整数都有可能出现,所以元素种类是无限的,不能使用数组,只能使用字典
3.怎么求各位的平方和(这个要对取模%和取整//操作比较熟悉)
代码实现:
class Solution:
def isHappy(self, n: int) -> bool:
def summ(n):
result = 0
while n != 0:
val = n % 10
n = n // 10
result += val*val
return result
tmp = {}
while n not in tmp.keys():
if n == 1:
return True
tmp[n] = 1
n = summ(n)
return False
心得:怎么从题目中发现是要求元素是否出现过这一问题核心是比较难的,还有就是对哈希表结构的选用。
分析:此题有两个问题需要注意
1.观察出使用哈希表的特点,也就是判断元素是否出现过,虽然此题表面上不是判断数组中某一个元素是否出现过,但是通过分析发现,我们需要判断的是target - nums[i]的值是否出现过,所以依然是哈希表的特点
2.用什么哈希表,因为数组中的数的种类无限,所以使用字典,但是一定要注意,字典的关键字和值表示的是什么,因为我们要判断的是target - nums[i]是否出现过,要求的是此值在数组中的下标,所以字典中的关键字应该是数组中元素的值,字典中的值应该是数组中的下标
代码实现:
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
i = 0
val = {}
while i < len(nums):
tmp = target - nums[i]
if tmp in val.keys():
return [i, val[tmp]]
val[nums[i]] = i
i += 1
心得:从题目当中发现哈希表的特点,如果使用字典的话,一定要分析好什么作为关键字,什么作为值。
(5).454. 四数相加 II - 力扣(LeetCode)
分析:该题和两数之和类似吧,四数相加可以看成两对两数之和,也就是说,先算nums1 + nums2的所有可能,再算nums3 + nums4的所有可能,最后再进行判断
代码实现:
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
def mapp(nums1, nums2):
result = {}
for i in nums1:
for j in nums2:
val = i + j
if val in result.keys():
result[val] += 1
else:
result[val] = 1
return result
n = 0
mpp1 = mapp(nums1, nums2)
mpp2 = mapp(nums3, nums4)
for i in mpp1.keys():
if -i in mpp2.keys():
n = n + mpp1[i] * mpp2[-i]
return n
一定要注意一点,比如nums1 + nums2 = 5的可能有三种,nums3 + nums4 = -5的可能有四种,则单这种nums1 + nums2 + nums3 + nums4 = 0的情况就是3 * 4 = 12种
心得:化繁为简,依然是有点像多路归并算法,体会精髓,八数之和也迎刃而解
分析:三数之和和上题最大的不同,一是要说明哪些数之和为0,二是还要去重,所以用哈希表的话很麻烦,这里采用的是双指针法。首先先把数组排序,以i=0号位值为基准开始,left=i+1,right = len(nums) - 1,计算三数之和并根据条件,left和right相互靠近,当left=right时,i再变成1,以1为基准再进行遍历。细节方面就是对i,left,right的去重操作,我也说不清楚hhh
代码实现:
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
result = []
nums.sort()
for i in range(len(nums)):
if nums[i] > 0:
break
if i > 0 and nums[i] == nums[i - 1]:
continue
left = i + 1
right = len(nums) - 1
while left < right:
val = nums[i] + nums[left] + nums[right]
if val > 0:
right -= 1
elif val < 0:
left += 1
else:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return result
心得:去重很烦人!!!麻了
分析:四数之和和三数之和有点类似,可以当作先限制k=0,然后在1到n-1位置再利用三数之和逻辑解题。最重要的还是要注意去重!!!
代码实现:
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
result = []
n = len(nums)
nums.sort()
for k in range(n):
if nums[k] > 0 and nums[k] > target:
break
if k > 0 and nums[k] == nums[k - 1]:
continue
for i in range(k + 1, n):
if i > k + 1 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
while left < right:
val = nums[k] + nums[i] + nums[left] + nums[right]
if val > target:
right -= 1
elif val < target:
left += 1
else:
result.append([nums[k], nums[i], nums[left], nums[right]])
while left < right and nums[right] == nums[right - 1]:
right -= 1
while left < right and nums[left] == nums[left + 1]:
left += 1
right -= 1
left += 1
return result
left和right的去重操作没有变,但是注意k和i的去重逻辑。
心得:之前我们从两数之和推导出了四数相加的思路,现在我们从三数之和推导出了四数之和的解题逻辑。要善于从之前做过的题中把我精髓,之后遇到相似的题套用之前逻辑解题(虽然我还是必须要看答案才懂hhh)。再就是去重!!!
总结
1.首先是对哈希表类别的了解,列表,集合,字典,懂得他们之前的区别
什么时候用列表:元素种类有限
什么时候用集合:集合里的元素不重复,可以简单的自动去重(但是注意,集合里的元素不能是列表)
什么时候用字典:元素种类无限,或者把元素散列后很分散,注意用字典的时候,什么作为key,什么作为value
2.一些概念上的东西,比如散列算法,解决哈希冲突的算法等等
3.去重逻辑