一、力扣题454. 四数相加 II
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2] 输出:2 解释: 两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0] 输出:1
提示:
n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228
本题的解题逻辑和上一天的两数之和的题差不多,并且是四个独立数组的元素下标组成元组,无需担心元素重复问题。
两数之和是把其中一个数组存入hash数组,然后遍历另一个数组的元素,计算出符合条件的另一个元素后从hash数组中寻找是否有符合条件的元素。
本题的逻辑也一样,先把两个数组中的两个元素的和存入hash数组,再遍历得到另外两个数组元素的和,在hash数组中查询是否有对应的值。
function fourSumCount($nums1, $nums2, $nums3, $nums4) {
$sum = 0;
$hash = [];
for($i = 0; $i < count($nums1); $i++) {
for($j = 0; $j < count($nums2); $j++) {
if(!isset($hash[$nums1[$i] + $nums2[$j]])) {
$hash[$nums1[$i] + $nums2[$j]] = 1;
} else {
$hash[$nums1[$i] + $nums2[$j]]++;
}
}
}
for($i = 0; $i < count($nums3); $i++) {
for($j = 0; $j < count($nums4); $j++) {
if(isset($hash[0 - $nums3[$i] - $nums4[$j]])) {
$sum += $hash[0 - $nums3[$i] - $nums4[$j]];
}
}
}
return $sum;
}
二、力扣题383. 赎金信
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b" 输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab" 输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab" 输出:true
提示:
1 <= ransomNote.length, magazine.length <= 105
ransomNote
和magazine
由小写英文字母组成
本题的解题逻辑和之前的题没什么区别,把magazine的字母存入hash数组,hash数组键名为字母,键值为该字母在magazine中出现的次数。然后遍历ransomNote字符串,当遍历的字母在hash数组中能查询到且值>0时,继续遍历,反之直接返回false。
function canConstruct($ransomNote, $magazine) {
$hash = [];
for($i = 0; $i < strlen($magazine); $i++) {
if(!isset($hash[$magazine[$i]])) {
$hash[$magazine[$i]] = 1;
} else {
$hash[$magazine[$i]]++;
}
}
for($i = 0; $i < strlen($ransomNote); $i++) {
if(isset($hash[$ransomNote[$i]]) && $hash[$ransomNote[$i]] > 0) {
$hash[$ransomNote[$i]]--;
} else {
return false;
}
}
return true;
}
三、力扣题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
这道题和之前的四数之和、两数之和不一样,这道题是在一个数组中找到三元组,还要求是元素不重复的,难点在于去重。我是直接看题解的,但是看完题解再来写一遍还是出了不少错漏之处。去重的细节很多,而且还要尽量注意剪枝。
方法一:哈希法
用哈希法的逻辑和之前的题还是差不多的,先把nums数组中的元素按顺序排列,设置两个循环,外循环遍历一遍数组,获取元组的第一个元素的下标 i ,内循环的 j 初始值为 i + 1,同样遍历一遍,这是元组的第二个元素的下标 j ,然后获取第三个元素 three 为 0 - nums[i] - nums[j] ,如果元素three 在hash 数组中存在,则把这个元组存入结果集,若不存在,则把元素下标 j 对应的值存入hash数组。
过程如下所示
i 的去重和剪枝
按照以上逻辑,若nums数组为 [ -1, -1, -1,0, 2, 0] 时,结果集就会出现两个相同的元组 [ -1, -1, 2],故需避免第一个元素的重复,当 nums[ i ] = nums[ i - 1] 且 i > 0 时,直接 i + 1 。至于为什么判断条件不是若 nums[ i ] = nums[ i + 1] 则 i + 1,因为如果这样判断的话, i 会直接跳到下标为 2 的地方,也就是第三个元素的位置再开始进行 j 的遍历,这样就错过了元组[ -1,-1,2]。
另一个就是剪枝问题,因为数组现在是顺序排列,并且元组需要三个元素的和为0,i 对应的元素作为元组中第一个元素,一定是小于或等于其他两个元素的,若 i 对应的值大于0,那一定不会有符合条件的元组,故当 i 对应的值大于 0 时就可以直接跳出循环了。
j 的去重
当nums数组为[-2,1,1,1,1,0,3]时,结果集也会出现两个重复元组[ -2,1,1], 故需去重,当 j > i + 2 且 nums[ j ] = nums [ j - 1] 且 nums[ j - 1] = nums[ j - 2] 时,j ++。
k的去重
元组中的第三个元素下标,原题定义为 k,代码中将 k 下标对应的值 设为three,当通过 i ,j 得出 第三个元素的值 three,若hash 数组中有键名为 three 的值,则元组[ nums[i], nums[j], three]成立,此时需 删除hash 数组中对应的 three 的值,如果没有删除,当nums 数组为 [ -2, -1, -1, 0, 3 ] 时,结果集就会出现两个元组 [ -2,-1,3]。
hash数组的循环
这里需要注意的一个点是,hash 数组是在外循环 i 的循环中被定义的,也就是每次 i + 1 时,hash 数组都会被重置为空,这样才不会让上一个循环里存放的值 影响这次循环的判断,举个例子,nums数组为[ -3, -2, 0 , 1, 3], 当 i 对应的值为 -4 时,内循环后hash数组为 [ -2, 1], 然后 i + 1,i对应的值变为 -2,若此时 hash 数组没有重置, 在内循环中 当 j 对应值 为 1 时,计算出第三个元素为 1, 而 hash 数组中有 1 这个元素,故将 [-2, 1, 1] 加入结果集,而这显然是错误的。
function threeSum($nums) {
$result = []; // 存放结果集
sort($nums); //对数组进行升序排列
for($i = 0; $i < count($nums) - 1; $i++) {
//因为数组为升序排列,故若第一个元素>0,i之后的元素也>0,就不可能组成三元组,故退出循环。
if($nums[$i] > 0) {
break;
}
$hash = [];
//这一步是对第一个元素i的去重,不使用 $nums[$i] == $nums[$i+1] 判断的原因看上面解释
if($i > 0 && $nums[$i] == $nums[$i-1]) {
continue;
}
for($j = $i + 1; $j < count($nums); $j++) {
//对第二个元素的去重
if($j > $i + 2 && $nums[$j] == $nums[$j - 1] && $nums[$j - 1] == $nums[$j - 2]) {
continue;
}
//得出若要满足三元组条件时,第三个元素的值
$three = 0-$nums[$i]-$nums[$j];
//若该判断成立,说明前面已经遍历完成的元素中有 three 这个值,可以组成三元组
if(isset($hash[$three])) {
$result[] = [$nums[$i], $nums[$j], $three];
//为了对第三个元素进行去重,会删去hash数组中three的值
unset($hash[$three]);
} else {
$hash[$nums[$j]] = 1;
}
}
}
return $result;
}
方法二:双指针法
很巧妙的方法,我完全想不到。
同样将数组进行顺序排列,设置两个循环,外循环 依旧为 i ,内循环 j = i + 1,而 k = count($nums) - 1,即数组的最后一个元素的下标,将 j 定义为 left 指针,k 定义为 right 指针,在 i 的外循环,当 nums[ i ] + nums [ j ] + nums [ k ] < 0 时,说明元素不够大,又因数组是升序排列, 故将left 指针向右移动,使 j 元素变大;当三数之和 > 0 时,则right 指针向左移动,使 k 元素变小; 当三数之和刚好等于 0 时,将这三个数的元组放入结果集,然后left 和 right 指针分别向右向左移动。
需要注意的一点是,在内循环里,当right <= left 时,意味着right 与 left 指针已经指向同一个值 或者 已经有过交集了,此时就可以结束循环了。
其中依旧要考虑去重问题。i 的去重和剪枝和上一个方法一样。
而 left 和 right 的去重则是:当left或rigt 的下一个值与 left 或rigt 当前的值相同,则left ++ 或 right --。在left 和 right 的去重判断中,可能会有两个以上相同的值,所以这个判断不能用 if ,要用 while,并且因为内循环的循环条件是 right > left, 在这个while循环里也要遵循 right > left 的规则。
还需注意的地方是,left 和 right 的去重,要在当前 i、left、right 的值已经形成一个元组加入结果集的情况下进行,如果在元组加入结果集之前进行判断,则会错过这个元组。
function threeSum($nums) {
$result = [];
sort($nums);
for($i = 0; $i < count($nums); $i++) {
if($nums[$i] > 0) {
break;
}
if($i > 0 && $nums[$i] == $nums[$i - 1]) {
continue;
}
$left = $i + 1;
$right = count($nums) - 1;
while($right > $left) {
if($nums[$i] + $nums[$left] + $nums[$right] > 0) {
$right--;
} else if($nums[$i] + $nums[$left] + $nums[$right] < 0) {
$left++;
} else {
$result[] = [$nums[$i], $nums[$left], $nums[$right]];
while ($right > $left && $nums[$right] == $nums[$right - 1]) {
$right--;
}
while ($right > $left && $nums[$left] == $nums[$left + 1]) {
$left++;
}
$right--;
$left++;
}
}
}
return $result;
}
四、力扣题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]]
提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
本题的逻辑和三数之和的逻辑差不多,就是在三数之和的基础上再套了一层for循环。需要注意的是,本题的四数之和不是特定的0,而是变化的值,所以 i 剪枝时,判断条件就要改变一下,
除了 nums[ i ] > target 的判断之外,还要加上 nums[ i ] > 0 的情况。若只判断 i 对应的值是否大于target,那么当 i 和 target 皆为负数,如 -3 > -9 ,是有可能存在 [ -3,-3,-3,0]这种符合条件的元组的。
在这里做一下三数之和中哈希法和双指针法中的去重、剪枝操作的对比:
可以看到,哈希法和双指针法在内循环时的去重和剪枝操作是不同的,哈希法相对于双指针法,剪枝判断会更复杂。还有一个点就是注意哈希法中 hash 置空的位置。
在三数之和的基础上加一层for循环,就要注意剪枝和hash置空的位置。
方法一:哈希法
function fourSum($nums, $target) {
$result = [];
sort($nums);
for($i = 0; $i < count($nums); $i++) {
if($nums[$i] > $target && $nums[$i] >= 0) {
break;
}
if($i > 0 && $nums[$i] == $nums[$i - 1]) {
continue;
}
for($j = $i + 1; $j < count($nums); $j++) {
if($nums[$j] + $nums[$i] > $target && $nums[$j] + $nums[$i] >= 0) {
break;
}
if($j > $i + 1 && $nums[$j] == $nums[$j - 1]) {
continue;
}
$hash = [];
for($k = $j + 1; $k < count($nums); $k++ ) {
if($k > $j + 2 && $nums[$k] == $nums[$k - 1] && $nums[$k - 1] == $nums[$k - 2]) {
continue;
}
$four = $target - $nums[$i] - $nums[$j] - $nums[$k];
if(isset($hash[$four])) {
$result[] = [$nums[$i], $nums[$j], $nums[$k], $four];
unset($hash[$four]);
} else {
$hash[$nums[$k]] = 1;
}
}
}
}
return $result;
}
方法二:双指针法
function fourSum($nums, $target) {
$result = [];
sort($nums);
for($i = 0; $i < count($nums); $i++) {
if($nums[$i] > $target && $nums[$i] >= 0) {
break;
}
if($i > 0 && $nums[$i] == $nums[$i - 1]) {
continue;
}
for($j = $i + 1; $j < count($nums); $j++) {
if($nums[$j] + $nums[$i] > $target && $nums[$j] + $nums[$i] >= 0) {
break;
}
if($j > $i + 1 && $nums[$j] == $nums[$j - 1]) {
continue;
}
$left = $j + 1;
$right = count($nums) - 1;
while($right > $left) {
if($nums[$i] + $nums[$j] + $nums[$left] + $nums[$right] > $target) {
$right--;
} else if($nums[$i] + $nums[$j] + $nums[$left] + $nums[$right] < $target) {
$left++;
} else {
$result[] = [$nums[$i], $nums[$j], $nums[$left], $nums[$right]];
while ($right > $left && $nums[$right] == $nums[$right - 1]) {
$right--;
}
while ($right > $left && $nums[$left] == $nums[$left + 1]) {
$left++;
}
$right--;
$left++;
}
}
}
}
return $result;
}