背景
先写的是 15. 三数之和 问题,发现该题解中提及可以使用 1. 两数之和 和“双指针”方法,于是用“双指针”的方法写“两数之和”问题,但是发现此题解使用的是HashMap。于是使用哈希表去写“三数之和”问题中的twoSum,结果发现“如何规避重复情况”需要仔细斟酌。分析总结如下:
先看两个问题的要求:
三数之和:返回所有和为 0 且不重复的三元组
两数之和:返回它们的数组下标;假设每种输入只会对应一个答案
发现区别在于:
1、“三数之和”要求返回“元素”,“两数之和”要求返回“下标”。
2、(在对两道题都使用哈希表的过程中发现,当固定一个数去找另一个数时)“两数之和”只要求返回一组解,而“三数之和”要求返回所有满足的解。
为什么“两数之和”题解使用HashMap而不是双指针?
因为:“两数之和”要求返回“下标”。如果使用“双指针”,那么需要对数组排序。但是排完序后如何知道该元素在原数组中的下标呢?
一种思路:将原数组nums copy一份得到c_nums(再对原数组进行排序)。使用c_nums找到对应元素在原数组中的下标。由于最终需要返回2个下标,为了减少一次循环:
//最终return finalnums。数组finalnums初始值为{-1, -1}。
for (int i = 0; i<nums.length; i++) {
if ((c_nums[i] == nums[start] || c_nums[i] == nums[end]) && finalnums[0] == -1) {
finalnums[0] = i;
} else if ((c_nums[i] == nums[start] || c_nums[i] == nums[end]) && finalnums[0] != -1) {
finalnums[1] = i;
break;
}
}
而使用HashMap的话,则不需要排序(下标不变),且空间复杂度小于“再重新开辟一个相同的数组空间”。
如果“三数之和”中的twoSum使用哈希表:
因为“三数之和”需要返回所有解,所以使用哈希表找到一个解后不能break;,依然得继续找。这时,重复答案就无法避免了。而“三数之和”问题的难点就在于“如何规避重复的答案数组”。如何规避重复答案呢?对数组进行排序(双指针的前提就是排序)。
此外,HashMap中的value值原本是用来记录“该元素在数组中的下标”的,在“三数之和”问题并没有用到。考虑后换成HashSet。
如果排完序之后依然使用HashSet:
以数组{0, 0, 0, 0}为例,如何避免重复?
固定一个数,剩下的构成“两数之和”问题,所以依次固定第1个0和第2个0会产生重复答案,因此在最外层循环里加上:
if (i > 0 && nums[i] == nums[i - 1]) continue;
将剩下的元素处理成“两数之和”问题:
固定第2个0,发现第3个0可组成答案,为了避免重复,此时第4个0应该continue;,因此,在第2层循环中加上:
if (i > curi + 2 && nums[i] == nums[i - 1]) continue;
其实这里考虑欠佳,对于数组{-2, 0, 1, 1, 2},会有答案缺失。对比{0, 0, 0, 0}和{-2, 0, 1, 1, 2}会发现,第3个0是答案,所以在第4个0处需要continue;但是在数组{-2, 0, 1, 1, 2}中,第3个1不是答案,第4个1不应该continue(这里{-2, 1, 1}也恰好是一个答案)。所以当元素重复时,需要对重复元素是否构成答案进行判断,只有构成答案时,下一个元素重复时才continue。正确地,在第2层循环中加上:
//循环外:boolean flag = false;
if (i > curi + 2 && nums[i] == nums[i - 1] && flag) continue;
else if (i > curi + 2 && nums[i] != nums[i - 1]) flag = false;
...
if (hs.contains(t)) {
flag = true;
...
}