K-SUM 算法及子问题 2-SUM、3-SUM、4-SUM

2-SUM 问题

Question

​ Given an array of integers, return indices of the two numbers such that they add up to a specific target. You may assume that each input would have exactly one solution, and you may not use the same element twice.

​ 根据给定整数数组,选出相加为给定整数的两个数组元素的位置;假定有且只有一个正解,数组中每个元素只能使用一次;

解法一

​ 根据题目可知,只需要计算数组中两两组合的和等于给定数即可,首先想到的是暴力遍历的方法,对数组遍历两次,两两计算和,找到正解就返回;

public int[] sum2(int[] nums, int target){
    int length = nums.length;
    for (int i = 0; i < length - 1; i++){
        for (int j = i + 1; j < length; j++){
            if (nums[i] + nums[j] == target){
                return new int[]{i, j};
            }
        }
    }
    return null;
}

​ 这种解法思路比较简单,但是时间复杂度是 O(n^2)

解法二

​ 对解法一进一步分析,暴力破解时的两层循环中的内层循环的作用其实就是在寻找外层循环元素的补足;内层循环的时间复杂度是 O(n),如果利用 HashMap 来完成这种查找,那么内层循环的时间复杂度是O(1);

public int[] sum2(int[] nums, int target){
    int length = nums.length;
    Map<Integer, Integer> numsMap = new HashMap<>();
    for (int i = 0; i < length; i++){
        numsMap.put(nums[i], i);
    }
    for (int i = 0; i< length; i++){
        int complement = target - nums[i];
        if (numsMap.containsKey(complement) && numsMap.get(complement) != i){
            return new int[]{i, numsMap.get(complement)};
        }
    }
    return null;
}

​ 在执行遍历之前,先将所有数组元素置入 HashMap,其中键为数组中元素,值为其索引;

​ 对数组元素进行遍历,每次遍历中计算当前元素的补足值,利用 HashMap 的 containsKey 方法查询是否存在,若存在则返回;

解法三

​ 在解法二中,利用 HashMap 实现了将时间复杂度降到了O(n),将两层循环改成了两次循环;在第一次循环完成了 HashMap 的填充,第二次循环实现计算与查找;仔细分析会发现解法一与解法二查找补足值的范围是不同的,解法一只在当前元素位置之后的元素中查找,而解法二中因为事先已经完成了 HashMap 的填充,所以其查找范围是整个数组,这也是为什么还需要做“ numsMap.get(complement) != i ”的判断;

​ 解法二中的查找范围是浪费的,但是因为我们采用的是 HashMap,其查找方式不是遍历的,所以查找范围并不影响性能;但是第一次循环完成 HashMap 的填充是浪费性能的,既然查找补足值的范围不需要全数组,那么可以进一步优化;

public int[] sum2(int[] nums, int target){
    int length = nums.length;
    Map<Integer, Integer> numsMap = new HashMap<>();
    for (int i = 0; i < length; i++) {
        int complement = target - nums[i];
        if (numsMap.containsKey(complement)){
            return new int[]{i, numsMap.get(complement)};
        }
        numsMap.put(nums[i], i);
    }
    return null;
}

​ 去掉了第一次循环完成了 HashMap 的填充的步骤,边遍历边填充;

3-SUM 问题

Question

​ Given an array nums of n integers, are there elements a, b, c in nums such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.The solution set must not contain duplicate triplets.

​ 根据给定的容量为 n 的整数数组,找到所有满足 a + b + c = 0 的三个元素a、b 、c组合,需去重;

解法

​ 修改问题为:满足 a + b + c = target,target是给定数,原题即 target = 0;

​ 根据题目可知,与 2-SUM 问题类似,在整数数组中不放回的取出三个元素,其和等于给定数(0),不同的是,满足条件的解有多个而且需要去重;

​ 首先想到的解法是,遍历数组,然后调用 2-SUM 问题中的方法寻找两个元素相加等于当前元素的补足,最后执行去重操作;这样的话,查询的时间复杂度是O(n^2),空间复杂度是O(n^2),去重的时间复杂度是O(n^2),空间复杂度是O(1),这绝对不能算一个好方案;

​ 思考其他思路,既然要去重,可以先对数组执行一次排序,这样的话在遍历的时候可以跳过相同元素;在确定一个当前元素后,找另外两个元素相加作为当前元素的补足,此时的解可能是多个的,采用首尾标记的方式可以一次遍历完成查找;

