算法训练day5 | php | 454.四数相加II , 383. 赎金信 , 15. 三数之和 , 18. 四数之和

一、力扣题454. 四数相加 II

给你四个整数数组 nums1nums2nums3 和 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 != ji != 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
  • abc 和 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;
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值