python解决哈希查找_Python哈希表一招解决nSum问题

本文详细探讨了如何使用哈希表优化解决nSum问题,从两数之和开始,逐步讲解到三数之和、四数之和的高效算法,以及在面对数的平方等于两数乘积的问题时如何利用哈希表减少重复计算,提高时间复杂度。通过实例展示了如何利用双指针、计数哈希表等技术来实现线性或二次方时间复杂度的解决方案。
摘要由CSDN通过智能技术生成

自古各门各家武学都存在套路,正所谓以不变应万变,就在于临战之时,可以一招制敌。有的招数可能出奇制胜,但是最稳定的方式一定是多次训练的套路,它不一定能让你解决所有的问题,但是它足以让你轻松应对一类问题。

nSum的问题,主要存在大量重复的数使得如果在数组中遍历每个数,再比较查询结果,时间复杂度会超过题目的要求。我们可以采用哈希表的方式,加速查询的过程,同时对遍历的过程,对相同的数或者不满足条件的数适当的跳过,可以有效的提升效率,通过测试用例。

那么先从最简单的两数之和讲起。

最暴力的方式是从头到尾枚举 nums 中的每一个数,然后再看是否在数组中存在 j 使得 nums[j]  == target - nums[i] 且 j != i,这样的方式,遍历是 O(n) 复杂度,每次循环在数组中查询也是 O(n) 复杂度,总的时间复杂度达到了 O(n^2)。代码和运行时间如下:

classSolution: def twoSum(self, nums: List[int], target: int) ->List[int]: result =[] for i inrange(len(nums)): if nums[i] in nums and target-nums[i] innums: j = nums.index(target-nums[i]) if i!=j: result.append(i) result.append(j) break

return result

这样做显然太过耗时,循环如果不做改变的话(做改变的话可以用双指针法,这里不做过多介绍),那么考虑在查找过程中进行加速。我们注意到哈希表中查找元素的时间是 O(1),因此可以把在数组中查找改为在哈希表中查找。对于这题而言,只要找到了答案就可以返回,不需要找出所有的解,那么可以边遍历边向哈希表中添加元素。添加前,查询是否有满足条件的解,如果满足条件,return结果就可以。

classSolution: def twoSum(self, nums: List[int], target: int) ->List[int]: dic ={} for i,num inenumerate(nums): tmp = target - num #a + b = target, a = num, b = target - num

if tmp in dic: #哈希表中查询是否有解

return[i, dic[tmp]] dic[num] = i #没有解的话就存下当前的数和位置

运行结果如下:

最差的情况下,在线性时间内就可以解决问题

接下来考虑复杂一点的问题

首先题目要求解集里面不包含重复的元素,那么按照一定的规律找答案,就可以得到不重复的解。可以想到的方法是先进行排序,这样就可以有规律的寻找了。排序以后,每个数也可能有多个重复的,假如每个解里不能包含相同的数字,那么简单的在循环里加上

if i > index and nums[i] == nums[i-1]: continue

其中 index 是循环开始的值, 并且下一层循环从 i + 1开始,就可以保证无重复了。这样的去重方式可以参考我的另一篇文章讲到的第三类问题, https://www.cnblogs.com/HMJIang/p/13575005.html

然而这道题则是每个解里可以包含相同的数字,比如 [-1, -1, 2] 和 [0, 0, 0] 都可以得到和为0,这时候去重就可以用到Python中计数哈希表, Counter。先统计每个数字出现的次数,再对键值进行排序,每层循环里判断剩余的数字是否够当前的变量选择。

代码如下:

from collections importCounter classSolution: def threeSum(self, nums: List[int]) ->List[List[int]]: res =[] dic = Counter(nums) #Counter可以统计数组每个元素的个数

hash_nums = sorted(dic.keys()) #对键值进行排序

for i, a inenumerate(hash_nums): dic[a] -= 1 #a已经取走了一个数字,字典里对应位置 -1

for b inhash_nums[i:]: if dic[b] < 1: #b从i开始遍历,i也是当前a的位置,如果减去1以后b不够选了,跳过这一个位置

continue

c = -(a +b) if c < b: #有序的查找,如果c都比b小,之后b再增大,肯定c更小,那么就跳出,防止重复

break

#再判断c和b的关系,如果相等,那就需要dic[c]至少为2,才够选,如果不等,只要有,就够选了

