这是算法记录的第6天,主要包含nSum类型题目
对应leetcode题目为15.18
两道都是nSum类型,主要是双指针技巧的使用
15.三数之和
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
思路
排序+左右指针,整体就是减少求和的个数,一层一层计算,还有关键是边界处理,重复处理。
具体来说,先排序使得左右指针指向一大一小有意义。求出比目标值偏大,r–;比目标值偏小,l++;当满足目标值的时候就加进结果中。其中由于排序了,连续相同的值就可以排除(否则会存在重复三元组)
依据这个思路可以引入递归,写出更通用的解法,只不过改变了target
排序+双指针
func threeSum(nums []int) [][]int {
res := make([][]int, 0)
//先对nums排序,时间复杂度是nlogn
sort.Ints(nums)
//将3个之和降为2个,这个k就是第一个
//len(nums)-2保证了nums[k+1]数组不越界
for k := 0; k < len(nums)-2; k++ {
//若第一个最小的都大于0说明后面的就更不满足了
if nums[k] > 0 {
return res
}
//和之前的比较进行去重
if k > 0 && nums[k] == nums[k-1] {
continue
}
//定义左右指针
l, r := k+1, len(nums)-1
for l < r {
//求和
sum := nums[k] + nums[l] + nums[r]
if sum < 0 {
l++
} else if sum > 0 {
r--
} else {
//找到一个目标三元组,添加到结果中
res = append(res, []int{nums[k], nums[l], nums[r]})
//将当前的值保存下来,便于后续进行比较
l1, r1 := nums[l], nums[r]
//先+1,减少一次冗余比较
for l++; l < r && nums[l] == l1; l++ {
}
//先-1,减少一次冗余比较
for r--; l < r && nums[r] == r1; r-- {
}
}
}
}
return res
}
通用解法(排序+双指针+递归)
func threeSum1(nums []int) [][]int {
// n为3,从 nums[0] 开始计算和为0的三元组
return nSumTarget(nums, 3, 0, 0)
}
// nSum的通用解法
// n填写想求的是几数之和,start从哪个索引开始计算(一般填0),target填想凑出的目标和
func nSumTarget(nums []int, n, start, target int) [][]int {
sort.Ints(nums)
length := len(nums)
var res [][]int
// 至少是 2Sum,且数组大小不应该小于 n
if n < 2 || length < n {
return res
}
//当n大于2的通用情况
if n > 2 {
// n > 2 时,递归计算 (n-1)Sum 的结果
for i := start; i < length; i++ {
//新的target就是原target减去当前遍历的那个
//这个递归函数返回的就是包含n-1元组的二维切片
sub := nSumTarget(nums, n-1, i+1, target-nums[i])
for _, arr := range sub {
//n-1元组加上 nums[i] 就是n元组了
arr = append(arr, nums[i])
res = append(res, arr)
}
//边界处理同时去重
for i < length-1 && nums[i] == nums[i+1] {
i++
}
}
} else {
//n=2时,双指针
l, r := start, length-1
for l < r {
sum := nums[l] + nums[r]
left, right := nums[l], nums[r]
if sum < target {
for l < r && nums[l] == left {
l++
}
} else if sum > target {
for l < r && nums[r] == right {
r--
}
} else {
res = append(res, []int{left, right})
for l++; l < r && nums[l] == left; {
l++
}
for r--; l < r && nums[r] == right; {
r--
}
}
}
}
return res
}
18.四数之和
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
思路
这个理论也可以像上面那道一样不使用通用解法,只不过是多套一层循环,但过于麻烦就不写了,直接使用通用解法更方便
代码
func fourSum(nums []int, target int) [][]int {
// n 为 4,从 nums[0] 开始计算和为 target 的四元组
return nSumTarget(nums, 4, 0, target)
}
//这个和上面那个完全一样
func nSumTarget(nums []int, n, start, target int) [][]int {
sort.Ints(nums)
length := len(nums)
var res [][]int
// 至少是 2Sum,且数组大小不应该小于 n
if n < 2 || length < n {
return res
}
//当n大于2的通用情况
if n > 2 {
// n > 2 时,递归计算 (n-1)Sum 的结果
for i := start; i < length; i++ {
//新的target就是原target减去当前遍历的那个
//这个递归函数返回的就是包含n-1元组的二维切片
sub := nSumTarget(nums, n-1, i+1, target-nums[i])
for _, arr := range sub {
//n-1元组加上 nums[i] 就是n元组了
arr = append(arr, nums[i])
res = append(res, arr)
}
//边界处理同时去重
for i < length-1 && nums[i] == nums[i+1] {
i++
}
}
} else {
//n=2时,双指针
l, r := start, length-1
for l < r {
sum := nums[l] + nums[r]
left, right := nums[l], nums[r]
if sum < target {
for l < r && nums[l] == left {
l++
}
} else if sum > target {
for l < r && nums[r] == right {
r--
}
} else {
res = append(res, []int{left, right})
for l++; l < r && nums[l] == left; {
l++
}
for r--; l < r && nums[r] == right; {
r--
}
}
}
}
return res
}