15. 3Sum (medium)

描述

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.

Note:

The solution set must not contain duplicate triplets.

Example:

Given array nums = [-1, 0, 1, 2, -1, -4],

A solution set is:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

分析

首先是最容易想到的暴力破解,通过三重遍历数组nums,依次确定nums[i]nums[j]nums[k]并计算三元素之和是否为0。这是最粗暴的解法,但是存在去重的问题,如[-1, 0, 1]和[0, 1, -1]这种情况。

其次,这个题目作为2Sum的进阶题目,很容易联想到将3Sum转化为求target值为0 - nums[i],并在数组剩余元素中找出两个元素之和为target的2Sum问题。但是同样存在去重问题。

关于去重,由于是一个List对象,不方便使用Set和Map。解决方法可以为对List<List>的每个item(List)进行排序,然后针对要add的item,和结果集中的所有item比较是否相等。因为都进行了排序操作,所以同时比较item.get(0)item.get(1)item.get(2)三者相等即为重复,拒绝添加进结果集。

最后,以上两种思路都存在去重问题,问题需要的是找出数组中三个元素之和为0的所有组合。去重过程很明显是和结果无关,但是却非常麻烦,因此要优化算法就要着眼于移除去重这个步骤。

在暴力破解的时候就该意识到边界问题。

在做第一层循环时可以这样写:for(int i = 0; i < nums.length - 2; i++)。即第一层循环的结束条件是nums.length - 2,并不需要到nums.length

同样第二层循环时:for(int j = i + 1; j < nums.length - 1; j++)。开始索引不需要从0开始,可以直接从i + 1开始,而结束为nums.length - 1

第三层:for(int k = j + 1; k < nums.length; k++)

可以看到在暴力破解的时候,我们已经有意识地通过边界条件过滤掉一些情况,进行了初步优化。注意到数组本身是无序的,所以在确定元素的时候难以界定当前遍历元素是否已经被选中过。如果数组是有序的,那么三重遍历的时候就可以有意识地跳过重复元素。到这里已经对暴力破解的解题思路进行了优化,但是三重遍历无疑是导致时间复杂度为O(N^3),这么高的时间复杂度肯定是要被抛弃的。那么该如何继续优化呢?

尝试优化思路二。首先使用排序解决去重问题。遍历排序后的数组,固定第一个元素为nums[i],接下来在索引位i + 1nums.length之间找出两个元素nums[j]nums[k],二者之和为0 - nums[i]。固然这可以做遍历两次达到目的,相信基本上2Sum都是这样完成的。但是针对一个有序数组,夹逼法可以将这个过程的时间复杂度降为O(N)。因此,使用夹逼法找出剩余两个元素。ps,别忘了同时对2Sum使用夹逼法进行优化。

代码

public List<List<Integer>> threeSum(int[] nums) {
	List<List<Integer>> result = new LinkedList<List<Integer>>();
	if (nums == null || nums.length < 3) {
		return result;
	}
	// 对数组排序
	Arrays.sort(nums);
	//固定第一个元素nums[i]
	for (int i = 0; i < nums.length - 2; i++) {
		//默认是从小到大的排序,所以当nums[i]大于0的时候,就可以结束
		if (nums[i] > 0) {
			break;
		}
		//nums[i - 1] != nums[i]执行了去重,注意这里在理解的时候要意识到此时操作的数组已经是有序数组
		if (i == 0 || nums[i - 1] != nums[i]) {
			//使用加逼法
			int j = i + 1;
			int k = nums.length - 1;
			while (j < k) {
				int sum = nums[i] + nums[j] + nums[k];
				if (sum == 0) {
					result.add(Arrays.asList(nums[i], nums[j], nums[k]));
				} 
                if (sum <= 0) {
					while (j < k && nums[j] == nums[++j]);
				}
                if(sum >= 0){
					while (j < k && nums[k] == nums[--k]);
				}
			}
		}
	}
	return result;
}

上面的代码是优化之后的代码,对于理解加逼的过程有点不便,下面是加逼的原始写法:

while (j < k) {
	int target = 0 - nums[i];
    if(target == (nums[j] + nums[k])){
        result.add(Arrays.asList(nums[i], nums[j], nums[k]));
        j++;
        while(nums[j] == nums[j - 1] && j < k){ //去重,注意这是一个有序数组
            j++;
        }
        k--;
        while(nums[k] == nums[k + 1] && j < k){ //去重,注意这是一个有序数组
            k--;
        }
    }else if(target < (nums[j] + nums[k])){
        k--;
        while(nums[k] == nums[k + 1] && j < k){ 
            k--;
        }
    }else if(target > (nums[j] + nums[k])){
        j++;
        while(nums[j] == nums[j - 1] && j < k){ 
            j++;
        }
    }  
}