【 454.四数相加II 】
方法一 看了思路之后自己写的
思路:将4个数组组合成2个,再利用两数之和的思路
1、获取nums1和nums2的各元素之和(key)出现的次数(value) map12
2、获取nums3和nums4的各元素之和(key)出现的次数(value) map34
3、使map12是短的一方,map34是长的一方(希望可以加快一点点)
4、map12和map34的key如果相加为0,则返回值加上对应value相乘的结果
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer, Integer> map12 = new HashMap<>();
Map<Integer, Integer> map34 = new HashMap<>();
// 获取nums1和nums2的各元素之和(key)出现的次数(value)
for(int i = 0; i < nums1.length; i++){
for(int j = 0; j < nums2.length; j++){
int temp = nums1[i] + nums2[j];
if (!map12.containsKey(temp)){
map12.put(temp, 1);
}
else{
map12.put(temp, map12.get(temp) + 1);
}
}
}
// 获取nums3和nums4的各元素之和(key)出现的次数(value)
for(int i = 0; i < nums3.length; i++){
for(int j = 0; j < nums4.length; j++){
int temp = nums3[i] + nums4[j];
if (!map34.containsKey(temp)){
map34.put(temp, 1);
}
else{
map34.put(temp, map34.get(temp) + 1);
}
}
}
// 使map12是短的一方,map34是长的一方
Map<Integer, Integer> map;
if (map12.size() > map34.size()){
map = map12;
map12 = map34;
map34 = map;
}
// map12和map34的key如果相加为0,则返回值加上对应value相乘的结果
int res = 0;
for (int i: map12.keySet()){
int temp = 0 - i;
if (map34.containsKey(temp)){
res += map12.get(i) * map34.get(temp);
}
}
return res;
}
}
时间复杂度: O(n²)
空间复杂度: O(n²)
方法二 优化后的版本
思路:可以在遍历nums3和nums4的时候直接计算方案数,简化代码,加快速度
注意:default V getOrDefault(Object key, V defaultValue)的使用可以免除键是否存在的判断,因为当键不存在时,getOrDefault函数会返回默认值defaultValue,而普通的get函数会返回null。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer, Integer> map12 = new HashMap<>();
Map<Integer, Integer> map34 = new HashMap<>();
// 获取nums1和nums2的各元素之和(key)出现的次数(value)
for(int i = 0; i < nums1.length; i++){
for(int j = 0; j < nums2.length; j++){
int temp = nums1[i] + nums2[j];
map12.put(temp, map12.getOrDefault(temp, 0) + 1);
}
}
// 在遍历nums3和nums4的时候寻找可行的组合
int res = 0;
for (int i = 0; i < nums3.length; i++){
for (int j = 0; j < nums4.length; j++){
res += map12.getOrDefault(0 - nums3[i] - nums4[j], 0);
}
}
return res;
}
}
时间复杂度: O(n²)
空间复杂度: O(n²)
【 383. 赎金信 】
方法一 利用HashMap
思路:
1、遍历magazine构建字母(key)和字母出现的次数(value)的映射map
2、遍历ransomNote,遍历到的字母在map中存在就-1,不存在就直接返回false
- 如果map中的字母次数被用透支了,直接返回fasle
- 如果map中的字母次数没被用完/刚好被用完,就返回true
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
Map<Character, Integer> map = new HashMap<>();
// 遍历magazine构建字母(key)和字母出现的次数(value)的映射map
for (int i = 0; i < magazine.length(); i++){
char c = magazine.charAt(i);
map.put(c, map.getOrDefault(c, 0) + 1);
}
// 遍历ransomNote,遍历到的字母在map中存在就-1,不存在就直接返回false
for (int i = 0; i < ransomNote.length(); i++){
char c = ransomNote.charAt(i);
if (!map.containsKey(c)){
return false;
}
else{
map.put(c, map.get(c) - 1);
if (map.get(c) < 0) return false; // 如果map中的字母次数被用透支了,直接返回fasle
}
}
return true; // 如果没用完/刚好用完,就返回true
}
}
时间复杂度: O(m+n),m是字符串magazine的长度,n是字符串ransomNote的长度
空间复杂度: O(m),HashMap的长度最大等于字符串magazine的长度
方法二 利用数组构建字母哈希表
思路:
1、利用字母的排序构建哈希表的索引,字母出现的次数作为值,哈希表为长度26的数组
2、遍历字符串magazine,填充哈希表
3、遍历字符串ransomNote,哈希表对应字母次数-1
如果次数小于0,则直接返回false
如果遍历完,就返回true
优化:
可以先判断字符串ransomNote的长度是否大于字符串magazine,大于则肯定不够用,直接返回false即可
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int[] hashtable = new int[26];
// 优化,通过长度判断加快速度
if (ransomNote.length() > magazine.length()) return false;
// 遍历magazine,填充哈希表
for (int i = 0; i < magazine.length(); i++){
char c = magazine.charAt(i);
hashtable[c - 'a'] += 1;
}
// 遍历randomNote,令哈希表对应字母次数-1,如果-1后出现负值,则直接返回false
for (int i = 0; i < ransomNote.length(); i++){
char c = ransomNote.charAt(i);
hashtable[c - 'a'] -= 1;
if (hashtable[c - 'a'] < 0) return false;
}
// 如果遍历完randomNote,对应字母处都没有出现负值,则返回true
return true;
}
}
时间复杂度: O(m+n),m是字符串magazine的长度,n是字符串ransomNote的长度
空间复杂度: O(1),hashtable为固定长度的数组,和输入数据的长度无关
【 15. 三数之和 】
方法 双指针
思路:
1、排序
令数据升序排序,方便判断后续左右指针的移动
2、遍历数组
- 判断当前遍历的值是否已经大于0,如果大于0就可以直接返回结果了
- 判断当前遍历的值是否和上一个遍历的值相等,相等就可以跳过操作,实现【去重操作1】
- 定义双指针的初始指向,左指针left指向遍历元素的后一位,右指针right指向数组的最后一位元素
3、根据 int s = nums[i] + nums[left] + nums[right]的值的大小,进行结果记录和指针操作
- s=0:记录结果,同时移动左右指针,判断左右指针是否与旧值相等,相等则继续移动,实现【去重操作2】;
- s>0:证明s太大了,要减小,只有左移 right 才能减小;
- s<0:证明s太小了,要增大,只有右移 left 才能增大;
注意:
涉及的两次去重操作,都是为了避免对返回值resLists进行去重,两次去重操作分别是针对遍历元素的指针和左右指针进行的。
优化:
1、对数组排序时,可以直接使用 Arrays 工具类提供的 sort 函数简化代码,加快速度,其时间复杂度为O(nlogn)
2、将数组转换为 List 集合时,可以直接使用 Arrays 工具类提供的 asList 函数简化代码,asList返回的List的元素类型为数组元素类型的包装类,如数组的元素是int型,则转换后的List的元素是Integer型。
3、在做题时要多思考循环的截止条件,例如此题如果遍历到的值已经大于0,那么后面的元素肯定大于当前遍历的值,三个数相加不可能等于0,就可以停止遍历,直接返回结果了。
4、双指针使得时间复杂度由暴力解的O(n^3)减小为O(n^2)。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 冒泡排序:O(n²)
// for (int i = nums.length - 1; i > 0; i--){
// for (int j = 1; j <= i; j++){
// if (nums[j] < nums[j-1]){
// int temp = nums[j];
// nums[j] = nums[j - 1];
// nums[j - 1] = temp;
// }
// }
// }
// Arrays类提供的快速排序算法: O(nlogn)
Arrays.sort(nums);
List<List<Integer>> resLists = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++){
// 优化
if (nums[i] > 0) return resLists;
// 当前遍历值=上一遍历值,【去重操作1】
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = nums.length - 1;
while (left < right){
int s = nums[i] + nums[left] + nums[right];
if (s == 0){
// 记录结果
// List<Integer> resList = new ArrayList<>();
// resList.add(nums[i]);
// resList.add(nums[left]);
// resList.add(nums[right]);
// resLists.add(resList);
resLists.add(Arrays.asList(nums[i], nums[left], nums[right])); // 减少内存消耗
// 同时操作左右指针
left++;
right--;
// 【去重操作2】
while (left < right && nums[left] == nums[left - 1]){
left++;
}
while (left < right && nums[right] == nums[right + 1]){
right--;
}
}
// 太大了,right左移让s小一点
else if (s > 0){
right--;
}
// 太小了,left右移让s大一点
else{
left++;
}
}
}
return resLists;
}
}
时间复杂度: O(n²),for循环内部嵌套了while
空间复杂度: O(1),返回值不需要考虑空间复杂度
【18. 四数之和 】
方法 双指针
思路:
1、排序
2、两层for循环+左右指针遍历
- 每层for开始时,都要判断当前遍历值与上一个遍历值是否相等,进行【去重操作1】
- s=0,左右指针同时移动的时候,要进行【去重操作2】
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
// 1、排序,O(nlogn)
Arrays.sort(nums);
// 2、两层for循环+左右指针
List<List<Integer>> resLists = new ArrayList<>();
// 第一层for循环
for (int i = 0; i < nums.length - 3; i++){
// 优化/剪枝
if (nums[i] > target && nums[i] > 0) return resLists;
// 去重操作1
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 第二层for循环
for (int j = i + 1; j < nums.length - 2; j++){
// 去重操作1
if (j > i + 1 && nums[j] == nums[j-1]) continue;
// 左右指针初始化
int left = j + 1;
int right = nums.length - 1;
// 左右指针遍历
while (left < right){
int s = nums[i] + nums[j] + nums[left] + nums[right];
if (s > target){
right--;
}
else if (s < target){
left++;
}
else{ // s = target
resLists.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 同时移动左右指针
left++;
right--;
// 去重操作2
while (left < right && nums[left] == nums[left - 1]){
left++;
}
while (left < right && nums[right] == nums[right + 1]){
right--;
}
}
}
}
}
return resLists;
}
}
总结:
1、双指针法将时间复杂度:O(n^4)的解法优化为 O(n^3)的解法。
2、此题相加的和不是0,而是任意值target,如果target<0,nums[i]>target,nums[i]加上后面的值可能会更小,即还有可能达到题目条件的组合未遍历完。只有当nums[i] > target,同时nums[i]>0,才能截止遍历返回结果。
时间复杂度: O(n^3),两层for循环内部嵌套了while
空间复杂度: O(1),返回值不需要考虑空间复杂度
【 总结 】
1、数组比Map构建哈希表更有效
2、双指针相比暴力法,可以将时间复杂度降低一个级别
3、重点理解数组、集合、映射三者构成哈希表的区别