视频讲解:
学透哈希表,map使用有技巧!LeetCode:454.四数相加II_哔哩哔哩_bilibili
梦破碎的地方!| LeetCode:15.三数之和_哔哩哔哩_bilibili
难在去重和剪枝!| LeetCode:18. 四数之和_哔哩哔哩_bilibili
解题状态:454,383看题解解出,18在383思想上解出
454. 四数相加 ||
思路:采用哈希表进行存储;
//时间复杂度O(m*n + k*t),空间复杂度O(m*n)(上限)
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer,Integer> map = new HashMap<>();
int count = 0;
for(int i=0; i<nums1.length; i++){
for(int j=0; j<nums2.length; j++){
int key = nums1[i]+nums2[j];
if(map.containsKey(key))
map.put(key, map.get(key)+1);
else
map.put(key, 1);
}
}
for(int i=0; i<nums3.length; i++){
for(int j=0; j<nums4.length; j++){
int key = -1*(nums3[i]+nums4[j]);
if(map.containsKey(key))
count += map.get(key);
}
}
return count;
}
}
15. 赎金信
思路:哈希表。构建英文字母表数组记录字符出现次数即可。
// 时间复杂度O(m+n),空间复杂度O(1)
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if(ransomNote.equals("") || magazine.equals(""))
return false;
int[] table = new int[26];
for(int i=0; i<ransomNote.length(); i++){
table[ransomNote.charAt(i) - 'a']++;
}
for(int i=0; i<magazine.length(); i++){
table[magazine.charAt(i) - 'a']--;
}
for(int i=0; i<26; i++){
if(table[i]>0)
return false;
}
return true;
}
}
383. 三数之和
思路:去重的双指针;采用了哈希表进行了解题,但没有解出来,但去重部分存在问题;后续自行改进。学习题解,理解采用双指针结合去重进行解题,题目两个条件,形成的列表内所有元素不能完全一致,abcd四个数的索引必须不一样;两个去重的目标.
关于去重的双指针所带来的思考:因为双指针都是从0位置开始往大的增加,所以当有多个指针存在的时候,并且存在要求彼此之间索引要不同,形成的元素集合不能与其他形成的集合元素完全一样,也就是很担心会出现 abcd 被添加了,但是后续出现了一个 acbd 这样的结果(也就是当前可行情况中的某个位置在若干次循环后被又一个指针所占据了,然后又被形成一次重复的结果被输出)。但其实需要明确的是,我们从前面开始进行遍历的,如果相a与b之间存在巨大空间,担心这其中是不是还有解没有被发掘,是不是还可以有个位置赋予b后,可以找到其他的c和d;其实不用考虑这么多,因为b是从数组开头一项一项遍历过来的,因此如果有可行的解,一定全部被包括进去了。因此在进行循环条件确定的时候,两层循环内部不用再从0开始走,直接从上一次循环索引加1处往后走。这里的理解可能还有所偏颇,但是对于有序的数组,一定在循环上进一步练习加深理解。
// 时间复杂度O(n^2),空间复杂度O(1)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 去重指的是数组的值不相等,而不是数组的下标不相等
Arrays.sort(nums);
// List<Integer> list = new ArrayList<>();
// Map<Integer, Integer> map = new HashMap<>();
List<List<Integer>> res = new ArrayList<>();
// // 优先提取nums中不重复元素,形成列表
// for(int i=0; i<nums.length; i++){
// list.add(nums[i]);
// }
// for(int i=0; i<list.size(); i++){
// for(int j=i+1; j<list.size(); j++){
// List<Integer> l = new ArrayList<>();
// int temp = (-1)*(list.get(i) + list.get(j));
// // 存在一个 a+b+c=0 的情况
// if(list.contains(temp) == true){
// // 采用<-c,b>或<-c,a>的形式存储,在后续判断时,要保障新的a与b不能与键值对的value相等,那么a,b,-c一定不是同一对
// if(map.containsKey(temp) == true){
// if(map.get(temp) != list.get(i) && map.get(temp) != list.get(j)) // 这是一个可行的结果
// if(list.lastIndexOf(temp)>i && list.lastIndexOf(temp)>j){
// l.add(list.get(i));
// l.add(list.get(j));
// l.add(temp);
// res.add(l);
// }
// }
// else{
// map.put(temp, list.get(i));
// if(list.lastIndexOf(temp)>i && list.lastIndexOf(temp)>j){
// l.add(list.get(i));
// l.add(list.get(j));
// l.add(temp);
// res.add(l);
// }
// }
// } // end if
// } // end for
// } // end for
for(int i=0; i<nums.length; 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){
// 出现一种可行的结果
if(nums[i] + nums[left] + nums[right] == 0){
List<Integer> l = new ArrayList<>();
l.add(nums[i]);
l.add(nums[left]);
l.add(nums[right]);
res.add(l);
while(left<right && nums[left] == nums[left+1])
left++;
while(left<right && nums[right] == nums[right-1])
right--;
left++;
right--;
}
else if(nums[i] + nums[left] + nums[right] > 0)
right--;
else
left++;
}
}
return res;
}
}
18. 四数之和
思路:去重的双指针。其实主要还是哈希表的思想,对重复元素不考虑。但是由于题目中还是存在四个数的索引彼此不同;而使用哈希表无法连带索引以及数据元素一同存储;采用键值对的话的确可以连带存储,但是为了方面查找是否有a+b+c+d=target的情况出现,设置target-a-b-c的数值为键方便检索,但是这样的键存在重复的可能性,即nums内有多个 -d ,而哈希表中最多存储一个 -d及其索引。
因为哈希表的磕磕碰碰,还是受三数之和的启发选用双指针。
几个做题时的心得:
- 每个位置都需要去重,但是要分清是外部边界的去重,还是内部指针的去重;
- 对于相加等于target这类题目,存在超出Integer取值范围的坑,所以不要讲四个数的和强转为long,而是在相加时就将第一个数的类型转为long;
- 四个位置去重之后,外部边界不需要再移动了;内部指针需要额外移动一次。这就是 index与index-1 相比和 index与index+1 两种去重方法所带来的不同。
// 时间复杂度O(n^3),空间复杂度O(1)(如果不算结果数组的话)
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
// 与三数之和有着相似的解法,采用双指针进行求解
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i=0; i<nums.length; i++){
// a 去重
if(i>0 && nums[i] == nums[i-1])
continue;
for(int j=i+1; j<nums.length; j++){
// 也可以使用这种方式提前去重,因为再次理解下来,只有left与right是内部,外部的循环都是边界,只要是为left与right打造边界,就可以用这个方法进行去重
// if(j>(i+1) && nums[j]==nums[j-1])
// continue;
// 这里对j不需要进行去重,因为[i,right]这个范围是待考察范围
// 而在三数之和中,此处的i就可以进行去重,是因为i就是边界;可以决定考察范围,而这里的j不可以
int left = j+1;
int right = nums.length-1;
while(left<right){
if((long)nums[i]+nums[j]+nums[left]+nums[right] == target){
List<Integer> l = new ArrayList<>();
l.add(nums[i]);
l.add(nums[j]);
l.add(nums[left]);
l.add(nums[right]);
res.add(l);
// 将与当前可行情况中left与right处的重复元素进行筛去
// c 与 d 去重
while(left<right && nums[left]==nums[left+1])
left++;
while(left<right && nums[right]==nums[right-1])
right--;
// 不管是去重之后还是未去重,left与right所显示的值都是与被输出的可行情况中的nums【left】和nums【right】一致的
// 所以需要再额外缩减范围,从而循环可以正常继续执行
left++;
right--;
}
else if((long)nums[i]+nums[j]+nums[left]+nums[right] > target)
right--;
else
left++;
} // end while
// 输出完一种可行情况后或者没有可行情况,也需要对 b 进行去重
while((j+1)<nums.length && nums[j] == nums[j+1])
j++;
} // end for
} // end for
return res;
}
}