两数之和、三数之和、四数之和、K数之和

两数之和、三数之和、四数之和和K数之和是最近听室友提起的几道有意思的基础题,可以说是把双指针运用的淋漓尽致。(K数之和其实是一个动态规划的题,此处因为满足*数之和的的结构,放在一起对比提一下)。

1、两数之和等于Target的下标

LintCodehttps://www.lintcode.com/problem/two-sum/

题目描述:给一个整数数组,找到两个数使得他们的和等于一个给定的数 target。假设只有一组答案。

你需要实现的函数twoSum需要返回这两个数的下标, 并且第一个下标小于第二个下标。注意这里下标的范围是 0 到 n-1。

简析:此处需要返回的是数字所在位置的下标,不是判断是否存在。

第一种思路借助hashMap,key为数组中的值,value为其下标。遍历一次就可以找出目标答案,空间复杂度为O(n),时间复杂度为O(n);

第二种思路构造Pair类然后进行排序,排序之后通过头尾双指针找到目标答案,时间复杂度为O(nlogn),空间复杂度为O(n);

代码:

    //方案一:自定义pair类,排序之后使用双指针 时间复杂度o(nlogn),空间复杂度o(1)
    // 并实现comparable接口-compare方法重写(当然也可以使用匿名内部类,继承comparator类,compare方法重写)
    
public class Solution {

    class Pair implements Comparable<Pair>{
        public int key;
        public int value;

        Pair(int key, int value){
            this.key = key;
            this.value = value;
        }

        @Override
        public int compareTo(Pair o1){
            if(o1.value > this.value)
                return -1;
            else if( o1.value == this.value)
                return 0;
            else
                return 1;
         }
    }  //end of Pair Class
    
    public int[] twoSum(int[] numbers, int target) {
        // write your code here
        int[] res = new int[2];
        if(numbers == null || numbers.length <= 1)
            return res;
        Pair[] pairs = new Pair[numbers.length];
        for(int i = 0; i < numbers.length; i ++){
            Pair pair = new Pair(i,numbers[i]);
            pairs[i] = pair;
        }
        Arrays.sort(pairs);
        int left = 0, right = numbers.length - 1;
        while(left < right){
            if(pairs[left].value + pairs[right].value == target){
                res[0] = Math.min(pairs[left].key, pairs[right].key);
                res[1] = Math.max(pairs[left].key, pairs[right].key);
                left ++;
                right --;
            }else if(pairs[left].value + pairs[right].value > target){
                right --;
            }else
            left ++;
        }
        return res;
    }
     //方法二:使用hashmap,此时不需要排序。时间复杂度o(n),空间复杂度o(n)
    //hashmap中存储键为数值,值为对应的数组下标
    
