对撞指针

leetcode1 Two Sum
题目描述
给出一个整型数组nums,返回这个数组中两个数字的索引值i和j,使得nums[i]+nums[j]等于给定的一个target值,两个索引不能相等。
如:nums = [2, 7, 11, 15], target=9, 返回[0, 1] 。

审题
需要考虑:
1、开始数组是否有序?
2、索引从0开始计算还是从1开始计算?
3、没有解该怎么办?
4、有多个解该怎么办?保证有唯一解。

分析实现
暴力法 O(n^2)
两次遍历数组,代码就不写了。

排序+指针对撞 O(n)+O(nlogn)=O(n)
因为问题本身不是有序的,因此需要对原来的数组进行一次排序,排序后就可以用O(n)的指针对撞进行解决。
但是问题是,返回的是数字的索引,如果只是对数组的值进行排序,那么数组原来表示的索引的信息就会丢失,所以在排序前要进行些处理。

错误代码示例–只使用dict来进行保存

class Solution:def twoSum(self, nums:List[int], target:int) -> List[int]:record = dict()for index in range(len(nums)):record[nums[index]] = indexnums.sort()l, r = 0, len(nums)-1while l < r:if nums[l] + nums[i] == target:return [record[nums[l]], record[nums[r]]elif nums[l] + nums[r] < target:l += 1else:r -= 1

当遇到相同的元素的索引问题时,会不满足条件:
如:[3,3] 6
在排序前先使用一个额外的数组拷贝一份原来的数组,对于两个相同元素的索引问题,使用一个bool型变量辅助将两个索引都找到,总的时间复杂度为O(n)+O(nlogn)=O(nlogn)

class Solution:def twoSum(self, nums, target):nums_copy = nums.copy()sameFlag_l = True #用于取重复元素的第一个元素sameFlag_r = Truenums.sort()l,r = 0,len(nums)-1while l < r:if nums[l] + nums[r] == target:breakelif nums[l] + nums[r] < target:l += 1else:r -= 1res = []for i in range(len(nums)):if nums_copy[i] == nums[l] and sameFlag_l:res.append(i)sameFlag_l = Falseelif nums_copy[i] == nums[r] and sameFlag_r:res.append(i)sameFlag_r = Falsereturn res

小tips
如果只是对数组的值进行排序,那么数组原来表示的索引的信息就会丢失的情况,可以在排序前:

更加pythonic的实现:
通过list(enumrate(mums))开始实现下标(也就是索引)和值的绑定,不用专门的再copy加bool判断。

nums = list(enumerate(nums))
nums.sort(key=lambda x:x[1])
i,j = 0, len(nums-1)
while i < j :if nums[i][1] + nums[j][1] == target:return nums[i][0], nums[j][0]elif nums[i][1] + nums[j][1] < target:i += 1else:j -= 1return nums[i][0], nums[j][0]

查找表–O(n)
遍历数组过程中,当遍历到元素v时,可以只看v前面的元素,是否含有target-v的元素存在。
1、如果查找成功,就返回解;
2、如果没有查找成功,就把v放在查找表中,继续查找下一个解。
即使v放在了之前的查找表中覆盖了v,也不影响当前v元素的查找。因为只需要找到两个元素,只需要找target-v的另一个元素即可。

class Solution:def twoSum(self, nums:List[int], target:int) -> List[int]:record = dict()for i in range(len(nums)):complement = target - nums[i]#已经在之前的字典中找到这个值if record.get(complement) is not None:res = [i, record[complement]]return resrecond[nums[i]] = i

代码只进行了一次循环,所以时间复杂度和空间复杂度都为O(n).

LeetCode15 3Sum
题目描述
给出一个整形数组,寻找其中的所有不同的三元组,(a,b,c),使得a+b+c=0。
注意:答案中不可以包含重复的三元组。
如:nums=[-1, 0, 1, 2, -1, -4],
结果为:[[-1, 0, 1], [-1, -1, 2]]

审题
1、数组不是有序的;
2、返回结果为全部解,多个解的顺序是否需要考虑?——不需要考虑顺序;
3、什么叫不同的三元组?索引不同即不同,还是值不同?——题目的定义是,值不同才为不同的三元组;
4、没有解的时候怎么返回?——空列表

分析实现
因为上篇中已经实现了Two Sum的问题,因此对于3Sum,首先想到的思路就是,开始固定一个k,然后在气候都当作two sum的问题来解决,但是这样并不是很ok。
没有考虑重复元素导致错误
直接使用Two Sum问题中查找表的解法,根据第一层遍历的i,将i之后数组作为two sum的问题进行解决。
但是这样会导致一个错误,错误用例如下:

输入:[-1, 0, 1, 2, -1, -4]
输出:[[-1, 1, 0], [-1, -1, 2], [0,-1, 1]]
预期结果:[[-1, -1, 2], [0, 1, -1]]

主要原因是代码在实现的过程中没有把第一次遍历i的索引指向相同元素的情况排除掉,于是出现了当i指针后面位置的元素有和之前访问过的相同的值,于是重复遍历了。
那么可以考虑,开始时对nums数组进行排序,排序后当第一次遍历的指针k遇到下一个和前一个指向的值重复时,就将其跳过。为了方便计算,可以在第二层循环中,可以使用对撞指针的套路:

#对撞指针的套路
l, r = 0, len(nums)-1
while l < r:if nums[l] + nums[r] == target:return nums[l], nums[r]elif nums[l] + nums[r] < targetl += 1else:r -= 1

但其中要注意的是,在里层循环中仍旧需要考虑重复值的情况,因此当值相等时,再次移动指针,需要保证其指向的值和前一次指向的值不重复,因此可以:

#对撞指针套路
l, r = 0, len(nums)-1
while l < r:sum = nums[i] + nums[l] + nums[r]if sum == target:res.append([nums[i], nums[l], nums[r])l += 1r -= 1while l < r and nums[l] == nums[l-1]: l += 1while l < r and nums[r] == nums[r+1]: r -= 1elif sum < target:l += 1else:r -= 1

再调整下遍历的范围,因为设了3个索引:i,l,r。边界情况下,r索引指向len-1, l指向len-2,索引i遍历的边界为len-3,故for循环是从0到len-2。

代码实现如下:

class Solution:def threeSum(self, nums, target):res = []nums.sort()print(nums)for i in range(len(nums)-2):print('此时选择i值:',i)if nums[i] > 0: break # 因为是排序好的数组所以大于0的可以直接排除if i > 0 and nums[i] == nums[i-1]: continue #跳出本次循环,避免出现两个相同的结果l,r = i+1, len(nums)-1while l < r:sum = nums[i] + nums[l] + nums[r]print(sum, nums[i],"+",nums[l],"+",nums[r] )if sum == target:res.append([nums[i],nums[l],nums[r]])l += 1    r -= 1while l < r and nums[l] == nums[l-1]: l += 1while l < r and nums[r] == nums[r+1]: r -= 1elif sum < target:l += 1else:r -= 1return res

输出结果:

[-4, -1, -1, 0, 1, 2]
此时选择i值: 0
-3 -4 + -1 + 2
-3 -4 + -1 + 2
-2 -4 + 0 + 2
-1 -4 + 1 + 2
此时选择i值: 1
0 -1 + -1 + 2
0 -1 + 0 + 1
此时选择i值: 2
此时选择i值: 3
3 0 + 1 + 2
[[-1, -1, 2], [-1, 0, 1]]

小tips

  1. 采用for+while的形式来处理三索引;
  2. 当数组不是有序时需要注意,有序的特点在哪里,有序就可以用哪些方法解决,无序的话不方便在哪里?
  3. 对撞指针的套路,规定做指针和有指针,注意指针移动的边界;
  4. 处理重复值的套路:先转换为有序数组,再循环判断与看一只是否重复。

LeetCode18 4sum
题目描述
给出一个整形数组,寻找其中所有不同的四元组(a, b, c, d), 使得a+b+c+d等于一个给定的数字target。
与三数之后的解法类似,所不同的是需要再套一层循环,对边界情况进行改动即可。

  1. 原来3个是到len-2,现在外层循环是到len-3;
  2. 在中间层得迭代中,当第二个遍历得值在第一个遍历得值之后且后项大于前项时,认定为重复;
  3. 加些边界条件判断:当len小于4时,直接返回;当只有4个值且长度等于target时,直接返回本身即可。

LeetCode16 3sum closest
题目描述
给定一个整形数组,寻找其中的三个元素a,b,c, 使得a+b+c的值最接近另外一个给定的数字target。
如:给定数组nums = [-1, 2, 1, -4], 和target=1。
与target最为接近的三个数的和为2。 (-1+2+1=2)

分析实现
这道题变形的地方在于不是直接找target,而是找最近的值。
那么最开始时可以随机设定一个三个数的和为某种结果值,在每次比较中,先判断三个数的和是否和target相等,如果不等,则设定一个结果值,如果差值小于这个结果值,如果小于则进行替换,并保存和的结果值。

LeetCode454 4SumII
题目描述
给定四个整形数组A,B,C,D,寻找有多少i,j,k,l的组合,使得A[i] + B[j] + C[k] + D[i] = 0。其中A,B,C,D中均含有相同的元素个数N, 且0<= N <= 500;
输入:
A = [1,2], B=[-2, -1], C=[-1, 2], D=[0,2]
分析:1+(-2)+(-1)+2 = 0; 2+(-1)+(-1)+0 = 0
输出:2

分析实现
这歌问题同样是sum问题的变种,其将同一个数组的条件,变成了四个数组中,依然可以用查找表的思想来实现。
首先可以考虑把D数组中的元素都放入查找表,然后遍历前三个数组,判断target减去每个元素后的值是否在查找表中存在,存在的话,把结果值加一。那么查找表的数据结构用set还是用dict?考虑到数组中可能存在重复的元素,而重复的元素属于不同的情况,因此用dict存储,最后的结果值加上dict相应key值的value,代码如下:

O(n^3)代码

from collections import Counter
record = Counter()
# 先建立数组D的查找表
for i in range(len(D)):record[D[i]] += 1
res = 0
for i in range(len(A)):for j in range(len(B)):for k in range(len(C)):num_find = 0-A[i]-B[j]-C[k]if record.get(num_find) != None:res += record(num_find)
return res

但是对于题目中给出的数据规模:N <=500,如果N为500时,时间复杂度非常高。
根据之前的思路继续往前走,如果只遍历两个数组,那么就可以得到N^ 2级别的算法,但是遍历两个数组的话,查找的就是另外两个数组,对这两个数组,我们可以通过dict来记录其和的个数,之后遍历结果在和中进行查找。

class Solution:def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:from collections import Counterrecord = Counter()for i in range(len(A)):for j in range(len(B)):record[A[i]+B[j]] += 1res = 0for i in range(len(C)):for j in range(len(D)):find_num = 0 - C[i] - D[j]if record.get(find_num) != None:res += record[find_num]return res

LeetCode 49 Group Anagrams
题目描述
给定一个字符串数组,将其中所有可以通过颠倒字符顺序产生相同结果的单词进行分组。

示例:
输入:[“eat”, “tea”, “tan”, “ate”, “nat”, “bat”].
输出:[[“ate”, “eat”, “tea”], [“nat”, “tan”], [“bat”]]
说明:所有输入均为小些字母
不考虑答案输出的顺序

根据前面的经验,异位词排序后的字符串显然都是相同的,那么可以把其当作key,把遍历的数组中的异位词当作value,对字典进行赋值,进而遍历字典的value,得到结果list。

  1. 需要注意的细节是,字符串和list之间的转换:
  2. 默认构造字典需为list的字典;
  3. 排序使用sorted()函数,而不用list.sort()方法,因为其不返回值;
  4. 通过’’.join(list),将list转换为字符串;
  5. 通过str.split(’,’)将字符串整个转换为list中的一项;
class Solution(self, strs):def groupAnagrams(self, strs):from collections import defaultdictstrs_dict = defaultdict(list)res = []for str in strs:key = ''.join(sorted(list(str)))strs_dict[key] += str.split(',')for v in strs_dict.values():res.append(v)return res

再将能用列表生成式替换的地方替换掉,代码实现如下:

class Solution:def groupAnagrams(self, strs: List[str]) -> List[List[str]]:from collections import defaultdictstrs_dict = defaultdict(list)for str in strs:key = ''.join(sorted(list(str)))strs_dict[key] += str.split(',')return [v for v in strs_dict.values()]

LeetCode 149 Max Points on a Line
题目描述
给出一个平面上的n个点,寻找存在多少个由这些点构成的三元组(i,j,k),使得i,j两点的距离等于i,k两点的距离。
其中n最多为500,且所有的点坐标的范围在[-10000,10000]之间。

输入:
[[0,0],[1,0],[2,0]]

输出:
2
解释:
两个结果为: [[1,0],[0,0],[2,0]] 和 [[1,0],[2,0],[0,0]]

分析实现

原始思路
题目的要求是:使得i,j两点的距离等于i,k两点的距离,那么相当于是比较三个点之间距离的,那么开始的思路就是三层遍历,i从0到len,j从i+1到len,k从j+1到len,然后比较三个点的距离,相等则结果数加一。

显然这样的时间复杂度为O(n^3),对于这道题目,能否用查找表的思路进行解决优化?

查找表
之前的查找表问题,大多是通过构建一个查找表,而避免了在查找中再内层嵌套循环,从而降低了时间复杂度。那么可以考虑在这道题中,可以通过查找表进行代替哪两层循环。

当i,j两点距离等于i,k时,用查找表的思路,等价于:对距离key(i,j或i,k的距离),其值value(个数)为2。

那么就可以做一个查找表,用来查找相同距离key的个数value是多少。遍历每一个节点i,扫描得到其他点到节点i的距离,在查找表中,对应的键就是距离的值,对应的值就是距离值得个数。

在拿到对于元素i的距离查找表后,接下来就是排列选择问题了:

如果当距离为x的值有2个时,那么选择j,k的可能情况有:第一次选择有2种,第二次选择有1种,为21;
如果当距离为x的值有3个时,那么选择j,k的可能的情况有:第一次选择有3种,第二次选择有2种,为3
2;
那么当距离为x的值有n个时,选择j,k的可能情况有:第一次选择有n种,第二次选择有n-1种。
距离
对于距离值的求算,按照欧式距离的方法进行求算的话,容易产生浮点数,可以将根号去掉,用差的平方和来进行比较距离。

实现代码如下:

class Solution:def numberOfBoomerangs(self, points):res = 0from collections import Counterfor i in points:record = Counter()for i in points:record = Counter()for j in points:if i!= j:record[self.dis(i,j)] += 1for k,v in record.items():res += v*(v-1)return resdef dis(self, point1, point2):return (point1[0]-point2[0]) ** 2 + (point1[1]-point2[1]) ** 2

优化
对实现的代码进行优化:

将for循环遍历改为列表生成式;
对sum+=的操作,考虑使用sum函数。
对不同的函数使用闭包的方式内嵌;

class Solution:def numberOfBoomerangs(self, points: List[List[int]]) -> int:from collections import Counterdef f(x1, y1):# 对一个i下j,k的距离值求和d = Counter((x2 - x1) ** 2 + (y2 - y1) ** 2 for x2, y2 in points)return sum(t * (t-1) for t in d.values())# 对每个i的距离进行求和return sum(f(x1, y1) for x1, y1 in points)

LeetCode149 Max Points on a Line
题目描述
给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。

示例 1:
输入: [[1,1],[2,2],[3,3]]
输出: 3

示例 2:
输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出: 4

分析实现
本道题目的要求是:看有多少个点在同一条直线上,那么判断点是否在一条直线上,其实就等价于判断i,j两点的斜率是否等于i,k两点的斜率。

回顾上道447题目中的要求:使得i,j两点的距离等于i,k两点的距离,那么在这里,直接考虑使用查找表实现,即查找相同斜率key的个数value是多少。

在上个问题中,i和j,j和i算是两种不同的情况,但是这道题目中,这是属于相同的两个点, 因此在对遍历每个i,查找与i相同斜率的点时,不能再对结果数res++,而应该取查找表中的最大值。如果有两个斜率相同时,返回的应该是3个点,故返回的是结果数+1。

查找表实现套路如下:

class Solution:def maxPoints(self,points):res = 0from collections import defaultdictfor i in range(len(points)):record = defaultdict(int)for j in range(len(points)):if i != j:record[self.get_Slope(points,i,j)] += 1for v in record.values():res = max(res, v)return res + 1def get_Slope(self,points,i,j):return (points[i][0] - points[j][0]) / (points[i][1] - points[j][1])

但是这样会出现一个问题,即斜率的求算中,有时会出现直线为垂直的情况,故需要对返回的结果进行判断,如果分母为0,则返回inf,如下:

def get_Slope(self,points,i,j):if points[i][1] - points[j][1] == 0:return float('Inf')else:return (points[i][0] - points[j][0]) / (points[i][1] - points[j][1])

再次提交,发现对于空列表的测试用例会判断错误,于是对边界情况进行判断,如果初始长度小于等于1,则直接返回len:

if len(points) <= 1:return len(points)

再次提交,对于相同元素的测试用例会出现错误,回想刚才的过程,当有相同元素时,题目的要求是算作两个不同的点,但是在程序运行时,会将其考虑为相同的点,return回了inf。但在实际运行时,需要对相同元素的情况单独考虑。

于是可以设定samepoint值,遍历时判断,如果相同时,same值++,最后取v+same的值作为结果数。

考虑到如果全是相同值,那么这时dict中的record为空,也要将same值当作结果数返回,代码实现如下:

class Solution:def maxPoints(self,points):if len(points) <= 1:return len(points)res = 0from collections import defaultdictfor i in range(len(points)):record = defaultdict(int)samepoint = 0for j in range(len(points)):if points[i][0] == points[j][0] and points[i][1] == points[j][1]:samepoint += 1else:record[self.get_Slope(points,i,j)] += 1for v in record.values():res = max(res, v+samepoint)res = max(res, samepoint)return resdef get_Slope(self,points,i,j):if points[i][1] - points[j][1] == 0:return float('Inf')else:return (points[i][0] - points[j][0]) / (points[i][1] - points[j][1])

时间复杂度为O(n^2),空间复杂度为O(n)

总结:遍历时多用索引,而不要直接用值进行遍历

Leetcode分类练习-查找(2)对撞指针相关推荐

  1. LeetCode 分类练习(四):查找2

    LeetCode 分类练习(四):查找2 目录 LeetCode 分类练习(四):查找2 一.双指针(快慢指针.对撞指针) 1.快慢指针 2.对撞指针 3.滑动窗口法 二.滑动数组 三.实战1(对撞指 ...

  2. leetcode分类刷题笔记

    leetcode分类刷题笔记--基于python3 写在前面 1.做题如果实在想不出时间复杂度比较优的解法,可以先写出暴力解法,尝试在其基础上优化 2.排序.双指针.二分等--经常可以优化时间复杂度 ...

  3. Leetcode分类解析:组合算法

    Leetcode分类解析:组合算法 所谓组合算法就是指:在解决一些算法问题时,需要产生输入数据的各种组合.排列.子集.分区等等,然后逐一确认每种是不是我们要的解.从广义上来说,组合算法可以包罗万象,甚 ...

  4. [算法]LeetCode 专题 -- 二分查找专题 34. 在排序数组中查找元素的第一个和最后一个位置

    LeetCode 专题 – 二分查找专题 34. 在排序数组中查找元素的第一个和最后一个位置 难度:中等 题目描述 给定一个按照升序排列的整数数组 nums,和一个目标值 target.找出给定目标值 ...

  5. mysql 家谱树查询_无限级分类之查找子孙树和家谱树

    $area=array( array('id'=>'1','name'=>'河南','parent'=>0), array('id'=>'2','name'=>'吉林', ...

  6. 双指针--快慢指针和对撞指针

    1.基础概念 两个指针有 n*n种组合,因此时间复杂度是 O(n^2) .而双指针算法就是运用单调性使得指针只能单向移动,因此总的时间复杂度只有 O(2n),也就是O(n).双指针可以分为两种类型,一 ...

  7. LeetCode分类训练 Task 1 分治

    群内编号:2-木铎铎 博文内容参考自DataWhale开源文档: LeetCode分类训练 Task 1 分治 主要思想 分治算法的主要思想是将原问题递归地分成若干个子问题,直到子问题满足边界条件,停 ...

  8. [LeetCode]704.二分查找及相关题目

    数组理论基础 数组理论 数组是存放在连续内存空间上的相同类型数据的集合 数组可以方便的通过下标索引的方式获取到下标下对应的数据 二维数组在内存的空间地址是连续的 二分查找 LeetCode 704.二 ...

  9. [Leetcode][第75题][JAVA][颜色分类][双(三)指针][计数排序]

    [问题描述][中等] [解答思路] 1. 三指针 时间复杂度:O(N) 空间复杂度:O(1) class Solution {public void sortColors(int[] nums) {i ...

最新文章

  1. 大盘点|YOLO 系目标检测算法总览
  2. UVALive6428 A+B【扩展欧几里得算法+GCD】
  3. C++ 控制结构和函数(三)—— 函数II(Functions II)
  4. LeetCode 501. 二叉搜索树中的众数(中序遍历)
  5. 《Programming with Objective-C》第四章 Encapsulating Data
  6. 数据库大咖解读“新基建”,墨天轮四重好礼相送!
  7. 容器的进程与namespace、rootfs
  8. idea javafx添加maven_IntelliJ IDEA使用之JavaFX
  9. dev万能头文件_超级好用的C++万能头文件
  10. node.js入门教程(B站黑马程序员)
  11. 有关php外文期刊,口腔外文杂志、收录数据库、参考信息汇总
  12. NVIDIA TAO 工具包 (TAO Toolkit) 的部署和应用【LDR、LPR】
  13. 审视AI界的“SOTA成瘾”丨AI学者万字论述
  14. 华为机试 放苹果
  15. php memcache内存大小,PHP memcache 内存缓存 数据库查询 应用 高洛峰 细说PHP
  16. 前端通用SEO技术优化指南
  17. 介绍几个预览效果不错的BIM网站链接
  18. linux nagios监控
  19. ios8.1.2耗电情况严重的解决方法
  20. 清华计算机本科学什么,清华大学计算机本科课程表

热门文章

  1. PTA 7-13 列车调度 (25 分) C语言和C++实现(二分查找)
  2. sql语句(select,create,drop,alter,delete,insert,update,grant)
  3. 5G和车联网的本质联系
  4. 全新UI众人帮任务帮PHP源码/悬赏任务抖音快手头条点赞源码/带三级分销可封装小程序
  5. 对APP强制更新的思考
  6. 20191127上海出差总结
  7. CentOS6实验模板机搭建
  8. 学习java第6天 模仿XP画板(10%)
  9. Spark的坑--Spark新手必看--Python Spark必读,耗费了我近三周的时间
  10. 高通平台开发系列讲解(充电篇)充电管理芯片PM7250B详解