【每日一题】Day5
文章目录
- 15.三数之和
- 题目
- 思路
- Go 内置函数:append
- sort 包提供的函数:sort.Ints
- 代码实现(Go)
- 1.两数之和
- 题目
- 思路
- 数据结构map[int]int
- 遍历数组或切片range
- 代码实现(Go)
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 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
思路
1) 核心:把 3Sum 变成多个 2Sum
- 先对数组排序:
nums[0] <= nums[1] <= ... <= nums[n-1]
- 枚举第一个数的下标
i
,问题就变成在区间(i, n-1]
里找两数之和 =-nums[i]
。 - 对有序数组求 2Sum 的最优解就是双指针:左指针
L=i+1
,右指针R=n-1
。
这样每个 i
的子问题用 O(n) 找到所有解,整体 O(n^2)。
2) 排序 + 双指针更高效
单调性(这是双指针成立的关键):
- 数组已排序,固定
i
后,当前和s = nums[i] + nums[L] + nums[R]
。 - 若
s < 0
,想让和变大,只能右移L
(更大的数)。 - 若
s > 0
,想让和变小,只能左移R
(更小的数)。 - 若
s == 0
,记录答案,然后把L
向右跳过相同值,把R
向左跳过相同值,再继续收缩(因为当前这两个值再参与只会得到重复三元组)。
所以,每次比较都能单向移动一个指针,不会回头,单次子问题是线性的。
3) 去重的三个“关口”
不重复是这个题的难点,正确的“去重点位”有 3 个:
-
固定点去重(外层 i)
- 当
i > 0
且nums[i] == nums[i-1]
,说明以nums[i]
为首的所有三元组,之前以nums[i-1]
为首时已经找过,直接 continue。
- 当
-
命中解后左侧去重(L)
- 当发现一组解
nums[i] + nums[L] + nums[R] == 0
,把这组解加入结果; - 随后
for L < R && nums[L] == nums[L+1] { L++ }
,跳过左边重复的值,避免得到完全相同的三元组。
- 当发现一组解
-
命中解后右侧去重(R)
- 同理,
for L < R && nums[R] == nums[R-1] { R-- }
,跳过右边重复的值。
- 同理,
只有在已经命中一个解之后,才对
L
、R
做“跳过相同值”的去重。否则在sum<0
或sum>0
的情况下,直接常规地移动一个指针即可(不需要跳过相同值,因为没产生解,自然不会重复记录)。
4) 正确性
-
不漏:
- 对每个
i
,在(i, n-1]
上用双指针列举所有两数和为-nums[i]
的组合; - 双指针的移动规则保证“所有可能的
(L, R)
”都会按序被考虑到(除了被判断为重复而跳过的那些,它们对应的三元组与已记录的完全相同)。
- 对每个
-
不重:
- 对
i
的去重避免同一个首元素被重复固定; - 命中解后对
L
、R
的去重,避免同一首元素下,相同的第二、第三元素被重复组合; - 所以每个唯一三元组只会被记录一次。
- 对
排序建立单调、双指针利用单调、三处去重保证唯一
Go 内置函数:append
语法:slice = append(slice, elem1, elem2, ... )
slice:必须是切片(slice),不能是数组。
elem1, elem2, …:要追加的元素,可以是一个或多个。
返回值:返回一个新的切片(可能和原来的底层数组不同)。
必须接收返回值:所以常见写法是 s = append(nums, x)
sort 包提供的函数:sort.Ints
定义:func Ints(a []int) ,只能排序 []int,无返回值,默认升序
使用:import "sort"
sort.Ints(nums)
参数:nums []int(一个 int 类型的切片)
代码实现(Go)
详细注解:
//package main
//
//import (
// "fmt"
// "sort"
//)func threeSum(nums []int) [][]int {res := [][]int{}n := len(nums)// 特判if n < 3 {return res}// 1. 先排序sort.Ints(nums)// 2. 遍历数组,固定第一个数 nums[i]for i := 0; i < n-2; i++ {// 剪枝:如果当前数大于0,后面不可能找到和为0的三元组if nums[i] > 0 {break}// 跳过重复的已处理过的数,避免重复结果if i > 0 && nums[i] == nums[i-1] {continue}// 3. 定义双指针// 在 nums[i+1 ... n-1] 之间,找另外两个数,让三者加和为 0L, R := i+1, n-1for L < R {sum := nums[i] + nums[L] + nums[R]// 根据结果来移动指针if sum == 0 {res = append(res, []int{nums[i], nums[L], nums[R]})// 跳过重复的左指针for L < R && nums[L] == nums[L+1] {L++}// 跳过重复的右指针for L < R && nums[R] == nums[R-1] {R--}// 移动双指针,继续寻找L++R--} else if sum < 0 {// 总和太小,左指针右移L++} else {// 总和太大,右指针左移R--}}}return res
}//func main() {
// nums := []int{-1, 0, 1, 2, -1, -4}
// fmt.Println(threeSum(nums)) // 输出 [[-1 -1 2] [-1 0 1]]
//}
无注释:
//package main
//
//import (
// "fmt"
// "sort"
//)func threeSum(nums []int) [][]int {res := [][]int{}n := len(nums)if n < 3 {return res}sort.Ints(nums)for i := 0; i < n-2; i++ {if nums[i] > 0 {break}if i > 0 && nums[i] == nums[i-1] {continue}L, R := i+1, n-1for L < R {sum := nums[i] + nums[L] + nums[R]if sum == 0 {res = append(res, []int{nums[i], nums[L], nums[R]})for L < R && nums[L] == nums[L+1] {L++}for L < R && nums[R] == nums[R-1] {R--}L++R--} else if sum < 0 {L++} else {R--}}}return res
}//func main() {
// nums := []int{-1, 0, 1, 2, -1, -4}
// fmt.Println(threeSum(nums)) // 输出 [[-1 -1 2] [-1 0 1]]
//}
1.两数之和
题目
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
思路
每次遍历一个数字 num,去找 target - num 是否在哈希表
里
如果找到了 → 返回 [之前存的下标, 当前下标]
如果没找到 → 把当前数字存入哈希表,备用
这样就能一次遍历找到答案,不需要双重循环
- 如果当前元素的配对元素在它之前出现 → 可以在哈希表中找到
- 如果当前元素的配对元素在它之后出现 → 当遍历到那个元素时,当前元素已经被加入哈希表 → 同样可以找到答案
- 因此只要遍历数组一次,就能找到唯一答案
数据结构map[int]int
map 是 Go 的哈希表(字典)
key:整数类型 int → 存储数字值
value:整数类型 int → 存储该数字在数组中的下标
if j, ok := m[num2]; ok { ... }
返回两个值:
j → 哈希表中 num2 对应的下标
ok → 布尔值,表示 key num2 是否存在于 map 中
true → 存在
false → 不存在
遍历数组或切片range
for i, num1 := range nums { ... }
返回两个值:
i → 当前元素的下标(索引)
num1 → 当前元素的值
for _, num1 := range nums { ... } // 忽略索引
for i := range nums { ... } // 忽略值
代码实现(Go)
详细注解:
//package main
//
//import "fmt"func twoSum(nums []int, target int) []int {// 创建哈希表,key: 数字值,value: 数字在数组中的下标m := make(map[int]int)for i, num1 := range nums { // range返回索引,值num2 := target - num1 // 需要的另一个数字if j, ok := m[num2]; ok { // j :哈希表中 nums2 的 下标// 如果哈希表中存在 num2,说明找到了答案return []int{j, i}}// 否则,把当前数字及索引存入哈希表m[num1] = i}return nil // 返回空数组 nil 代替空切片
}//func main() {
// nums := []int{2, 7, 11, 15}
// target := 9
// result := twoSum(nums, target)
// fmt.Println(result) // 输出 [0, 1]
//}
无注释:
//package main
//
//import "fmt"func twoSum(nums []int, target int) []int {m := make(map[int]int)for i := 0; i < len(nums); i++ {num1 := nums[i]num2 := target - num1if j, ok := m[num2]; ok {return []int{j, i}}m[num1] = i}return nil
}//func main() {
// nums := []int{2, 7, 11, 15}
// target := 9
// result := twoSum(nums, target)
// fmt.Println(result) // 输出 [0, 1]
//}