2 つの数値の合計はハッシュ テーブルで解決されますが、3 つの数値の合計はどうなるでしょうか。
問題 15. 3 つの数値の和
n 個の整数を含む配列 nums が与えられた場合、nums に 3 つの要素 a、b、c があり、a + b + c = 0 となるかどうかを判断します。条件を満たし、重複していないトリプルをすべて見つけてください。
注:回答内で重複する 3 つの要素を使用することはできません。
例:
配列 nums = [-1, 0, 1, 2, -1, -4] の場合、
要件を満たすトリプルのセットは
[
[-1, 0, 1],
[-1, -1, 2]
]です。
一連の考え
[0, 0, 0, 0] このデータセットに注意してください
ハッシュソリューション
2 層の for ループで a と b の値を決定できます。ハッシュ法を使用して、配列に 0-(a+b) が出現したかどうかを判断できます。実際、この考えは正しいですが、非常に難しい問題です。つまり、タイトルには、繰り返しのトリプルを含めることはできないと書かれています。
修飾されたトリプルをベクトルに入れて重複を除去するのは非常に時間がかかり、タイムアウトになりやすいです。これがこの質問の合格率が低い根本原因でもあります。
重複排除のプロセスは、面接で考えるのが難しい場合でも、細かい点がたくさんあるため、対処するのは簡単ではありません。
時間計算量は O(n^2) になる可能性がありますが、枝刈り操作を実行するのは簡単ではないため、それでも時間がかかります。
実際にハッシュメソッドを使って書いてみると、その難しさが分かると思います。
C++ コードのハッシュ:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 三元组元素b去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
ダブルポインタ
実際、この質問にハッシュ法を使用することはあまり適切ではありません。重複排除操作には注意が必要な詳細が多く、インタビューでバグのないコードを直接記述するのは困難だからです。
また、ハッシュ法を使用して for ループを 2 層使用する場合、実行できる枝刈り操作は非常に限られており、時間計算量は O(n^2) で leetcode にも渡すことができますが、実行時間は非常に限られます。プログラムはまだ比較的長いです。
次に、もう一つの解決策であるダブルポインタ法を紹介しますが、この問題はハッシュ法よりもダブルポインタ法を使用した方が効率が良いので、具体的な実装の考え方を説明します。
アニメーション効果は次のとおりです。
この nums 配列を例として、まず配列をソートし、次に for ループの層を設け、i を添え字 0 から開始し、同時に i+1 の位置に定義する添え字を設定して、定義します。添字は配列位置の最後に配置されます。
さらに、配列内で a + b +c = 0 となる abc を見つけます。ここでは、a = nums[i]、b = nums[left]、c = nums[right] と等価です。
次に、左右に移動するにはどうすればよいでしょうか? nums[i] + nums[left] + nums[right] > 0 の場合、配列はソートされているため、この時点で 3 つの数値の合計が大きいことを意味します。右の添え字は、3 つの数値の合計が小さくなるように左に移動する必要があります。
nums[i] + nums[left] + nums[right] < 0 の場合、この時点では 3 つの数値の合計が小さいことを意味し、左が右に移動して左まで 3 つの数値の合計が大きくなります。そして右に会う。
時間計算量: O(n^2)。
C++ コードのコードは次のとおりです。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0) right--;
else if (nums[i] + nums[left] + nums[right] < 0) left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
重複排除の論理的思考
の重複排除
重複排除といえば、実際、主に考慮すべきは 3 つの番号の重複排除です。a、b、c、nums[i]、nums[left]、nums[right] に対応
a が繰り返された場合はどうなるでしょうか。a は nums で走査される要素であるため、直接スキップする必要があります。
ただし、ここで nums[i] が nums[i + 1] と同じかどうかを判断するか、nums[i] が nums[i-1] と同じかどうかを判断するかという問題があります。
学生の中には「どれも同じではないか?」と思う人もいるかもしれません。
実は違うんです!
それらはすべて、前のものと後のものを比較するかどうかに関係なく、nums[i] と比較されます。
このように書くと:
if (nums[i] == nums[i + 1]) { // 去重操作
continue;
}
次に、トリプレット内の繰り返し要素のケースを直接渡します。たとえば、 {-1, -1 ,2} このデータセットは、最初の -1 まで移動すると、次のデータも -1 であると判断され、このデータセットが渡されます。
私たちがしなければならないことは、トリプルを繰り返すことはできませんが、トリプル内の要素は繰り返すことができるということです。
したがって、ここには 2 つの繰り返し次元があります。
次に、次のように書く必要があります。
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
この書き方は現在 nums[i] を使用しており、最初の -1 に到達するときに、データのセット {-1, -1 ,2} を見て、前のものが同じ要素であるかどうかを判断します。前のもの -1 がない場合は、{-1, -1 ,2} も結果セットに含めることができます。
これは非常に詳細な思考プロセスです。
b と c の重複排除
多くの学生がこの質問を書いたとき、重複排除のロジックに右と左の重複排除が追加されました: (コードのコメント部分)
while (right > left) {
if (nums[i] + nums[left] + nums[right] > 0) {
right--;
// 去重 right
while (left < right && nums[right] == nums[right + 1]) right--;
} else if (nums[i] + nums[left] + nums[right] < 0) {
left++;
// 去重 left
while (left < right && nums[left] == nums[left - 1]) left++;
} else {
}
}
しかし、よく考えてみると、この種の重複排除は実際にはプログラムの効率向上には役に立ちません。
右重複排除を例にとると、この重複排除ロジックがなくてもwhile (right > left)
、if (nums[i] + nums[left] + nums[right] > 0)
右操作は と に従って完了します。
while (left < right && nums[right] == nums[right + 1]) right--;
このコード行の追加は、実際には事前に実行する必要があるロジックを実行するためのものですが、判定ロジックを削減するものではありません。
最も単純な思考プロセスは、権利が 1 つずつ減算されるため、減算はどこでも同じであるということです。
したがって、この種の重複排除は省略できます。それは重複排除のロジックを前進させるだけです。
考える質問
3つの数値の和はダブルポインタ法が使えるので、先ほど話した1.2つの数値の和にもダブルポインタ法が使えるでしょうか?
そうでない場合、ダブルポインタ法を使用できるように質問の意味をどのように変更できますか? みなさんもぜひメッセージを残して感想を伝えてください!
ダブル ポインタ メソッドは 1 であるため、2 つの数値の合計には使用できません。インデックス添字を返すには2 つの数値の合計が必要であり、ダブル ポインタ メソッドは並べ替える必要があります。並べ替えると、元の配列のインデックスは次のようになります。かわった。
1 の場合、値を返すために2 つの数値の合計が必要な場合は、ダブル ポインター メソッドを使用できます。
他の言語バージョン
ジャワ:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
}
パイソン:
class Solution:
def threeSum(self, nums):
ans = []
n = len(nums)
nums.sort()
# 找出a + b + c = 0
# a = nums[i], b = nums[left], c = nums[right]
for i in range(n):
left = i + 1
right = n - 1
# 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if nums[i] > 0:
break
if i >= 1 and nums[i] == nums[i - 1]: # 去重a
continue
while left < right:
total = nums[i] + nums[left] + nums[right]
if total > 0:
right -= 1
elif total < 0:
left += 1
else:
ans.append([nums[i], nums[left], nums[right]])
# 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while left != right and nums[left] == nums[left + 1]: left += 1
while left != right and nums[right] == nums[right - 1]: right -= 1
left += 1
right -= 1
return ans
Python (v3):
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
if len(nums) < 3: return []
nums, res = sorted(nums), []
for i in range(len(nums) - 2):
cur, l, r = nums[i], i + 1, len(nums) - 1
if res != [] and res[-1][0] == cur: continue # Drop duplicates for the first time.
while l < r:
if cur + nums[l] + nums[r] == 0:
res.append([cur, nums[l], nums[r]])
# Drop duplicates for the second time in interation of l & r. Only used when target situation occurs, because that is the reason for dropping duplicates.
while l < r - 1 and nums[l] == nums[l + 1]:
l += 1
while r > l + 1 and nums[r] == nums[r - 1]:
r -= 1
if cur + nums[l] + nums[r] > 0:
r -= 1
else:
l += 1
return res
行く:
func threeSum(nums []int) [][]int {
sort.Ints(nums)
res := [][]int{}
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for i := 0; i < len(nums)-2; i++ {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
n1 := nums[i]
if n1 > 0 {
break
}
// 去重a
if i > 0 && n1 == nums[i-1] {
continue
}
l, r := i+1, len(nums)-1
for l < r {
n2, n3 := nums[l], nums[r]
if n1+n2+n3 == 0 {
res = append(res, []int{n1, n2, n3})
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
for l < r && nums[l] == n2 {
l++
}
for l < r && nums[r] == n3 {
r--
}
} else if n1+n2+n3 < 0 {
l++
} else {
r--
}
}
}
return res
}
JavaScript:
var threeSum = function(nums) {
const res = [], len = nums.length
// 将数组排序
nums.sort((a, b) => a - b)
for (let i = 0; i < len; i++) {
let l = i + 1, r = len - 1, iNum = nums[i]
// 数组排过序,如果第一个数大于0直接返回res
if (iNum > 0) return res
// 去重
if (iNum == nums[i - 1]) continue
while(l < r) {
let lNum = nums[l], rNum = nums[r], threeSum = iNum + lNum + rNum
// 三数之和小于0,则左指针向右移动
if (threeSum < 0) l++
else if (threeSum > 0) r--
else {
res.push([iNum, lNum, rNum])
// 去重
while(l < r && nums[l] == nums[l + 1]){
l++
}
while(l < r && nums[r] == nums[r - 1]) {
r--
}
l++
r--
}
}
}
return res
};
解決策 2: nSum の一般的な解決策。再帰
/**
* nsum通用解法,支持2sum,3sum,4sum...等等
* 时间复杂度分析:
* 1. n = 2时,时间复杂度O(NlogN),排序所消耗的时间。、
* 2. n > 2时,时间复杂度为O(N^n-1),即N的n-1次方,至少是2次方,此时可省略排序所消耗的时间。举例:3sum为O(n^2),4sum为O(n^3)
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function (nums) {
// nsum通用解法核心方法
function nSumTarget(nums, n, start, target) {
// 前提:nums要先排序好
let res = [];
if (n === 2) {
res = towSumTarget(nums, start, target);
} else {
for (let i = start; i < nums.length; i++) {
// 递归求(n - 1)sum
let subRes = nSumTarget(
nums,
n - 1,
i + 1,
target - nums[i]
);
for (let j = 0; j < subRes.length; j++) {
res.push([nums[i], ...subRes[j]]);
}
// 跳过相同元素
while (nums[i] === nums[i + 1]) i++;
}
}
return res;
}
function towSumTarget(nums, start, target) {
// 前提:nums要先排序好
let res = [];
let len = nums.length;
let left = start;
let right = len - 1;
while (left < right) {
let sum = nums[left] + nums[right];
if (sum < target) {
while (nums[left] === nums[left + 1]) left++;
left++;
} else if (sum > target) {
while (nums[right] === nums[right - 1]) right--;
right--;
} else {
// 相等
res.push([nums[left], nums[right]]);
// 跳过相同元素
while (nums[left] === nums[left + 1]) left++;
while (nums[right] === nums[right - 1]) right--;
left++;
right--;
}
}
return res;
}
nums.sort((a, b) => a - b);
// n = 3,此时求3sum之和
return nSumTarget(nums, 3, 0, 0);
};
タイプスクリプト:
function threeSum(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
let length = nums.length;
let left: number = 0,
right: number = length - 1;
let resArr: number[][] = [];
for (let i = 0; i < length; i++) {
if (nums[i]>0) {
return resArr; //nums经过排序后,只要nums[i]>0, 此后的nums[i] + nums[left] + nums[right]均大于0,可以提前终止循环。
}
if (i > 0 && nums[i] === nums[i - 1]) {
continue;
}
left = i + 1;
right = length - 1;
while (left < right) {
let total: number = nums[i] + nums[left] + nums[right];
if (total === 0) {
resArr.push([nums[i], nums[left], nums[right]]);
left++;
right--;
while (nums[right] === nums[right + 1]) {
right--;
}
while (nums[left] === nums[left - 1]) {
left++;
}
} else if (total < 0) {
left++;
} else {
right--;
}
}
}
return resArr;
};
ルビー:
def is_valid(strs)
symbol_map = {
')' => '(', '}' => '{', ']' => '['}
stack = []
strs.size.times {
|i|
c = strs[i]
if symbol_map.has_key?(c)
top_e = stack.shift
return false if symbol_map[c] != top_e
else
stack.unshift(c)
end
}
stack.empty?
end
PHP:
class Solution {
/**
* @param Integer[] $nums
* @return Integer[][]
*/
function threeSum($nums) {
$res = [];
sort($nums);
for ($i = 0; $i < count($nums); $i++) {
if ($nums[$i] > 0) {
return $res;
}
if ($i > 0 && $nums[$i] == $nums[$i - 1]) {
continue;
}
$left = $i + 1;
$right = count($nums) - 1;
while ($left < $right) {
$sum = $nums[$i] + $nums[$left] + $nums[$right];
if ($sum < 0) {
$left++;
}
else if ($sum > 0) {
$right--;
}
else {
$res[] = [$nums[$i], $nums[$left], $nums[$right]];
while ($left < $right && $nums[$left] == $nums[$left + 1]) $left++;
while ($left < $right && $nums[$right] == $nums[$right - 1]) $right--;
$left++;
$right--;
}
}
}
return $res;
}
}
迅速:
// 双指针法
func threeSum(_ nums: [Int]) -> [[Int]] {
var res = [[Int]]()
var sorted = nums
sorted.sort()
for i in 0 ..< sorted.count {
if sorted[i] > 0 {
return res
}
if i > 0 && sorted[i] == sorted[i - 1] {
continue
}
var left = i + 1
var right = sorted.count - 1
while left < right {
let sum = sorted[i] + sorted[left] + sorted[right]
if sum < 0 {
left += 1
} else if sum > 0 {
right -= 1
} else {
res.append([sorted[i], sorted[left], sorted[right]])
while left < right && sorted[left] == sorted[left + 1] {
left += 1
}
while left < right && sorted[right] == sorted[right - 1] {
right -= 1
}
left += 1
right -= 1
}
}
}
return res
}
さび:
// 哈希解法
use std::collections::HashSet;
impl Solution {
pub fn three_sum(nums: Vec<i32>) -> Vec<Vec<i32>> {
let mut result: Vec<Vec<i32>> = Vec::new();
let mut nums = nums;
nums.sort();
let len = nums.len();
for i in 0..len {
if nums[i] > 0 { break; }
if i > 0 && nums[i] == nums[i - 1] { continue; }
let mut set = HashSet::new();
for j in (i + 1)..len {
if j > i + 2 && nums[j] == nums[j - 1] && nums[j] == nums[j - 2] { continue; }
let c = 0 - (nums[i] + nums[j]);
if set.contains(&c) {
result.push(vec![nums[i], nums[j], c]);
set.remove(&c);
} else { set.insert(nums[j]); }
}
}
result
}
}
// 双指针法
use std::cmp::Ordering;
impl Solution {
pub fn three_sum(nums: Vec<i32>) -> Vec<Vec<i32>> {
let mut result: Vec<Vec<i32>> = Vec::new();
let mut nums = nums;
nums.sort();
let len = nums.len();
for i in 0..len {
if nums[i] > 0 { return result; }
if i > 0 && nums[i] == nums[i - 1] { continue; }
let (mut left, mut right) = (i + 1, len - 1);
while left < right {
match (nums[i] + nums[left] + nums[right]).cmp(&0){
Ordering::Equal =>{
result.push(vec![nums[i], nums[left], nums[right]]);
left +=1;
right -=1;
while left < right && nums[left] == nums[left - 1]{
left += 1;
}
while left < right && nums[right] == nums[right+1]{
right -= 1;
}
}
Ordering::Greater => right -= 1,
Ordering::Less => left += 1,
}
}
}
result
}
}
子:
//qsort辅助cmp函数
int cmp(const void* ptr1, const void* ptr2) {
return *((int*)ptr1) > *((int*)ptr2);
}
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
//开辟ans数组空间
int **ans = (int**)malloc(sizeof(int*) * 18000);
int ansTop = 0;
//若传入nums数组大小小于3,则需要返回数组大小为0
if(numsSize < 3) {
*returnSize = 0;
return ans;
}
//对nums数组进行排序
qsort(nums, numsSize, sizeof(int), cmp);
int i;
//用for循环遍历数组,结束条件为i < numsSize - 2(因为要预留左右指针的位置)
for(i = 0; i < numsSize - 2; i++) {
//若当前i指向元素>0,则代表left和right以及i的和大于0。直接break
if(nums[i] > 0)
break;
//去重:i > 0 && nums[i] == nums[i-1]
if(i > 0 && nums[i] == nums[i-1])
continue;
//定义左指针和右指针
int left = i + 1;
int right = numsSize - 1;
//当右指针比左指针大时进行循环
while(right > left) {
//求出三数之和
int sum = nums[right] + nums[left] + nums[i];
//若和小于0,则左指针+1(因为左指针右边的数比当前所指元素大)
if(sum < 0)
left++;
//若和大于0,则将右指针-1
else if(sum > 0)
right--;
//若和等于0
else {
//开辟一个大小为3的数组空间,存入nums[i], nums[left]和nums[right]
int* arr = (int*)malloc(sizeof(int) * 3);
arr[0] = nums[i];
arr[1] = nums[left];
arr[2] = nums[right];
//将开辟数组存入ans中
ans[ansTop++] = arr;
//去重
while(right > left && nums[right] == nums[right - 1])
right--;
while(left < right && nums[left] == nums[left + 1])
left++;
//更新左右指针
left++;
right--;
}
}
}
//设定返回的数组大小
*returnSize = ansTop;
*returnColumnSizes = (int*)malloc(sizeof(int) * ansTop);
int z;
for(z = 0; z < ansTop; z++) {
(*returnColumnSizes)[z] = 3;
}
return ans;
}
C#:
public class Solution
{
public IList<IList<int>> ThreeSum(int[] nums)
{
var result = new List<IList<int>>();
Array.Sort(nums);
for (int i = 0; i < nums.Length - 2; i++)
{
int n1 = nums[i];
if (n1 > 0)
break;
if (i > 0 && n1 == nums[i - 1])
continue;
int left = i + 1;
int right = nums.Length - 1;
while (left < right)
{
int n2 = nums[left];
int n3 = nums[right];
int sum = n1 + n2 + n3;
if (sum > 0)
{
right--;
}
else if (sum < 0)
{
left++;
}
else
{
result.Add(new List<int> {
n1, n2, n3 });
while (left < right && nums[left] == n2)
{
left++;
}
while (left < right && nums[right] == n3)
{
right--;
}
}
}
}
return result;
}
}
スカラ:
object Solution {
// 导包
import scala.collection.mutable.ListBuffer
import scala.util.control.Breaks.{
break, breakable}
def threeSum(nums: Array[Int]): List[List[Int]] = {
// 定义结果集,最后需要转换为List
val res = ListBuffer[List[Int]]()
val nums_tmp = nums.sorted // 对nums进行排序
for (i <- nums_tmp.indices) {
// 如果要排的第一个数字大于0,直接返回结果
if (nums_tmp(i) > 0) {
return res.toList
}
// 如果i大于0并且和前一个数字重复,则跳过本次循环,相当于continue
breakable {
if (i > 0 && nums_tmp(i) == nums_tmp(i - 1)) {
break
} else {
var left = i + 1
var right = nums_tmp.length - 1
while (left < right) {
var sum = nums_tmp(i) + nums_tmp(left) + nums_tmp(right) // 求三数之和
if (sum < 0) left += 1
else if (sum > 0) right -= 1
else {
res += List(nums_tmp(i), nums_tmp(left), nums_tmp(right)) // 如果等于0 添加进结果集
// 为了避免重复,对left和right进行移动
while (left < right && nums_tmp(left) == nums_tmp(left + 1)) left += 1
while (left < right && nums_tmp(right) == nums_tmp(right - 1)) right -= 1
left += 1
right -= 1
}
}
}
}
}
// 最终返回需要转换为List,return关键字可以省略
res.toList
}
}