对撞指针
对撞指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
换言之,双指针法充分使用了数组有序这一特征,从而在某些情况下能够简化一些运算。将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。
两数之和
题目描述:
给出一个整型数组 nums,返回这个数组中两个数字的索引值 i 和 j,使得 nums[i] + nums[j] 等于一个给定的 target 值,两个索引不能相等。
示例:
nums= [2,7,11,15],target=9
返回[0,1]
解题思路:
-
暴力法
时间复杂度为 O ( n 2 ) O(n^2) O(n2),第一遍遍历数组,第二遍遍历当前遍历值之后的元素,其和等于target则return。
代码:
class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: for i in range(len(nums)): for j in range(i+1,len(nums)): if nums[i]+nums[j] == target: return i,j
-
排序+指针对撞
时间复杂度为 ( O ( n ) + O ( n l o g n ) = O ( n ) ) (O(n)+O(nlogn)=O(n)) (O(n)+O(nlogn)=O(n)),先对 nums 进行备份, 然后对 nums 进行排序,然后利用对撞指针(类似于二分法)找到在排序后数组中和等于 target 的两个数,然后再在 nums 的备份中找到这两个元素对应的位置。
代码:
class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: # 备份 nums_copy = nums.copy() # 排序 nums.sort() # 利用对撞指针查找 l,r = 0,len(nums)-1 for i in range(len(nums)): if nums[l] + nums[r] == target: break elif nums[l] + nums[r] > target: r -= 1 elif nums[l] + nums[r] < target: l += 1 res = [] same = True # 查找nums[l]和nums[r]在原数组的位置 for i in range(len(nums)): if same and nums_copy[i] == nums[l]: same = False res.append(i) elif nums_copy[i] == nums[r]: res.append(i) return res
此外也可以利用 list(enumerate(nums)) 实现下标和值的绑定。
class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: 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: j -= 1 elif nums[i][1] + nums[j][1] < target: i += 1 else: return min(nums[i][0],nums[j][0]),max(nums[i][0],nums[j][0])
-
查找表:
时间复杂度为 O ( n ) O(n) O(n),在遍历数组过程中,当遍历到元素 v 时,可以只看 v 前面的元素,是否含有 target-v 的元素存在:
- 如果查找成功,就返回解
- 如果没有查找成功,就把 v 放在查找表中,继续查找下一个解
代码:
class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: record = {} for i in range(len(nums)): t = target - nums[i] # 查找 target - nums[i] 是否已经存在 if record.get(t) is not None: return record[t],i else: record[nums[i]] = i
三数之和
题目描述:
给出一个整型数组,寻找其中的所有不同的三元组 (a,b,c),使得a+b+c=0
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
解题思路:
- 对于数组长度 n,如果 n 小于 3,则返回空
- 对数组按照从小到大的顺序进行排序,遍历排序后数组:
- 若 nums[i]>0,因为已经排序好,所以后面不可能有三个数加和等于 0,返回结果
- 对于重复元素:跳过,避免出现重复解
例如 [-1,-1,0,1],如果不跳过的话会输出两个值:nums[0],nums[2],nums[3] 和 nums[1],nums[2],nums[3]. 但两者都是[-1,0,1],与题目要求不符 - 令左指针 l=i+1,右指针 r=n-1,当 l<r 时,执行循环:
- 当 nums[i]+nums[l]+nums[r] = 0 时,执行循环,判断左界 nums[l] 和右界 nums[r] 是否和下一位置重复,去除重复解,将 l,r 移到下一位置,寻找新的解
- 若和大于 0,说明 nums[r] 太大,r 左移
- 若和小于 0,说明 nums[l] 太小,l 右移
代码:
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
res = []
if n < 3:
return res
nums.sort()
for i in range(n):
if nums[i] > 0:
return res
if i > 0 and nums[i] == nums[i-1]:
continue
l,r = i+1,n-1
while l < r:
if nums[l] + nums[i] + nums[r] == 0:
res.append([nums[i],nums[l],nums[r]])
while l<r and nums[l] == nums[l+1]:
l = l+1
while l<r and nums[r] == nums[r-1]:
r = r-1
l = l+1
r = r-1
elif nums[l] + nums[i] + nums[r] > 0:
r = r-1
else:
l = l+1
return res
对撞指针模板:
# 对撞指针套路
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] < target:
l += 1
else:
r -= 1
处理重复值的模板:
# 1.
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i-1]: continue
# 2.
while l < r:
while l < r and nums[l] == nums[l-1]: l += 1
四数之和
题目描述:
给出一个整形数组,寻找其中的所有不同的四元组(a,b,c,d),使得a+b+c+d等于一个给定的数字target。
示例:
nums = [1, 0, -1, 0, -2, 2],target = 0
结果为:
[[-1, 0, 0, 1],[-2, -1, 1, 2],[-2, 0, 0, 2]]
解题思路:
-
设指针从左至右依次为 p,k,i,j,如果 nums[p] + 3 * nums[p + 1] > target,因为 nums 按升序排列,所以之后的数肯定都大于 target ,直接 break
-
如果 nums[p] + 3 * nums[-1] < target,那么当前的 nums[p] 加其余三个数一定小于 target,故 p 直接下一位即可,continue
-
k 和 p 判断完全一样,只是将 3 变成了 2,target 变成了 target - nums[p]
-
同样地,为了避免结果重复,某个指针遇到相同的数需要直接跳过,这与三数之和是一样的
class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
n = len(nums)
nums.sort()
res = []
p = 0
while p < n-3:
if nums[p] + 3*nums[p+1] > target: # p太大,直接break
break
if nums[p] + 3*nums[-1] < target: # p太小
while p < n - 4 and nums[p] == nums[p+1]: # 处理重复情形,避免p = n-3 越界
p += 1
p += 1
continue
# 对于固定的p,讨论k
k = p+1
while k < n-2:
if nums[p] + nums[k] + 2*nums[k+1] > target: # k太大,直接break
break
if nums[p] + nums[k] + 2*nums[-1] < target: # k太小
while k < n-3 and nums[k] == nums[k+1]:
k += 1
k += 1
continue
# 对于固定的p,k,讨论i,j
new_target = target - nums[p] - nums[k]
i = k+1
j = n-1
while i < j:
if nums[i] + nums[j] > new_target: # j太大
j -= 1
elif nums[i] + nums[j] < new_target: # i太小
i += 1
else:
res.append([nums[p],nums[k],nums[i],nums[j]])
i += 1
j -= 1
while i < j and nums[i] == nums[i-1]:
i += 1
while i < j and nums[j] == nums[j+1]:
j -= 1
while k < n-3 and nums[k] == nums[k+1]:
k += 1
k += 1
while p < n-4 and nums[p] == nums[p+1]:
p += 1
p += 1
return res
最接近的三数之和
题目描述:
给出一个整形数组,寻找其中的三个元素 a,b,c,使得 a+b+c 的值最接近另外一个给定的数字target。
示例:
nums = [-1,2,1,-4], target = 1.
与 target 最接近的三个数的和为 2.
(-1 + 2 + 1 = 2).
解题思路:
开始时可以随机设定一个三个数的和为结果值,在每次比较中,先判断三个数的和是否和target相等,如果相等直接返回和。如果不相等,则判断三个数的和与target的差是否小于这个结果值时,如果小于则进行则进行替换,并保存和的结果值。
代码:
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
n =len(nums)
nums.sort()
# 初始化res和diff
res = nums[0]+nums[1]+nums[2]
diff = abs(res-target)
for i in range(n):
t = target-nums[i]
# 对于固定的i,讨论j,k
j,k = i+1,n-1
while j < k:
if nums[j] + nums[k] == t:
return target
else:
# 更新res和diff
if abs(t-nums[j]-nums[k]) < diff:
diff = abs(t-nums[j]-nums[k])
res = nums[i] + nums[j] + nums[k]
if nums[j] + nums[k] > t: # k太大
k -= 1
elif nums[j] + nums[k] <t: # j太小
j += 1
return res
查找表
四数相加 II
题目描述:
给出四个整形数组 A,B,C,D,寻找有多少 i,j,k,l 的组合,使得 A[i]+B[j]+C[k]+D[l]=0. 其中 A,B,C,D 中均含有相同的元素个数N,且0<=N<=500.
示例:
输入:
A = [ 1, 2] B = [-2,-1] C = [-1, 2] D = [ 0, 2]
输出:2
解题思路:
先遍历 A,B 两个数组,并记录 A[i]+B[j] 的值以及该值出现的次数,然后再遍历 C,D 两个数组,并在 A[i]+B[j] 的结果中寻找等于 0-C[k]-D[l] 的记录,并将该值对应的次数相加。
代码:
class Solution:
def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
from collections import Counter
record = Counter()
res = 0
n = len(A)
for i in range(n):
for j in range(n):
record[A[i]+B[j]] += 1
for k in range(n):
for l in range(n):
find = 0-C[k]-D[l]
if record.get(find):
res += record.get(find)
return res
更简洁的写法:
class Solution:
def fourSumCount(self, A: List[int], B: List[int], C: List[int], D: List[int]) -> int:
from collections import Counter
record = Counter([a+b for a in A for b in B])
return sum(record.get(- c - d,0) for c in C for d in D)
字母异位词分组
题目描述:
给出一个字符串数组,将其中所有可以通过颠倒字符顺序产生相同结果的单词进行分组。
示例:
输入: ["eat", "tea", "tan", "ate", "nat", "bat"],
输出:[["ate","eat","tea"],["nat","tan"],["bat"]]
说明:
所有输入均为小写字母。
不考虑答案输出的顺序。
解题思路:
如果将字符串统一排序,异位词排序后的字符串,显然都是相同的。因此可以把排序后的字符串当作 key,把异位词当作 value,对字典进行赋值,进而遍历字典的value,得到结果 list。
代码:
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
from collections import defaultdict
dic = defaultdict(list)
# 指定字典value的值的类型
res = []
for i in strs:
key = ''.join(sorted(list(i)))
dic[key] += i.split(',')
# str.split()把字符串转换为list
for v in dic.values():
res.append(v)
return res
回旋镖的数量
题目描述:
给定平面上 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]]
解题思路:
- 构造查找表,取点之间的距离为 key,value 为距离等于 key 的个数
- 因为要求 i,j 之间的距离等于 i,k 之间的距离,所以 j,k 的选择不唯一
- 当距离为 x 的值有 n 个时,选择 j,k 的可能情况有:j 的选择有 n 种,k 的选择有 n-1 种,因此共有 n(n-1) 种可能
- 对于距离值的计算,按照欧式距离的方法进行计算的话,容易产生浮点数,可以将根号去掉,用差的平方和来进行比较距离
代码:
class Solution:
def numberOfBoomerangs(self, points: List[List[int]]) -> int:
res = 0
from collections import Counter
for i in points:
# 固定i,讨论 j和k
record = Counter()
for j in points:
d = (i[0]-j[0])**2+(i[1]-j[1])**2
record[d] += 1
for v in record.values():
res += v*(v-1)
return res
更简洁的写法:
class Solution:
def numberOfBoomerangs(self, points: List[List[int]]) -> int:
from collections import Counter
def f(i):
# 对一个i下j,k的距离值求和
d = Counter((i[0]-j[0]) ** 2 + (i[1]-j[1]) ** 2 for j in points)
return sum(v*(v-1) for v in d.values())
# 对每个i的距离进行求和
return sum(f(i) for i in points)
直线上最多的点数
题目描述:
给定一个二维平面,平面上有 n 个点,求最多有多少个点在同一条直线上。
示例1:
输入: [[1,1],[2,2],[3,3]]
输出: 3
解释:
^
|
| o
| o
| o
+------------->
0 1 2 3 4
示例2:
输入: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出: 4
解释:
^
|
| o
| o o
| o
| o o
+------------------->
0 1 2 3 4 5 6
解题思路:
- 判断点 i,j,k 是否在一条直线上,等价于判断 i,j 两点的斜率是否等于 i,k 两点的斜率
- 遍历数组,查找过 i 点且斜率 (key) 相同的点的个数 (value)
- 在遍历数组对每个 i 查找过 i 且具有相同斜率的点之后,取查找表中的最大值
- 如果过 i 且斜率相同的点有 k 个,那么有 k+1 个点在同一条直线上
代码:
class Solution:
def maxPoints(self,points):
if len(points) <= 1:
return len(points)
res = 0
from collections import defaultdict
for i in range(len(points)):
record = defaultdict(int)
samepoint = 0
for j in range(len(points)):
if points[i] == points[j]:
samepoint += 1
else:
record[self.get_Slope(points,i,j)] += 1
for v in record.values():
res = max(res, v+samepoint)
res = max(res, samepoint)
return res
# 求最大公约数
def gcd(x, y):
if y == 0:
return x
else:
return gcd(y, x % y)
# 计算斜率
def get_Slope(self,points,i,j):
if points[i][1] - points[j][1] == 0:
return 'Inf'
else:
g = gcd((points[i][1] - points[j][1]),(points[i][0] - points[j][0]))
if g != 0:
k = str((points[i][1] - points[j][1])/g)+'/'+str((points[i][0] - points[j][0])/g)
else:
k = str((points[i][1] - points[j][1]))+'/'+str((points[i][0] - points[j][0]))
return k
滑动数组
滑动数组可以想象成显示屏,每次只显示有限个的数字,用完(显示完)后就向后移动一位,显示的数量不变。每次只储存数组中的几个元素,以起到压缩,节省存储空间的作用。
存在重复元素 II
题目描述:
给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的绝对值至多为 k.
示例1:
输入: nums = [1,2,3,1], k = 3
输出: true
示例 2:
输入: nums = [1,2,3,1,2,3], k = 2
输出: false
解题思路:
利用滑动数组,对于数组中的元素 i 判断其后面的 k 个元素中是否存在等于 i 的元素。
代码:
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
n = len(nums)
if n == 0 or k == 0:
return False
for i in range(0,n):
if nums[i] in nums[i+1:min(i+k+1,n)]:
return True
return False
使用 set,固定滑动数组的长度为 k+1,当这个滑动数组内如果能找到两个元素的值相等,就可以保证两个元素的索引的差小于等于 k. 如果当前的滑动数组中没有元素相同,就右移滑动数组的右边界 r ,同时将左边界 l 右移。
代码:
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
record = set()
for i in range(len(nums)):
if nums[i] in record:
return True
record.add(nums[i])
if len(record) == k+1:
record.remove(nums[i-k])
return False
存在重复元素 III
题目描述:
在整数数组 nums 中,是否存在两个下标 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值小于等于 t ,且满足 i 和 j 的差的绝对值也小于等于 ķ. 如果存在则返回 true,不存在返回 false.
示例 1:
输入: nums = [1,2,3,1], k = 3, t = 0
输出: true
示例 2:
输入: nums = [1,0,1,1], k = 1, t = 2
输出: true
解题思路:
和上题类似,使用循环。
代码:
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
n = len(nums)
for i in range(n):
for j in range(i+1,min(n,i+1+k)):
if abs(nums[i]-nums[j]) <= t:
return True
return False
另一种写法:
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
if t == 0 and len(nums) == len(set(nums)):
return False
for i in range(len(nums)):
for j in range(1,k+1):
if i+j > len(nums)-1:
break
if abs(nums[i]-nums[i+j]) <= t:
return True
return False
固定滑动窗口的长度为 k+1,判断滑动窗口内是否存在两个数 v 和 w,满足 a b s ( v − w ) ≤ t abs(v-w) \le t abs(v−w)≤t. 对于给定元素 v,该条件等价于在滑动窗口内是否存在元素 w 满足 v − t ≤ w ≤ v + t v-t \le w \le v+t v−t≤w≤v+t. 因此只需要找到滑动窗口内大于等于v-t 的最小元素,并判断该元素是否小于等于 v+t 即可。
代码:
class Solution:
def containsNearbyAlmostDuplicate(self, nums, k, t) -> bool:
record = set()
for i in range(len(nums)):
if len(record) != 0:
rec = list(record)
# 查找rec中大于等于nums[i]-t的最小元素的索引
find_index = self.lower_bound(rec,nums[i]-t)
if find_index != -1 and rec[find_index] <= nums[i] + t:
# 如果rec中大于等于nums[i]-t的最小元素(记为w) < num[i]+t
# 那么,nums[i]-t <= w <= nums[i]+t,即 abs(nums[i]-w)<=t
return True
record.add(nums[i])
if len(record) == k + 1:
record.remove(nums[i - k])
return False
# 利用二分法查找nums中大于等于target的最小元素的索引
def lower_bound(self, nums, target):
low, high = 0, len(nums)-1
while low<high:
mid = int((low+high)/2)
if nums[mid] < target:
low = mid+1
else:
high = mid
return low if nums[low] >= target else -1
二分查找
模板:
class Solution:
def BinarySearch(self, arr):
l, h = 0, len(arr)-1
while l < h:
mid = (l+h) // 2 # 整除
if f(x):
l = mid + 1
else:
h = mid
return l
搜索插入位置
题目描述:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
解题思路:
需要注意的是 h 要设置为 len(nums),而不是 len(nums)-1,因为可能会出现插入的位置在数组结尾(如示例 3)的情况,需要 l 能取到 len(nums)-1.
代码:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
l, h = 0, len(nums)
while l < h:
mid = (l + h) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
l = mid + 1
else:
h = mid
return l
有序数组中的单一元素
题目描述:
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: [3,3,7,7,10,11,11]
输出: 10
解题思路:
- 因为每个元素都会出现两次,唯有一个数只会出现一次,所以 len(nums) 一定是奇数
- 如果mid是偶数,那么和 1 异或得到 mid+1,如果 mid 和 1 异或得到的是 mid-1
- 若 nums[mid] 和 nums[mid ^ 1] 相等,那么唯一的元素在 nums[mid] 的右半部分 (l 右移),若 nums[mid] 和 nums[mid ^ 1] 不相等,那么唯一的元素在 nums[mid] 的左半部分 (h 左移)
代码:
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
l,h = 0,len(nums)-1
while l < h:
mid = (h+l)//2
if nums[mid] == nums[mid^1]:
l = mid + 1
else:
h = mid
return nums[l]
分割数组的最大值
题目描述:
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
数组长度 n 满足以下条件:
- 1 ≤ n ≤ 1000
- 1 ≤ m ≤ min(50, n)
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
解题思路:
二分法+贪心算法:假设 x 为连续子数组之和的最大值,即划分的每一个子数组之和都小于等于 x,若超过 x 则把当前值划分到新的序列。然后利用二分法求出满足划分组数小于等于 m 条件的最小 x.
代码:
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
def check(x): # 判断以x为上界将nums划分的组数是否<=m
cut = 1
sums = 0
for i in nums:
if sums+i > x:
sums = i
cut += 1
else:
sums += i
return cut <= m
# 二分法
left = max(nums)
right = sum(nums)
while left < right:
mid = (left + right) // 2
if check(mid):
right = mid
else:
left = mid+1
return left