不知道大家是否还能够想起自己刷leetcode的第一道题,反正我能清晰的记得——两数之和。当时用两个for循环,暴力地得到了结果。然后,一看答案,TMD hash 是什么鬼,接着看那个答案的讲解(脑中瞬间想起那个声音),看完视频,遂放弃......
其实,像两数之和、三数之和以及四数之和在校招面试中还是很可能出现的,一定不能做不出来,否则就可以去买块豆腐撞shi自己了。
不多废话,今天一次性把这个n数之和给大家总结一下:
就不要两个for循环去做了吧,会被鄙视的。
这道题的前提是每种输入只会对应一个答案,所以找到结果就可以立即返回。
我们要利用一个HashMap,其中map的key为target - nums[i],value为当前位置下标i。
以下图为例:
当我们走到4的时候,发现之前hashmap中存过4,那么我们就找到了下标0和下标2的位置加起来是得target的。
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
map.put(target - nums[0], 0);
int[] res = new int[2];
for (int i = 1; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
res[0] = map.get(nums[i]);
res[1] = i;
break;
}
map.put(target - nums[i], i);
}
return res;
}
这道题也不能用三个for循环去做了吧?太暴力了,还是用双指针吧。
其实这道题的思路也很清晰,题目不是说让我们找三个数的和吗?那我们把第一个数固定,让另外两个数变不就好了,当另外两个数把数组都遍历完了,我们再移动第一个数,再让另外两个数变。
图解如下:
通过这种方法遍历下去,就能获取到所有解。但是如果只是这样的话,还是不能获取到最终的正确答案,因为题目说了答案中不可以包含重复的三元组,所以我们还得给获得的结果去重。比如说,当我们移动到如下位置时,会得到相同的结果,所以在我们写代码的时候,可以通过以下代码跳过去:
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
这样就可以获得正确答案了。
但是,细心的小伙伴就发现了,在这道题的第一幅图中,第一步和第二步,也是重复的:
左边指针在两个-1处获得的所有组合都是一致的,除了{-4, -1, -1},但是这种情况包含在左边的-1的所有结果内,因此可以说,右边-1的所有结果是左边-1所有结果的子集。由此,我们可以得出一个结论,就是只要我的左指针向右移动时,碰到和我相邻的前一个数一样,就可以直接跳过他直接继续向右移动,直到下一个值与之前的值都不一样。同理,我们的右指针向左移动也要遵循这个原则。这段代码如下:
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
最后,就是这道题要求解的前提是,必须给这个数组排序,否则是找不出全正确答案的。直接调用Arrays.sort(nums)就行,你也可以自己写个排序(但是我们掉的这个sort方法要比我们自己写的快,包括快排、堆排等,之后我也会写出一篇关于排序算法的介绍)。
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) {
return res;
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < 0) {
left++;
} else if (sum > 0) {
right--;
} else {
res.add(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--;
}
}
}
return res;
}
这道题和上面的三数之后非常相似,逻辑是一样的,只不过这道题是找最接近目标值target的数,那么其实我们找到和target相等的数就可以立刻返回。如果找不到,那么和target最接近的数就一定在nums[i] + nums[left] + nums[right] 和 nums[i] + nums[left + 1] + nums[right]之间,或者在nums[i] + nums[left] + nums[right] 和 nums[i] + nums[left] + nums[right - 1]。也就是说,当到了某个临界值的时候,当left所在位置的三数和小于target,而当left向右移动一个位置,三数和就大于target了,之后时候就要取这两个位置和的最接近target的值。right也是同样的道理。
所以有了如下代码:
res = Math.abs(res - target) <= Math.abs(sum - target) ? res : sum;
if (sum < target) {
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
} else {
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
}
上述whlie中代码是用来去重的,跟上题原理一致。
最终题解如下:
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int res = nums[0] + nums[1] + nums[2];
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == target) {
return sum;
}
res = Math.abs(res - target) <= Math.abs(sum - target) ? res : sum;
if (sum < target) {
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
} else {
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
}
}
}
return res;
}
四数之和,就是一个数组中的四个数相加,让其等于一个目标值(没完了是吧)。解法跟三数之和一样的思路,只要三数之和你弄明白了,四数之和不看题解就可以自己做出来。三数之和是固定一个值,让两个指针去遍历数组剩余部分。
那四数之和呢?
让三个指针呗。
开什么玩笑,三个指针咋遍历,那不乱套了。
so,就固定两个值,然后让两个指针去遍历剩余部分,不就好了。
就这样(举个例子):
剩下的就和三数之和大差不差了。
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
if (nums.length < 4) {
return res;
}
Arrays.sort(nums);
for (int i = 0; i < nums.length - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < nums.length - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = nums.length - 1;
while (left < right) {
long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
if (sum < target) {
left++;
} else if (sum > target) {
right--;
} else {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return res;
}
其实还有很多类似题,之后如果遇到的话我再补充进来。