1.三数之和(双指针)
题目:15.三数之和
思路:题目中要求找到所有不重复且和为0的三元组,如果是暴力法,我们需要三重循环。我们先基于三重大循环的框架,来逐步考虑优化。
-
优化1:如何去重? 一般我们采用哈希表来去重,但这个做法的时间复杂度和空间复杂度要求都很高,因此换一种思考方式:不重复的本质是什么?我们保持暴力法三重循环的大框架不变,在有序数组的情况下只需要保证:
(1)第二重循环枚举到的元素>=当前第一重循环枚举到的元素
(2)第三重循环枚举到的元素>=当前第二重循环枚举到的元素
换言之,保证(a,b,c)这个顺序会被枚举到,而(b,a,c)、(c,b,a)这些不会。 -
优化2:如何处理列表中的重复元素?对于每一重循环,相邻两次枚举的元素不能相同,否则也会造成重复,因此我们需要将循环跳到下一个不相同的元素,举个例子:
(1)[0,1,2,2,2,3]->第一个三元组(0,1,2),第二个三元组(0,1,2),第三个三元组(0,1,2)造成重复,我们希望得到(0,1,2)后的下一个三元组是(0,1,3)
(2)具体优化,体现到代码上就是:if i > 0 and nums[i] == nums[i-1]: continue
-
如果我们固定了前两重循环枚举到的元素a和b,那么唯一的c满足a+b+c=0。当第二重循环往后枚举一个元素b’时,由于b’>b,那么满足a+b’+c’的c’一定有c’<c,即c’在数组中一定出现在c的左侧。也就是我们可以从小到大枚举b的同时,从大到小枚举c(即双指针),即第二重循环和第三重循环实际上是并列关系。当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,就可以使用双指针方法,将时间复杂度由O(n^2)降到O(n)。
for i in range(len(nums)-2): if i > 0 and nums[i] == nums[i-1]: continue left = i + 1 right = len(nums) - 1 res = [] while left < right: if nums[i] + nums[left] + nums[right] == 0: res.append([nums[i], nums[left], nums[right]]) return res elif nums[i] + nums[left] + nums[right] > 0: right -= 1 else: left += 1
-
整体优化代码
def threeSum(nums:List[int]): nums.sort() res = [] for i in range(len(nums)): if i > 0 and nums[i] == nums[i-1]: continue k = len(nums) - 1 for j in range(i+1, len(nums)): if j > i + 1 and nums[j] == nums[j-1]: continue while j < k and nums[i] + nums[j] + nums[k] > 0: k -= 1 if j == k: break if nums[i] + nums[j] + nums[k] == 0: res.append([nums[i], nums[j], nums[k]]) return res
2.四数之和(双指针)
题目:18.四数之和
思路和三数之和同,只是多了一层循环,注意剪枝操作的坑!!!循环过程仍然遵循以下几点:
(1)每一重循环枚举到的下标必须大于上一重循环枚举到的下标;
(2)同一重循环中,如果当前元素与上一个元素相同,则跳过当前元素。
- 具体过程:
(1)使用两重循环分别枚举前两个数a,b,然后在两重循环枚举到的数之后使用双指针枚举后两个数c,d。假设a,b两个数的下标分别为i,j,则左右指针分别指向下标j+1,n-1。每次计算四个数之和,并进行如下操作:- 如果和等于target,则将该4个数装进列表中,然后左指针右移直到遇到不同的数,将右指针左移直到遇到不同的数
- 如果和小于target,左指针右移
- 如果和大于target,右指针左移
- 剪枝操作:
(1)在确定第一个数之后,如果nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target,说明此时剩下的三个数无论取什么值,四数之和一定大于target,因此退出第一重循环;注意是退出循环,直接break
(2)在确定第一个数之后,如果nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target,说明此时剩下的三个数无论取什么值,四数之和一定小于target,因此第一重循环直接进入下一轮,枚举nums[i+1];注意此时的i是太小了,所以退出当前循环,去枚举nums[i+1],因此用continue
(3)在确定前两个数之后,如果nums[i] + nums[j] + nums[j+1] + nums[j+2] > target,说明此时剩下的两个数无论取什么值,四数之和一定大于 \textit{target}target,因此退出第二重循环;同理,是退出循环,直接break
(4)在确定前两个数之后,如果nums[i]+nums[j]+nums[n−2]+nums[n−1] < target,说明此时剩下的两个数无论取什么值,四数之和一定小于target,因此第二重循环直接进入下一轮,枚举nums[j+1]。同理,退出当前循环,去枚举nums[i+1],因此用continue - 整体优化代码
def fourSum(self, nums: List[int], target: int) -> List[List[int]]: res = list() if not nums or len(nums) < 4: return res nums.sort() n = len(nums) for i in range(n-3): if i > 0 and nums[i] == nums[i-1]: continue if nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target: break # 剪枝操作 if nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target: continue # 剪枝操作 for j in range(i+1, n-2): if j > i+1 and nums[j] == nums[j-1]: continue if nums[i] + nums[j] + nums[j+1] + nums[j+2] > target: break # 剪枝操作 if nums[i] + nums[j] + nums[n-2] + nums[n-1] < target: continue # 剪枝操作 left, right = j+1, n-1 while left < right: if nums[i] + nums[j] + nums[left] + nums[right] == target: res.append([nums[i], nums[j], nums[left], nums[right]]) while left < right and nums[left] == nums[left+1]: left += 1 left += 1 while left < right and nums[right] == nums[right-1]: right -= 1 right -= 1 elif nums[i] + nums[j] + nums[left] + nums[right] < target: left += 1 else: right -= 1 return res