    public int[] twoSum2(int[] numbers, int target){
        if(numbers == null || numbers.length <= 1)
            return null;
        int[] res = new int[2];
        HashMap<Integer, Integer> map = new HashMap<>();  //key-numbers[i]  value- i
        for(int i = 0; i < numbers.length; i ++){
            //首先判断当前元素与前面的元素是否可以组成target
            if(map.keySet().contains(target - numbers[i])){
                res[1] = i;
                res[0] = map.get(target - numbers[i]); //另一个数的下标序号
                return res; //只有一组结果,此处直接返回
            }
            map.put(numbers[i],i); //将当前元素加入到HashMap
        }
        return res;
    }

2、三数之和等于0的组合排列

LintCode
题目描述:给出一个有n个整数的数组S,在S中找到三个整数a, b, c,找到所有使得a + b + c = 0的三元组。在三元组(a, b, c),要求a <= b <= c。结果不能包含重复的三元组。

简析:此题目target是固定等于0,那么要求的a、b、c中一定是正负相加或者全0。我们可以枚举首位a,然后用双指针遍历找出和为target - a的俩数。注意,要使用双指针必须首先保证有序性。

注意此题目要求输出的组合中不能有重复,所以要去重。去重的方法无非就是:枚举首位元素a的时候,保证不重复。当找到某组组合之后,第二位的元素和第三位的元素都需要去重。在有序数组中,该去重操作就是就是移到到下一位不想等的元素上。a元素如果发现已经被枚举过了,则将a元素指针i进行i ++b元素如果之后重复是相等的,则不断后移,直到移到后面第一个不是该元素的位置上。实现起来就是while(numbers[left] == numbers[left + 1]) left ++,完成while循环之后,再加一个left ++

代码:

public class Solution {
    /**
     * @param numbers: Give an array numbers of n integer
     * @return: Find all unique triplets in the array which gives the sum of zero.
     */
    public List<List<Integer>> threeSum(int[] numbers) {
        // write your code here
        
        //边界判断
        if(numbers == null || numbers.length == 0)
            return null;
        
        //返回的二维list
        List<List<Integer>> res = new ArrayList<>();
        
        //数组排序
        Arrays.sort(numbers);
        
        //枚举首位元素a + 双指针遍历
        for(int i = 0; i < numbers.length - 2 && numbers[i] <= 0; i ++){
            
            //首位元素去重
            while(i >= 1 && i < numbers.length - 2 && numbers[i] == numbers[i - 1])
                i ++;
            int left = i + 1;
            int right = numbers.length - 1;
            //left < right 同时约束了指针的范围,避免了 i++ 越界
            while(left < right){ 
                if(numbers[i] + numbers[left] + numbers[right] == 0){
                    List<Integer> tmp =  new ArrayList<>();
                    tmp.add(numbers[i]);
                    tmp.add(numbers[left]);
                    tmp.add(numbers[right]);
                    res.add(tmp);
                    //第二位元素去重
                    while(left + 1 < right && numbers[left] == numbers[left + 1])
                        left ++;
                    //第三位元素去重
                    while(left < right - 1 && numbers[right] == numbers[right - 1])
                        right --;
                     //注意!!去重结束之后仍要同时移动left和right指针
                     //保证指针移动到了第一个不是重复元素的位置上
                    left ++; right --;    
                }else if(numbers[i] + numbers[left] + numbers[right] > 0)
                    right --;
                else
                    left ++;    
            }
        }
        
        return res;
    }
}

3、三数之和小于Target的组合总数目

LintCode: https://www.lintcode.com/problem/3sum-smaller/description

题目描述:给定一个n个整数的数组和一个目标整数target,找到下标为i、j、k的数组元素0 <= i < j < k < n,满足条件nums[i] + nums[j] + nums[k] < target。输出满足上述条件的组合的总数。

简析:需要找和为target的三个数,可以首先确定一个数A,然后找和为target-A的两个数。

(错误思路:“从数学角度来看,三个数和为target,一定至少有1个小于等于target的数。所以可以A数的范围定在<= target的范围进行枚举,剩余两数则在大于A的范围内进行双指针遍历”。 这种想法非常非常错误的,比如target= (-4) = (-2) + (-2) + 0,三个数都比target要大。当target为0和正数的时候,刚才的说法才是正确的。)

通过剖析上面的错误思路,对于首位数字的枚举,不能仅仅只枚举小于等于target的数,应该遍历到数组中所有的数,也就是从[0, length -3],最后两位要留给剩余两个数字。

代码

public class Solution {
    /**
     * @param nums:  an array of n integers
     * @param target: a target
     * @return: the number of index triplets satisfy the condition nums[i] + nums[j] + nums[k] < target
     */
    public int threeSumSmaller(int[] nums, int target) {
        // Write your code here
        int cnt = 0;
        if(nums == null || nums.length == 0)
            return cnt;
        //数组排序后方可使用双指针
        Arrays.sort(nums);

        //枚举首位数字
        for(int i = 0; i <= nums.length - 3; i ++){
            int left = i + 1;
            int right = nums.length - 1;
            //确定首位数字后,双指针遍历
            while(left < right){
               //当和小于target时:保持当前left不变,right指针从此处前移到left+1,所有的和都将小于target
               //所以此时满足条件的情况的数目等于 right - (left + 1) + 1 = right - left
                if(nums[left] + nums[right] < target - nums[i]){ 
                    cnt += right - left;  
                    left ++;
                }else
                    right --;
            }
        }
        return cnt;
    }
}

4、四叔之和等于Target的组合

LintCode:https://www.lintcode.com/problem/4sum/description

题目描述:给一个包含n个数的整数数组S,在S中找到所有使得和为给定整数target的四元组(a, b, c, d)。四元组(a, b, c, d)中,需要满足a <= b <= c <= d。答案中不可以包含重复的四元组。

简析:四数之和在三数之和的基础上又多了一个数字,仍旧使用双指针,此时需要枚举前两个元素ab,双指针寻找cd,是的c + d = target - a - b。在有序数组中的去重仍旧采用移动指针的方法,直到移动到第一个不相等的位置。

具体来说,在处理的时候要注意两个问题:1.数组越界 2.去重遍历的范围。

例如对第一个元素来说,i本身的范围是[0, numbers.length - 4],但是当他处理0位置时候,不需要和前面的元素判重。判重的范围是[1, numbers.length - 4],例如第1位置的数值与第0位置的数值相等,则需要跳过第1位置。

针对第二个元素,j本身的范围是[i + 1, numbers.length -3]。和第一个元素一样,他判重的范围是包含i + 1的。从i + 2位置开始,如果和前面元素相等,则需要跳过。

针对第三个和第四个元素而言,当出现满足要求的组合时候,就要进行去重,分别把left和right指针移动到不等于当前指针的第一个位置。移动指针过程中非常容易出现数组越界,在while(left < right)的循环中,要保证left指针left ++后都要小于rightright指针保证right -- 要大于left

代码:

public class Solution {
    /**
     * @param numbers: Give an array
     * @param target: An integer
     * @return: Find all unique quadruplets in the array which gives the sum of zero
     */
    public List<List<Integer>> fourSum(int[] numbers, int target) {
        // write your code here
        List<List<Integer>> res = new ArrayList<>();
        if(numbers == null || numbers.length == 0)
            return res;
        
        Arrays.sort(numbers);
        //a元素遍历
        for(int i = 0; i < numbers.length - 3; i ++){
            
            //a元素去重
            while(i > 0 && i <  numbers.length - 3 && numbers[i] == numbers[i - 1])
                i ++;
            //b元素遍历
            for(int j = i + 1; j < numbers.length - 2; j ++){
                
                //b元素去重
                while(j > i + 1 &&  j < numbers.length - 2 && numbers[j] == numbers[j - 1])
                    j ++;
                    
                //双指针
                int left = j + 1;
                int right  = numbers.length - 1;
                while(left < right){
                    if(numbers[left] + numbers[right] + numbers[i] + numbers[j] == target){
                        List<Integer> tmp = new ArrayList<>();
                        tmp.add(numbers[i]); 
                        tmp.add(numbers[j]); 
                        tmp.add(numbers[left]); 
                        tmp.add(numbers[right]);
                        res.add(tmp);
                        //第三个元素去重
                        while(left + 1 < right && numbers[left + 1] == numbers[left])
                            left ++;
                         //第四个元素去重   
                        while(left < right - 1 && numbers[right] == numbers[right - 1])
                            right --;
                        left ++; right --;    
                    }else if(numbers[left] + numbers[right] + numbers[i] + numbers[j] < target)
                        left ++;
                    else
                        right --;
                }
            }
        }        
        return res;
    }
}

5、K数之和

此题目与上面的题目都是同一类型的,前面的题都是双指针的。此题目虽然是k数之和,看起来像三数之和、四数之和的拓展。但其实是动态规划的问题。

LintCode: https://www.lintcode.com/problem/k-sum/

题目描述:给定 n 个不同的正整数,整数 k(k <= n)以及一个目标数字 target。 
在这 n 个数里面找出 k 个数,使得这 k 个数的和等于目标数字,求问有多少种方案?
输入:
List = [1,2,3,4] ; k = 2 ; target = 5
输出: 2
说明: 1 + 4 = 2 + 3 = 5

简析:设状态为f[i][j][p],表示前i个数中找出j个数且和等于p的方案数目。

那么状态转移方程: f[i][j][p] = f[i - 1][j][p] + f[i - 1][j - 1][p -num[i]]

最终返回f[n][k][target]即可

代码

public class Solution {
    /**
     * @param A: An integer array
     * @param k: A positive integer (k <= length(A))
     * @param target: An integer
     * @return: An integer
     */
    public int kSum(int[] A, int k, int target) {
        // write your code here
        if(A == null || A.length ==0 || k <= 0)
            return 0;
        //正整数的总数从0 - A.length; 可选的个数从 0 - k; target也是从 0 - target
        int[][][] f = new int[A.length + 1][ k + 1][target + 1];
        //初始值
        for(int i = 0; i <= A.length; i ++){
            f[i][0][0] = 1;
        }
        //f[i][j][t] = f[i - 1][j][t] + f[i - 1][j - 1][t -A[i]]
        for(int i = 1; i <= A.length; i ++)
            for(int j = 1; j <= k; j ++)
                for(int t = 1; t <= target; t ++){
                    f[i][j][t] = f[i - 1][j][t];
                    f[i][j][t] += t - A[i - 1] >=0 ? f[i - 1][j - 1][t -A[i - 1]] : 0;
                }
        return f[A.length][k][target];
    }
}
<think>嗯,用户问的是用C++的哈希表来实现三数之和的问题。首先,我得回忆一下三数之和的常见解法。通常有两种方法:双指针和哈希表。双指针法需要对组排序,然后固定一个,用左右指针向中间逼近。哈希表的方法可能也是先排序,然后固定两个,用哈希表来找第三个,这样可以减少一层循环,时间复杂度可能从O(n³)降到O(n²)。不过需要注意去重的问题。 首先,我需要确定如何用哈希表来优化。比如,对于每个元素nums[i],遍历后面的元素nums[j],然后检查哈希表中是否存在需要的那个,即0 - nums[i] - nums[j]。不过这样的话,可能需要处理重复的情况。因为直接使用哈希表可能会记录之前的所有元素,导致重复的三元组被加入。所以需要先对组排序,这样在遍历时跳过重复的元素,避免重复解。 然后,具体步骤可能是这样的:先对组进行排序,然后外层循环固定第一个nums[i],内层循环固定第二个nums[j],然后在哈希表中查找是否存在第三个target = -nums[i]-nums[j],其中target应该在j+1到组末尾的范围内?或者可能哈希表存储的是已经处理过的元素,这样在找的时候可以避免重复? 或者,可能的实现方式是,外层循环遍历第一个nums[i],然后对于每个i,内层循环从i+1开始遍历j,同时维护一个哈希表,存储已经遍历过的元素,这样对于每个j,检查哈希表中是否存在target = -nums[i]-nums[j]。如果存在,就组成一个三元组。然后,为了去重,因为组已经排序过了,所以在i和j的遍历过程中,遇到相同的元素就跳过。比如,当nums[i] == nums[i-1]时,跳过i,同理j也是。 这样的话,哈希表的实现方式大致是:排序组,遍历i从0到n-3,跳过重复的i。然后对于每个i,遍历j从i+1到n-1,在j的循环中,计算需要的target = -nums[i]-nums[j],然后检查这个target是否在哈希表中。如果在的话,就添加三元组。同时,在j的循环中,也需要处理重复的情况,比如在找到一个有效的三元组之后,跳过所有与nums[j]相同的元素。 不过,可能这样的方法的时间复杂度是O(n²),因为外层循环i是O(n),内层循环j也是O(n),而哈希表的查找是O(1)。但是,这样的方法需要处理重复的情况,因此需要在每次找到解之后,或者在遍历时跳过重复的j。或者可能还有其他方法? 或者,可能的实现方式是将第三个的查找通过哈希表来完成,而不用双指针。比如,固定i,然后在i+1到末尾的区间内,用哈希表来记录每个的补。这可能和两数之和的解法类似。例如,对于每个i,遍历j从i+1到n-1,然后对于每个j,计算需要的第三个是target = -nums[i]-nums[j],如果这个存在于之前建立的哈希表中,那么就可以得到一个解。但这里可能需要注意第三个是否在j的后面或者前面,否则可能会有重复或者遗漏的情况。 或者,哈希表的建立可能需要动态维护,比如在j的循环中,将元素逐个添加到哈希表中,然后对于每个j,查找target是否已经在哈希表中存在。比如,当处理到j的时候,哈希表中存储的是从i+1到j-1的元素,这样当计算target时,如果在哈希表中存在,说明在之前的元素中有这个,从而组成三元组。这可能不太正确,因为可能需要第三个在j之后的位置? 或者可能哈希表应该存储的是所有可能的,但是在这种情况下,可能会出现同一个元素多次使用的问题。例如,如果组中有重复的元素,可能会导致同一个位置的元素被多次使用,从而产生重复的三元组或者错误的结果。 因此,正确的哈希表方法可能需要结合排序和跳过重复元素,同时在遍历时动态管理哈希表。比如,对组排序后,外层循环i遍历每个元素,跳过重复的i。然后内层循环j从i+1开始,遍历每个元素,并在过程中维护一个哈希集合,存储已经处理过的j元素。这样,对于每个j,计算target = -nums[i]-nums[j],然后检查这个target是否存在于哈希集合中。如果存在,就将这三个作为结果之一。同时,在找到结果后,需要跳过后续与nums[j]相同的元素,避免重复。 这样,每次处理j时,哈希集合存储的是i+1到j-1之间的元素,这样当处理到j时,查找target是否存在,如果存在,则说明在之前的元素中有满足条件的,这样就能组成一个三元组。此时,将这三个加入结果。然后,为了避免重复,需要在处理完当前j后,跳过所有与nums[j]相同的元素,直到下一个不同的元素。 不过,这样的方法是否能正确覆盖所有可能的情况呢?例如,假设组中有多个相同的元素,但不同的位置组合可能形成不同的三元组。但由于组已经排序,所以相同的元素会被排列在一起,所以跳过重复的j可以避免生成重复的三元组。 所以,具体的步骤应该是: 1. 对组进行排序。 2. 遍历组中的每个元素作为第一个nums[i],从0到n-3的位置。 3. 对于每个nums[i],如果i>0且nums[i] == nums[i-1],跳过,避免重复。 4. 初始化一个哈希集合,用于存储第二个的候选。 5. 遍历第二个nums[j],从i+1到n-1的位置。 6. 计算需要的第三个target = -nums[i]-nums[j]。 7. 检查target是否存在于哈希集合中: a. 如果存在,将三元组加入结果。 b. 然后需要跳过所有与nums[j]相同的元素,直到下一个不同的元素,以避免重复。 8. 将当前的nums[j]加入哈希集合中,以便后续的j元素使用。 这样,每次处理j的时候,哈希集合中存储的是i+1到j-1之间的元素。当处理到j时,检查是否存在target,这样可以确保三个的顺序是i < j < k(其中k是之前处理过的某个元素的位置?或者k可能在j之后?这里可能存在逻辑错误,因为哈希集合存储的是i+1到j-1的元素,此时处理到j的时候,查找的target是否可能位于j之后的位置呢?) 哦,这里可能存在错误。例如,假设现在处理到j的位置,哈希集合中存储的是i+1到j-1的元素。所以当计算target时,如果target存在于哈希集合中,那么它的位置k必须是在i+1到j-1之间,即k < j。这样,得到的三元组顺序是i < k < j,这显然不符合要求,因为k应该在j之后的位置,因为j是第二个,第三个应该在剩下的部分。这显然会导致遗漏正确的三元组。 这说明这种方法是错误的,因为第三个需要在j之后的某个位置。所以原来的思路有问题。因此,可能需要调整哈希表的使用方式。 或者,可能应该在内层循环中,哈希表存储的是j之后的元素?或者,可能应该预先处理所有可能的元素? 这时候可能需要另一种方法。例如,对于每个i,我们从i+1到末尾的部分,使用两数之和的哈希表方法。比如,对于每个i,在i+1到末尾的区间内,寻找两个,使得它们的和等于 -nums[i]。这可以使用两数之和的哈希表方法,即遍历j,然后检查哈希表中是否存在所需的补。这样,第三个k的位置可能在j之后吗? 或者,具体来说,当处理每个i时,我们需要在j的范围是i+1到n-1,然后在其中寻找两个nums[j]和nums[k],使得它们的和是 -nums[i]。此时,可以用两数之和的哈希表方法,但需要确保k > j。例如,在遍历j时,我们维护一个哈希表,保存已经处理过的元素。对于当前的j,如果哈希表中存在target = -nums[i] - nums[j],那么说明在之前的某个位置存在一个,使得三者之和为0。但这样的话,得到的k的位置是小于j的,这样顺序是i < k < j,这可能不符合要求,因为通常三数之和的问题要求i < j < k。所以这种方法可能无法正确找到所有的三元组,或者导致重复。 这时候,可能正确的哈希表方法应该是在固定i的情况下,在j的循环中,从i+1到末尾,将每个元素加入哈希表,然后对于每个j,检查是否存在target = -nums[i] - nums[j]。如果存在,并且这个target的位置在j之后的话,那么就可以组成三元组。但是,哈希表只能记录是否存在这个值,而无法记录其位置,所以可能导致错误。例如,可能存在target的值在j之后的位置,但哈希表无法保证这一点。 这说明,使用哈希表的方法可能无法有效处理元素的位置问题,因此可能导致错误的结果。这时候可能哈希表方法并不是最优的,或者需要其他处理方式。例如,在排序后的组中,一旦i和j被固定,那么第三个值必须是特定的,并且应该在j的右侧。因此,在哈希表中存储j右侧的所有值,然后进行查找。 例如,对于每个i,遍历j从i+1到末尾,同时维护一个哈希集合,其中存储j+1到末尾的所有元素。这样,当处理到j时,哈希集合中的元素都是j+1之后的元素,此时查找是否有target = -nums[i]-nums[j]。这样,可以得到k的位置在j之后。但这样的话,哈希集合需要在每次j变化时更新,这会导致时间复杂度增加,因为每次j移动时,哈希集合需要排除j之前的元素,这可能需要O(n²)的时间复杂度。 或者,可以预先将整个组存入哈希表,然后在处理i和j时,检查是否存在target,并且target的位置必须大于j。但哈希表无法直接记录位置信息,所以需要额外的处理。例如,使用unordered_multimap来存储值和对应的索引,然后当找到target时,确保其索引大于j。但这样可能会比较复杂,因为可能存在多个相同值的元素,需要逐个检查它们的索引是否大于j。 这似乎不太高效,尤其是在存在大量重复元素的情况下。所以,可能哈希表的方法在解决三数之和的问题时,虽然理论上可以达到O(n²)的时间复杂度,但实现起来较为复杂,容易出错,尤其是处理重复元素和保证元素顺序的问题。 因此,正确的哈希表实现可能需要以下步骤: 1. 对组进行排序。 2. 遍历每个i,从0到n-3的位置。 3. 对于每个i,跳过重复的i。 4. 对于每个i,初始化一个哈希集合。 5. 遍历j,从i+1到n-1的位置。 6. 计算target = -nums[i] - nums[j]。 7. 检查target是否存在于哈希集合中: a. 如果存在,则添加三元组。 b. 同时,需要跳过后续的重复的j,以避免重复的三元组。 8. 将nums[j]加入哈希集合中。 这样,每次处理j时,哈希集合存储的是i+1到j-1之间的元素。当处理到j时,检查是否存在target,如果存在,那么说明在i+1到j-1之间有一个元素等于target,此时三个的索引是i < k < j,这样顺序是否符合要求? 不,因为三数之和通常要求三个的索引是i < j < k,所以这里如果k的位置是k < j的话,则顺序会被破坏。所以,这种方法得到的三元组的顺序是错误的,导致遗漏正确的三元组。 这说明,这样的哈希表方法无法正确找到符合顺序要求的三元组,导致结果错误。例如,假设组是[-1, 0, 1, 2, -1],排序后是[-1, -1, 0, 1, 2]。当i=0(nums[i]=-1),j=1(nums[j]=-1),计算target=2。此时哈希集合中还没有元素,所以无法找到。当j=2(nums[j]=0),target=1。哈希集合中有-1(当j=1时加入的),所以无法找到。当j=3(nums[j]=1),target=0。此时哈希集合中有-1,0,所以存在,得到三元组[-1,1,0],但是顺序是i=0, j=3, k=2(因为哈希集合中的0是当j=2时加入的,此时k=2 < j=3),这导致三元组的顺序是i < k < j,这不符合要求,并且原组中可能存在这样的组合,但排序后的组顺序会导致这种组合被忽略或者重复。 所以,这样的哈希表方法无法正确找到所有符合条件的三元组,或者会生成重复的三元组。因此,正确的实现方式可能需要双指针的方法,而哈希表方法可能难以处理这种情况。 但用户的问题是希望用哈希表来实现,所以必须找到正确的方法。这时候可能需要重新思考哈希表的使用方式。 另一种思路是,在固定i的情况下,遍历j从i+1到n-1,然后在j的右侧(即从j+1到n-1)使用哈希表来查找是否存在target。例如,在每次处理i和j的时候,哈希表预先存储j+1到n-1的所有元素,这样查找target时,可以确保k在j的右侧。但每次构建这样的哈希表需要O(n)的时间,这会导致整体时间复杂度为O(n³),这显然不如双指针的O(n²)方法。 或者,可以将整个组存入一个哈希表,然后在处理i和j的时候,计算target,并检查target是否存在于哈希表中,并且其索引大于j。这时候,可以通过哈希表存储元素到它们的索引列表的映射,比如用unordered_map<int, vector<int>>,这样对于每个target,可以遍历其所有索引,找到是否有索引大于j的。但这在存在大量重复元素的情况下,会导致较高的时间复杂度,例如,如果组全为0,那么每个查询可能需要遍历大量的索引。 这种情况下,这样的方法可能无法通过时间限制,尤其是在处理较大的输入时。 综上所述,可能使用哈希表的方法虽然可行,但需要更多的处理步骤,并且需要注意去重和元素顺序的问题。正确的实现步骤可能如下: 1. 对组进行排序。 2. 遍历i从0到n-3: a. 如果i>0且nums[i] == nums[i-1],跳过。 3. 遍历j从i+1到n-1: a. 如果j>i+1且nums[j] == nums[j-1],跳过。 b. 计算target = -nums[i] - nums[j]。 c. 在哈希表中查找是否存在target,并且该target的索引必须大于j。 d. 如果存在,则添加三元组。 4. 哈希表需要存储元素及其最后一次出现的索引,这样当处理j时,可以检查是否存在target,并且其索引大于j。 但这需要哈希表能够快速找到是否存在这样的元素。例如,可以预先遍历组,记录每个元素的最后出现位置。这样,在处理i和j时,当计算target,检查该元素是否存在于哈希表中,并且其最后出现的索引是否大于j。如果是,则存在有效的k。 这种方法的具体实现可能如下: - 预先构建一个哈希表,存储每个值的最后出现索引。 - 对于每个i,遍历j从i+1到n-1: - 如果j>i+1且nums[j]==nums[j-1],跳过。 - 计算target = -nums[i]-nums[j]. - 检查哈希表中是否存在target,且该target的last_index > j。 - 如果存在,将nums[i], nums[j], target加入结果。 这样,由于组已经排序,所以相同的值的最后一个索引肯定是最大的。例如,当处理到j时,如果target存在,并且其最后出现的索引>j,那么肯定存在一个k>j,使得nums[k]=target。因此,可以保证i<j<k的顺序。 同时,需要处理重复的三元组。例如,当i和j固定后,如果多个相同的j值被处理,可能会导致相同的三元组被多次添加。因此,在j的循环中,如果当前nums[j]与前一个相同,则跳过。 例如,当组排序后为[-1, -1, 0, 1, 1],当i=0,j=1时,nums[j]=-1,这时候计算target=2,不存在。当j=2时,nums[j]=0,target=1,哈希表中最后一个1的索引是4,大于j=2,所以添加三元组[-1,0,1]。当j=3时,nums[j]=1,此时target=0,哈希表中0的最后索引是2,不大于j=3,所以不添加。而j=4时,nums[j]=1,但i=0,j=4,此时target=0,但哈希表中0的最后索引是2,所以无法满足。 这样,可以正确找到所有可能的三元组。同时,由于排序后,重复的值会被跳过,避免重复的三元组。 但这种方法是否覆盖所有情况呢?例如,假设组中有多个相同的target值,但它们的索引大于j。例如,组是[-1,0,1,1,1]。当i=0,j=1时,target=1,哈希表中最后出现的1的索引是4,大于j=1,所以添加三元组[-1,0,1]。之后,当j=2时,nums[j]=1,此时target=0,哈希表中最后出现的0的索引是1,不大于j=2,所以不添加。这样,避免了重复添加相同的三元组。 不过,这种方法可能导致某些情况被遗漏吗?例如,组是[-2,1,1,1,1]. 当i=0,j=1时,target=1。哈希表中最后出现的1的索引是4,大于j=1,所以添加三元组[-2,1,1]。但这时候,可能还有其他有效的三元组,比如当j=2时,同样的情况。但此时,因为nums[j]=1与前一个j=1时的nums[j]=1相同,会被跳过吗? 在步骤3a中,如果j>i+1且nums[j] == nums[j-1],则跳过。所以当i=0,j=1时处理,当j=2时,由于nums[j]==nums[j-1](i+1=1,j=2>i+1),所以会被跳过。这样,三元组[-2,1,1]只被添加一次,避免了重复。 这样,这种方法是可行的,并且能够正确处理重复的情况。 综上,实现步骤为: 1. 排序组。 2. 构建哈希表,记录每个值最后出现的索引。 3. 遍历i从0到n-3: a. 跳过重复的i。 4. 遍历j从i+1到n-1: a. 跳过重复的j。 b. 计算target。 c. 检查target是否存在,且最后出现的索引>j。 d. 如果存在,添加三元组。 这样,这种方法的时间复杂度是O(n²),因为每个i和j的遍历是O(n²),哈希表的查询是O(1)。同时,排序的时间是O(n log n)。 现在,编写代码的话,需要注意: - 如何构建哈希表记录每个的最后出现的索引? 可以遍历组一次,对于每个元素,更新其在哈希表中的值为当前索引。这样,最后哈希表中每个的值是对应的最大索引。 - 如何处理重复的i和j? 在i的循环中,当i>0且nums[i] == nums[i-1]时,跳过。 在j的循环中,当j>i+1且nums[j] == nums[j-1]时,跳过。 代码示例: #include <vector> #include <algorithm> #include <unordered_map> using namespace std; vector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> res; sort(nums.begin(), nums.end()); unordered_map<int, int> last_occurrence; for (int i = 0; i < nums.size(); ++i) { last_occurrence[nums[i]] = i; // 记录每个的最后出现位置 } int n = nums.size(); for (int i = 0; i < n - 2; ++i) { if (i > 0 && nums[i] == nums[i-1]) continue; // 跳过重复的i for (int j = i + 1; j < n - 1; ++j) { if (j > i + 1 && nums[j] == nums[j-1]) continue; // 跳过重复的j int target = -nums[i] - nums[j]; if (last_occurrence.find(target) != last_occurrence.end() && last_occurrence[target] > j) { res.push_back({nums[i], nums[j], target}); } } } return res; } 但这样代码会有问题吗?例如,当nums[i] + nums[j] + target =0,且target存在于哈希表中,并且其索引大于j。这样确实成立。但是,假设组中有多个相同的target,比如多个相同的值,这时候哈希表中记录的是最后一个出现的索引,所以只要存在一个索引大于j的target,就能组成一个三元组。但这样是否会导致重复的三元组? 比如,假设组是[-1, -1, 2]. 排序后为[-1, -1, 2]. i=0,j=1。计算target=0,此时哈希表中0不存在,所以不添加。然后i=0,j=2?但n-1是2,所以j的范围是到n-1,所以j只能到1?或者我的循环条件是否有误? 原代码中的j循环是for (j = i+1; j < n-1; ++j). 这里n是组长度,当组长度是3时,n-1=2,所以j可以取到1和2吗?不,因为j < n-1的条件是j < 2,所以当n=3时,j只能取i+1=1,如果i=0的话。那这样,当组是[-1,-1,2],i=0,j=1,target=0,不存在,所以不添加。但正确的三元组是[-1,-1,2]. 所以这里代码无法正确找到这个解。 这说明代码中存在错误。问题出在循环的条件上。例如,在三数之和中,j应该遍历到n-1的位置,因为当i=0,j=1,k=2时,此时j的范围应该是i+1到n-2?或者原代码中的j循环条件不正确。 例如,原代码中的j循环条件是j < n-1,这导致j只能到n-2的位置。这样,当组长度为3时,i=0,j只能到1的位置,而k必须大于j,即k=2。这时候,target = -nums[0]-nums[1] = 2,此时last_occurrence[2] = 2,确实大于j=1。所以条件满足,应该添加三元组。但原代码中的j循环条件是j < n-1,当n=3时,n-1=2,所以j <2,即j只能取到1。这时候,当i=0,j=1,计算target=2,哈希表中存在且索引是2>1,所以应该添加三元组。这样,原代码在这种情况下应该可以正确添加。 那为什么在上述例子中,原代码会得到正确的解? 在例子组[-1,-1,2]中: 排序后组为[-1, -1, 2]. i=0,nums[i]=-1。j从i+1=1开始,到n-1=2吗? 不,原代码中j循环的条件是j < n-1,当n=3时,j <2,所以j只能取1。 此时j=1,nums[j]=-1,计算target = 2. last_occurrence[2] =2,即索引2>1,满足条件。所以三元组[-1, -1, 2]会被添加到结果中。这样代码是正确的。 那为什么在之前的分析中,认为当n=3时,j只能到1,而k=2的位置,此时确实会被处理。这说明原代码的循环条件是正确的? 可能我之前对循环条件的理解有误。例如,j的循环条件应该是j <n-1吗?或者应该是j <=n-2? 例如,在n=3的情况下,i的范围是i <n-2,即i<1,所以i只能取0。j的循环条件是j <n-1即j<2,所以j只能取1。这样,k=2的位置,因为last_occurrence[target]必须>j,所以当j=1时,k=2确实满足条件。所以这种情况下,代码是正确的。 那这说明原代码是正确的?那之前的例子应该没问题。 那么另一个例子,组是[0,0,0,0]. 排序后为四个0。i遍历0到n-3=1。当i=0时,跳过重复吗?i=0时,i>0不成立,所以不跳过。然后j从i+1=1到n-1=3,但循环条件是j <n-1=3,所以j只能到2。对于每个j=1和j=2: 当j=1时,跳过重复吗?j>i+1=1+0+1=1+0+1?原代码中j>i+1的条件是当j大于i+1时才检查重复。例如,当i=0,j=1时,j等于i+1,所以不跳过。此时target=0-0-0=0。检查哈希表中是否存在0,且last_occurrence[0]=3>1,满足条件,所以添加三元组[0,0,0]。当j=2时,j>i+1=1,所以检查nums[j]是否等于nums[j-1],即0=0,所以跳过。这样,最终结果中只有一个三元组,正确。 这说明原代码在正确的情况下处理重复问题。 综上,原代码是正确的。那这似乎是一个可行的哈希表实现三数之和的方法。但需要测试更多的案例。 现在,总结: 哈希表的方法步骤是: 1. 排序组。 2. 建立哈希表记录每个的最后出现位置。 3. 遍历i,跳过重复。 4. 遍历j从i+1开始,跳过重复。 5. 计算target,并检查是否存在且最后出现的位置>j。 6. 添加结果。 这种方法可以正确处理所有情况,并且避免重复的三元组。 但需要注意的是,哈希表记录的是每个最后出现的索引,这样确保当target存在时,至少有一个k>j的位置。因此,该方法是可行的。 因此,给出的代码应该如上述所示,并且能够正确处理三数之和的问题。</think>以下是使用哈希表实现三数之和的C++代码及详细解释: ```cpp #include <vector> #include <algorithm> #include <unordered_map> using namespace std; vector<vector<int>> threeSum(vector<int>& nums) { vector<vector<int>> res; sort(nums.begin(), nums.end()); unordered_map<int, int> last_occurrence; // 记录每个元素最后一次出现的索引 for (int i = 0; i < nums.size(); ++i) last_occurrence[nums[i]] = i; int n = nums.size(); for (int i = 0; i < n - 2; ++i) { // 跳过重复的i if (i > 0 && nums[i] == nums[i - 1]) continue; for (int j = i + 1; j < n - 1; ++j) { // 跳过重复的j if (j > i + 1 && nums[j] == nums[j - 1]) continue; int target = -nums[i] - nums[j]; // 检查是否存在target且其索引在j之后 if (last_occurrence.count(target) && last_occurrence[target] > j) { res.push_back({nums[i], nums[j], target}); } } } return res; } ``` **实现原理**: 1. **排序组**:通过排序解决重复元素问题,并简化查找逻辑 2. **哈希表记录最后出现位置**:预处理时记录每个值最后一次出现的索引 3. **双循环遍历i和j**: - i从0到n-3,j从i+1到n-2 - 通过`nums[i] == nums[i-1]`和`nums[j] == nums[j-1]`跳过重复元素 4. **计算目标值**:`target = -nums[i] - nums[j]` 5. **哈希表验证**:检查target是否存在且其最后出现位置在j之后 **时间复杂度**:O(n²),排序O(nlogn) + 双循环O(n²) **注意事项**: - 必须跳过重复的i和j来避免重复解 - 哈希表需要存储最后出现的索引才能保证k>j - 排序是保证去重和正确索引验证的关键步骤
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值