public List<List<Integer>> threeSum(int[] nums, int target){
    int length = nums.length;
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(nums);  
    for(int i = 0; i < length - 2; i++) {
        if(nums[i] + nums[i+1] + nums[i+2] > target)break; // too large
        if(nums[i] + nums[length-1] + nums[length-2] < target)continue; // too small
        if(i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = length - 1;
        while(left < right){
            int diff = target - nums[i] - nums[left] - nums[right];
            if (diff == 0){
                result.add(new ArrayList<Integer>(Arrays.asList(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--;
            }else if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    return result;
}
  1. 首先执行 Arrays.sort(nums) 对数组进行一次排序,此处仍有优化空间,但针对的是排序,所以不考虑优化;
  2. 对数组中的元素进行循环,若当前元素与其相邻的后两个元素相加仍大于给定数则结束,若当前元素与末尾两个元素相加仍小于给定数则跳过,若当前元素与上一元素相同则跳过,终点是 length - 2 或 nums[i] > 0,因为总是在当前元素之后选择其补足,而且明显可知解中至少有一个元素小于等于0,既然排序了就要充分利用排序带来的便利;
  3. 标记首尾,首是当前元素之后的第一个元素,尾是数组最后一个元素;
  4. 计算当前元素与首尾两个元素的和与给定数的差值,若差值为 0,则满足解条件,将其加入解集,并且移动首尾标记跳过相同元素以避免重复解;若差值大于 0,则说明三个元素之和小于给定数,右移首标记,使得三个元素之和增大;若差值小于 0,则同理左移尾标记;直至首尾标记重合或交错;

3-SUM Closest 问题

Question

​ Given an array nums of n integers and an integer target, find three integers in nums such that the sum is closest to target. Return the sum of the three integers. You may assume that each input would have exactly one solution.

​ 根据给定的长度为 n 的整数数组,找到三个整数的和最接近给定数,返回三个数的和;假定有且只有一个解;

解法

​ 这个题目是 3-SUM 问题的一个变种,整体思想并没有什么变化,简单改动就可以实现了

public int threeSumCloesest(int[] nums, int target) {
    int length = nums.length;
    int result = Integer.MAX_VALUE;
    Arrays.sort(nums);  
    for(int i = 0; i < length - 2; i++) {
        if(i > 0 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = length - 1;
        while(left < right){
            int diff = target - nums[i] - nums[left] - nums[right];
            if (diff == 0){
                return target;
            }
            if (Math.abs(diff) < Math.abs(result)){
                result = diff;
            }
            if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    return target - result;
}

4-SUM 问题

Question

​ Given an array nums of n integers and an integer target, are there elements a, b, c, and d in nums such that a + b + c + d = target? Find all unique quadruplets in the array which gives the sum of target.

​ 根据给定的容量为 n 的整数数组,找到所有满足 a + b + c + d = 0 的三个元素 abcd组合,需去重;

解法

​ 4-SUM 问题与 3-SUM 问题非常类似,其解法也如出一辙,基本思路是,遍历数组,选取当前元素,然后调用 3-SUM 问题的解法,在当前元素之后的元素范围内找到 3-SUM 问题的解即可;

public List<List<Integer>> threeSum(int[] nums, int target, int from){
    int length = nums.length;
    List<List<Integer>> result = new ArrayList<>();
    for(int i = from + 1; i < length - 2; i++) {
        if(nums[i] + nums[i+1] + nums[i+2] > target)break;
        if(nums[i] + nums[length-1] + nums[length-2] < target)continue;
        if(i > from + 1 && nums[i] == nums[i - 1]) continue;
        int left = i + 1;
        int right = length - 1;
        while(left < right){
            int diff = target - nums[i] - nums[left] - nums[right];
            if (diff == 0){
                result.add(new ArrayList<Integer>(Arrays.asList(nums[from], 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--;
            }else if (diff > 0){
                left++;
            }else {
                right--;
            }
        }
    }
    return result;
}

public List<List<Integer>> fourSum(int[] nums, int target){
    int length = nums.length;
    List<List<Integer>> result = new ArrayList<>();
    Arrays.sort(nums);
    for(int i = 0; i < length - 3; i++) {
        if(nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target)break; // too large
        if(nums[i] + nums[length-1] + nums[length-2] + nums[length-3] < target)continue; // too small
        if(i > 0 && nums[i] == nums[i - 1]) continue;
        int complement = target - nums[i];
        result.addAll(threeSum(nums, complement, i));
    }
    return result;
}

首先对数组进行排序,进行第一层数组遍历,类似 3-SUM问题 中,判断 too large 与 too small 并跳过重复元素;调用 threeSum 函数获取 3-SUM问题的解;

K-SUM 问题

Question

​ 在解决之前的 2-SUM、3-SUM、4-SUM之后,延伸一个问题,如果是一个 极大数-SUM 的问题应该如何解;

解法

​ 根据之前的思路,我们往往是固定一个数,然后计算小范围解,例如 4-SUM 问题中,我们遍历的固定每个元素,然后在其之后的元素范围内解 3-SUM 问题;也就是说对于 极大数-SUM 的问题,可以不停的缩小问题,直至缩小至 2-SUM 问题;

​ 这样的话,可以使用递归的方式来解决问题,那么现在需要抽象公共问题,以及公共问题的最小解;

  1. 公共问题抽象

    公共问题是,在数组中找到 k 个元素相加的和为 target,其中 k、target 均是变量;另外还需要两个变量来表示解的找寻范围;此外我们使用List<Integer> 来保存已经被固定的元素,使用List<List<Integer>> 来保存解;

    那么,公共问题对应的函数就可以创建了

    public void kSum(int[] nums, int k, int target, int from, int end, List<Integer> cur, List<List<Integer>> result){
     ···
    }
    
  2. 公共问题的最小解

    根据之前解决 3-SUM、4-SUM 问题的经验,可以知道 2-SUM 问题是最小解,也就是说当 k == 2 时问题达到最小解

    public void kSum(int[] nums, int k, int target, int from, int end, List<Integer> cur, List<List<Integer>> result){
     if(k == 2) {
         int left = from;
         int right = end;
         while(left < right){
             int diff = target- nums[left] - nums[right];
             if (diff == 0){
                 List<Integer> r = new ArrayList<>();
                 r.add(nums[left]);
                 r.add(nums[right]);
                 r.addAll(cur);
                 result.add(r);
                 while(left < right && nums[left] == nums[left + 1]) left++;
                 while(left < right && nums[right] == nums[right - 1]) right--;
                 left++;
                 right--;
             }else if (diff > 0){
                 left++;
             }else {
                 right--;
             }
         }
     }
    }
    

    可以看到,当 k == 2 时,可以计算出当前已固定的元素在剩余范围内是否存在解,如果存在则 2-SUM 问题的解加上已固定的元素为 K-SUM 问题的解,将其添加进入 result;

  3. 公共问题的缩小

    当 k > 2 时,问题都可以由 K-SUM 问题降至 (K-1)-SUM 问题

    public void kSum(int[] nums, int k, int target, int from, int end, List<Integer> cur, List<List<Integer>> result){
         if(k == 2) {
             ···
         } else {
             for(int i = from; i < end - k + 2; i++){
                 if(i > from && nums[i] == nums[i - 1]) continue;
                 cur.add(nums[i]);
                 kSum(nums, k - 1, target - nums[i], i + 1, end, cur, result);
                 cur.remove(cur.size() - 1);
             }
         }
     }
    

    若 k > 2,遍历 from - end 中的元素,固定元素将其添加至 cur 中,并修改 k 为 k-1,修改解范围为当前位置加一至末尾;

​至此,K-SUM 问题就解决了,但是还有优化的空间,和之前解决 4-SUM 问题一样,可以添加固定值的极大值与极小值用于判断是否有必要缩小问题,如果极大值小于 target 或极小值大于 target 就可以直接知道无解可以直接返回;

​ 以下就是完整代码

public void kSum(int[] nums, int k, int target, int from, int end, List<Integer> cur, List<List<Integer>> result){
        if(k == 2) {
            int left = from;
            int right = end;
            while(left < right){
                int diff = target- nums[left] - nums[right];
                if (diff == 0){
                    List<Integer> r = new ArrayList<>();
                    r.add(nums[left]);
                    r.add(nums[right]);
                    r.addAll(cur);
                    result.add(r);
                    while(left < right && nums[left] == nums[left + 1]) left++;
                    while(left < right && nums[right] == nums[right - 1]) right--;
                    left++;
                    right--;
                }else if (diff > 0){
                    left++;
                }else {
                    right--;
                }
            }
        } else {
            for(int i = from; i < end - k + 2; i++){
                int temp = k;
                int large = 0;
                int small = 0;
                while (temp > 0){
                    large += nums[end - temp + 1];
                    small += nums[from + temp - 1];
                    temp--;
                }
                if(small > target) return;
                if(large < target) return;
                if(i > from && nums[i] == nums[i - 1]) continue;
                cur.add(nums[i]);
                kSum(nums, k - 1, target - nums[i], i + 1, end, cur, result);
                cur.remove(cur.size() - 1);
            }
        }
    }

    public List<List<Integer>> sum(int[] nums, int target, int k){
        int length = nums.length;
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> cur = new ArrayList<>();
        Arrays.sort(nums);
        if (k < 2) return;
        kSum(nums, k, target, 0, length - 1, cur, result);
        return result;
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容