if (c > b and dic[c] > 0) or (c == b and dic[c] > 1): res.append([a, b, c]) return res

时间复杂度 O(n^2)

空间复杂度 O(n)

提交结果:

有了这样的经验以后,我们可以用已有的套路看更复杂的四数之和

最外层循环遍历到什么位置,就在对应位置上减1,接下来内层循环里也把选择的数减1,方便后面进行判断,只要不够选了,就continue跳过这一次循环,如果最终的d比c还大,依旧break掉,和三数之和的差别在于,第二个数选择的时候要减去1,最内层循环结束以后还要加回1,因为之后最外层的a也会再遍历到这个位置。

代码如下:

from collections importCounter classSolution: def fourSum(self, nums: List[int], target: int) ->List[List[int]]: res =[] dic = Counter(nums) #对每个数出现的次数进行统计

arr = sorted(dic.keys()) #排序键值

for i, a inenumerate(arr): dic[a] -= 1 #a用掉了一次,而且a的位置之后不会再遍历到了,不需要加回

for j, b in enumerate(arr[i:]): #从arr[i]开始找b的值

if dic[b] < 1: #b可能等于a,判断一下,如果dic[b]不够1个,跳过这次循环

continue

dic[b] -= 1

for c in arr[i+j:]: #从arr[i+j]开始找c的值,注意上一层循环枚举j以后,需要再加最外层的i

if dic[c] < 1: #同上层循环b的判断

continue

d = target - (a + b +c) if d < c: #因为是非递减顺序,如果d小于c,就直接跳出,这样就可以避免重复

break

if (d == c and dic[d] > 1) or (d > c and dic[d] >0): res.append([a, b, c, d]) dic[b] += 1 #b现在所处的位置,之后a还会遍历到,因此需要加回1

return res

时间复杂度 O(n^3)

空间复杂度 O(n)

提交结果:

以此可以类推到更多数字的和。最外层循环每选到一个位置以后,都减去1,内层的循环也选到一个位置减去1,在更内层的循环结束以后加回1就可以。最内层转化为2数之间的大小关系的比较和查询哈希表是否有满足条件的值。

最后看一个变种问题

Leetcode 1577   数的平方等于两数乘积的方法数

这一题如果暴力求解,必然超时,那么就需要一些优化策略。两个数组里可能会存在很多相同的数,它们仅仅是位置不同,找到的 j, k的结果却一样,比如 nums1 = [1,1,1,1],nums2 = [1,1,1,1,1,1],1中每个数的平方,都等于2中任意两个不同位置的数的乘积,我们没有必要对每个相同的 nums1 中的数都找一遍 nums2 中所有的数,这就又回到了 nSums 问题,可以想到的去重的方式是哈希表。

这里的技巧在于如果对于每个平方数去找是否存在两个数和它相等,每个平方数遍历的时间是 O(n), 再找两个数,如果要达到 O(m) 复杂度,就应该考虑双指针的方式,然后需要各种比较,代码相对复杂,容易出错。如果换个思路,从右向左找,对于每个数字的乘积,都在哈希表里找是否存在相应的平方数,那么时间复杂度就是两次遍历数组的时间复杂度 × 哈希表查找的时间复杂度,由于哈希表查找是 O(1),最终等于两次遍历数组的时间复杂度。

代码如下:

from collections importCounter classSolution: def numTriplets(self, nums1: List[int], nums2: List[int]) ->int: square1 = Counter([i*i for i innums1]) square2 = Counter([i*i for i innums2]) res = for i inrange(len(nums2)): for j in range(i+1, len(nums2)): tmp = nums2[i] * nums2[j] #可以用tmp存一下两数之积,避免后面字典查询的时候再次重复计算键值

if tmp insquare1: res +=square1[tmp] for i inrange(len(nums1)): for j in range(i+1, len(nums1)): tmp = nums1[i] * nums1[j] #同理

if tmp insquare2: res +=square2[tmp] return res

时间复杂度 O(n^2 + m^2)  其中 m,n 是 nums1 和 nums2 的数组长度

空间复杂度 O(n+m)

代码执行结果如下:

之所以说在每次循环中要用 tmp 存一下两数的乘积,因为随着数据量的增加,重复计算两数乘积的代价也是相当大的,如果两次都直接用

if nums2[i] * nums2[j] insquare1: res += square1[nums2[i] * nums2[j]]

运行时间将会明显提升,提交结果如下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值