454. 四数相加 II
这道题暴力解法就是四个for循环嵌套,时间复杂度为n4
卡哥的讲解中,核心思路我觉得可以这么理解:
- 把A、B分为一组,C、D分为一组,创建map1、map2
- map1每组键值对的含义为
(A、B中的元素能组合出来的某个数字,这个数字共有多少个下标组合)
,比如说如果map1中的某一条为(2,3),说明A、B中有3组下标的组合能够新加得到2。map2的含义类似。 - 设置一个sum,记录组合的总数
- 遍历map1,每拿到一个键key1,就查map2的键中有没有0-key1,如果有,key1对应的值v1就和key2对应的值v2相乘,再累加到sum中,表示ABCD四个数组中,共有v1*v2种下标组合,能够以key1+key2=0的形式达到A[i] + B[j] + C[k] + D[l] = 0
但是这么写的话就得两次for遍历A、B+两次for遍历C、D+额外遍历一次map1/map2,而卡哥这么写就不用额外遍历一次map
为什么要2+2分组?因为1+3分组的话时间复杂度就是n3
为什么想到用哈希表呢?
我感觉这道题有种根据目标,拿着A去问B有没有的那种感觉,所以可以用哈希表。
不过卡哥说这道题是哈希表经典题目,那就记住吧。
为什么能用哈希表?
因为不需要去重。当然要去重的题目哈希表也可以做,就是比较麻烦
为什么要用Map?
因为这道题不仅要记录“有没有”,还要记录“有多少”。Set只有一个位置,只能记录“有没有”
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
HashMap<Integer, Integer> map1 = new HashMap<>();
int len = nums1.length;
int count = 0;
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
int sum1 = nums1[i] + nums2[j];
map1.put(sum1, map1.getOrDefault(sum1, 0) + 1);
}
}
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
int sum2 = nums3[i] + nums4[j];
int target = -sum2;
if (map1.containsKey(target)) {
count += map1.get(target);
}
}
}
return count;
}
383. 赎金信
如果只有小写字母或者只有大写字母,那么就可以用数组做哈希表
思路方面没啥
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) return false;
int[] letters = new int[26];
// 计算magazine中出现了哪些字母,每个字母出现了多少次;
for (int i = 0; i < magazine.length(); i++) {
letters[magazine.charAt(i) - 'a']++;
}
// ransomNote开始消耗字母
for (int i = 0; i < ransomNote.length(); i++) {
letters[ransomNote.charAt(i) - 'a']--;
}
// 遍历letters,如果有<0的说明就不行
for (int i = 0; i < letters.length; i++) {
if (letters[i] < 0) return false
}
return true;
}
15. 三数之和
思路方面我觉得记住就好,把这道题当做母题
暴力解法是三个for循环,卡哥的解法是用双指针把两个for循环改成一次循环遍历,我觉得挺有意思的
由于这道题求的具体的三元组,不关心位置,因此可以把数组排序
我觉得这道题最难的地方在于想清楚去重的逻辑:
- 对于a,要朝后看,因为三元组中可能出现(x,x,y)这种形式
- 对于b,c,要朝前看,因为当我在找b、c的时候,代表a已经确定,此时要找的b+c就是一个确定值。假如说我找到了b0+c0满足要求,那我就不可能在a不变的情况下再找到一组b0+c1或者b1+c0也满足要求,因此要朝前去重。 去重的时候要注意,一定要先找到,再考虑去重
双指针的面对面遍历的时候要注意:
- 有没有超过数组边界
- 有没有超过对面的指针
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
// 对数组排序
Arrays.sort(nums);
// 剪枝:如果第一个数就>0,那么直接返回null
if (nums[0] > 0) return ans;
// a开始遍历
for (int i = 0; i < nums.length - 2; i++) {
// 对a去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.length - 1;
// b,c开始遍历
while (left < right) {
if (nums[i] + nums[left] + nums[right] == 0) { //找到一组满足要求的
// 创建三运组
List<Integer> ansUnit = Arrays.asList(nums[i], nums[left], nums[right]);
// 添加进答案
ans.add(ansUnit);
// 对b去重
while (left < right && nums[left] == nums[left + 1]) left++;
// 对c去重
while (left < right && nums[right] == nums[right - 1]) right--;
//寻找下一组
left++;
right--;
} else if (nums[i] + nums[left] + nums[right] > 0) {
right--;
} else {
left++;
}
}
}
return ans;
}
然后我思考了一下,什么时候去重的时候要朝前看,什么时候去重的时候要朝后看呢?我希望有一个大一统的理论。
说一下我思考的结果,我首先要明确向前看和向后看去重的含义是什么。
x向前看进行去重,代表的就是要求结果集中每个结果单位中x的对应位置上,在当前的限制条件下,不能是同一个元素。向前看进行去重的方式是先找到,再剔除。
x向后看进行去重,代表的就是要求结果集中每个结果单位中x的对应位置上,在当前的限制条件下,可以是同一个元素。向后看进行去重的方式是先判断,然后决定要不要contine
那么思考这道题。
首先思考a,a代表三元组中第一个位置,选择什么去重方式呢?
如果在for循环体开头进行去重,那么此时a没有限制,三元组中的第一个位置的数可以重复,比如(-2,0,2)和(-2,1,1),因此是向后看,就是先判断重不重,再判断要不要continue。
如果在for循环结尾进行去重,那么此时的条件为三元组中第一个数为x的组合已经全部被找到,那么此时三元组的第一个位置的数不能重复,因此要向前看。也就是下面这个版本
public List<List<Integer>> threeSum2(int[] nums) {
List<List<Integer>> ans = new ArrayList<>();
// 对数组排序
Arrays.sort(nums);
// 剪枝:如果第一个数就>0,那么直接返回null
if (nums[0] > 0) return ans;
// a开始遍历
for (int i = 0; i < nums.length - 2; i++) {
// 对a去重
// if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.length - 1;
// b,c开始遍历
while (left < right) {
if (nums[i] + nums[left] + nums[right] == 0) { //找到一组满足要求的
// 创建三运组
List<Integer> ansUnit = Arrays.asList(nums[i], nums[left], nums[right]);
// 添加进答案
ans.add(ansUnit);
// 对b去重
while (left < right && nums[left] == nums[left + 1]) left++;
// 对c去重
while (left < right && nums[right] == nums[right - 1]) right--;
//寻找下一组
left++;
right--;
} else if (nums[i] + nums[left] + nums[right] > 0) {
right--;
} else {
left++;
}
}
// 对a进行去重,向前看版本
while (i < nums.length - 2 && nums[i] == nums[i+1]) i++;
}
return ans;
}
接下来思考b。
b的限制条件为三元组第一个位置已经确定,也就是说b+c是一个定值,那么就不可能出现多个三元组,a、b位置都相同,c不同;同理,不可能出现多个三元组a、c位置都相同,b不同。因此b、c都要用向前看,就是先找到,再剔除。
18. 四数之和
思路和三数之和差不多,关键还在于去重。
我a、b都放在循环开头去重,因此都是无限制,用向后看。
c、d和上一题的b、c没区别,你把a、b看成一个整体,那么就是上一题中一个确定的A
要用long,有点小坑
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> ans = new ArrayList<>();
Arrays.sort(nums);
//开始遍历a
for (int i = 0; i < nums.length - 3; i++) {
// 对a进行向后看去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 开始遍历b
for (int j = i + 1; j < nums.length - 2; j++) {
// 对b进行向后看去重
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
// 定义c、d
int left = j + 1;
int right = nums.length - 1;
// 开始遍历c、d
while (left < right) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum < target) {
left++;
} else if (sum > target) {
right--;
} else {
// 创建四元组
List<Integer> ansUnit = Arrays.asList(nums[i], nums[j], nums[left], nums[right]);
// 添加进答案
ans.add(ansUnit);
// 对c去重
while (left < right && nums[left] == nums[left + 1]) left++;
// 对d去重
while (left < right && nums[right] == nums[right - 1]) right--;
//寻找下一组
left++;
right--;
}
}
}
}
return ans;
}