Ltd的算法大抄·nSum类型

leetcode:15.18

Posted by Ltd on Monday, March 13, 2023

这是算法记录的第6天,主要包含nSum类型题目

对应leetcode题目为15.18

两道都是nSum类型,主要是双指针技巧的使用

15.三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != 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
  • abcd 互不相同
  • 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
}