文章目录
- (简单)242.有效的字母异位词
- (简单)383. 赎金信
- (中等)49. 字母异位词分组
- (*中等)438. 找到字符串中所有字母异位词
- (简单)349. 两个数组的交集
- (简单)350. 两个数组的交集||
- (*简单)202. 快乐数
- (*简单)1. 两数之和
- (中等)454. 四数相加 ||
- (*中等)15. 三数之和
- (中等)18. 四数之和
(简单)242.有效的字母异位词
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length()!=t.length()){
return false;
}
int[] s1 = new int[26];
int[] t1 = new int[26];
for (char c : s.toCharArray()) {
s1[c - 'a']++;
}
for (char c : t.toCharArray()) {
t1[c - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (s1[i] != t1[i]) {
return false;
}
}
return true;
}
}
官方其他思路,方法一:排序
t是s的异位词等价于【两个字符串排序后相等】。因此可以对字符串s和t分别排序,看排序后的字符串是否相等即可判断。此外,如果s和t的长度不同,t必然不是s的异位词。
import java.util.Arrays;
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
char[] arr1 = s.toCharArray();
char[] arr2 = t.toCharArray();
Arrays.sort(arr1);
Arrays.sort(arr2);
return Arrays.equals(arr1, arr2);
}
}
复杂度分析:
- 时间复杂度:O(nlogn),其中n为s的长度。排序的时间复杂度为O(nlogn),比较两个字符串是否相等的时间复杂度为O(n),因此,总体时间复杂度为O(nlogn+n)=O(nlogn)
- 空间复杂度:O(logn),排序需要O(logn)的空间复杂度
官方其他思路,方法二:哈希表
从另一个角度考虑,t是s的异位词等价于【两个字符串中出现的种类和次数均相等】。由于字符串只包含26个小写字母,因此,可以维护一个长度为26 的数组,先遍历记录字符串s中字符出现的频次,然后遍历字符串t,减去数组中对应的频次,如果出现某一数组元素小于0的情况,则说明t包含一个不在s中的额外字符,返回false
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
int[] table = new int[26];
for (char c : s.toCharArray()) {
table[c - 'a']++;
}
for (char c : t.toCharArray()) {
table[c - 'a']--;
if (table[c - 'a'] < 0) {
return false;
}
}
return true;
}
}
对于进阶问题, 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
这样就不能用数组来维护哈希表,直接使用HashMap即可
import java.util.HashMap;
class Solution {
public boolean isAnagram(String s, String t) {
if (s.length() != t.length()) {
return false;
}
HashMap<Character, Integer> map = new HashMap<>();
for (char c : s.toCharArray()) {
map.put(c, map.getOrDefault(c, 0) + 1);
}
for (char c : t.toCharArray()) {
map.put(c, map.getOrDefault(c, 0) - 1);
if (map.get(c) < 0) {
return false;
}
}
return true;
}
}
复杂度分析:
- 时间复杂度:O(n),其中n为s的长度
- 空间复杂度:O(S),其中S为字符集大小,本题S为26
(简单)383. 赎金信
我的思路:因为题目中说明了randomNote和magazine都是由小写字母组成,所以维护一个长度为26的数组,先统计magazine中各个字母出现的次数,在去计算randomNote中各个字母出现的次数是否小于等于magazine中各个字母出现的次数
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false;
}
int[] arr = new int[26];
for (char c : magazine.toCharArray()) {
arr[c - 'a']++;
}
for (char c : ransomNote.toCharArray()) {
arr[c - 'a']--;
if (arr[c - 'a'] < 0) {
return false;
}
}
return true;
}
}
(中等)49. 字母异位词分组
两个字符串互为字母异位词,当且仅当两个字符串包含的字母相同。同一组字母异位词中的字符串具备相同点,可以使用相同点作为一组字母异位词的标志,使用哈希表存储每一组字母异位词,哈希表的键作为一组字母异位词的标志,哈希表的值为一组字母异位词列表。
思路一:排序
在排序方法中,一组字母异位词的标志是排序以后的字符串
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, ArrayList<String>> map = new HashMap<>();
for (String str : strs) {
char[] chars = str.toCharArray();
Arrays.sort(chars);
String s = new String(chars);
ArrayList<String> list = map.getOrDefault(s, new ArrayList<>());
list.add(str);
map.put(s, list);
}
return new ArrayList<>(map.values());
}
}
复杂度分析:
- 时间复杂度:O(nklogk),其中n是strs中字符串的数量,k是strs中的字符串的最大长度。需要遍历n个字符串,对于每个字符串,需要O(klogk)的时间进行排序以及O(1)的时间更新哈希表,因此总时间复杂度是O(nklogk)
- 空间复杂度:O(nk),其中n是strs中字符串的数量,k是strs中的字符串的最大长度。需要用哈希表存储全部字符串
思路二:计数
由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的,故可以将每个字母出现的次数使用字符串表示,作为哈希表的键
由于字符串只包含小写字母,因此对于每个字符除按,可以使用长度为26的数组记录每个字母出现的次数,需要注意的是,在使用数组作为哈希表的键时,不同语言的支持程度不同,因此不同语言的实现方式也不同
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> map = new HashMap<>();
for (String str : strs) {
int[] counts = new int[26];
for (char c : str.toCharArray()) {
counts[c - 'a']++;
}
//将每个出现次数大于0的字母和出现次数按顺序拼接成字符串,作为哈希表的键
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
stringBuilder.append((char) ('a' + i));
stringBuilder.append(counts[i]);
}
}
String key = stringBuilder.toString();
List<String> value = map.getOrDefault(key, new ArrayList<>());
value.add(str);
map.put(key, value);
}
return new ArrayList<>(map.values());
}
}
(*中等)438. 找到字符串中所有字母异位词
我的思路:找到包含在p中的字符,截取和p长度一样的子串,比较两个子串是否是字母异位词
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<Integer> findAnagrams(String s, String p) {
ArrayList<Integer> res = new ArrayList<>();
if (p.length() > s.length()) {
return res;
}
if (p.length() == s.length()) {
char[] s1 = s.toCharArray();
char[] p1 = p.toCharArray();
Arrays.sort(s1);
Arrays.sort(p1);
if (Arrays.equals(p1, s1)) {
res.add(0);
}
return res;
}
int[] cnt = new int[26];
for (char c : p.toCharArray()) {
cnt[c - 'a']++;
}
int sLength = s.length();
int pLength = p.length();
char[] sCharArray = s.toCharArray();
char[] pCharArray = p.toCharArray();
Arrays.sort(pCharArray);
int i = 0;
while (i + pLength <= sLength) {
if (cnt[sCharArray[i] - 'a'] > 0) {
String substring = s.substring(i, i + pLength);
char[] subCharArray = substring.toCharArray();
Arrays.sort(subCharArray);
if (Arrays.equals(pCharArray, subCharArray)) {
res.add(i);
}
}
i++;
}
return res;
}
}
官方思路,方法一:滑动窗口
根据题目要求,我们需要在字符串s寻找字符串p的异位词。因为字符串p的异位词的长度一定与字符串p相同,所以可以在字符串s中构造一个长度为与字符串p的长度相同的滑动窗口,并在滑动中维护窗口中每种字母的数量;当窗口中每种字母的数量与字符串p中每种字母数量相同时,则说明当前窗口为字符串p的异位词
使用数组来存储字符串p和滑动窗口中每种字母的数量
当字符串s的长度小于字符串p的长度时,字符串s中一定不存在字符串p的异位词
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<Integer> findAnagrams(String s, String p) {
ArrayList<Integer> res = new ArrayList<>();
if (p.length() > s.length()) {
return res;
}
int pLen = p.length();
int sLen = s.length();
int[] sCount = new int[26];
int[] pCount = new int[26];
for (int i = 0; i < pLen; i++) {
sCount[s.charAt(i) - 'a']++;
pCount[p.charAt(i) - 'a']++;
}
if (Arrays.equals(sCount, pCount)) {
res.add(0);
}
for (int i = 0; i < sLen - pLen; i++) {
sCount[s.charAt(i) - 'a']--;
sCount[s.charAt(i + pLen) - 'a']++;
if (Arrays.equals(sCount, pCount)) {
res.add(i + 1);
}
}
return res;
}
}
复杂度分析:
- 时间复杂度:O(m+(n-m)x ∑ \sum ∑),其中n为字符串s的长度,m为字符串p的长度, ∑ \sum ∑为所有可能的字符数。O(m)来统计字符串p中每种字母的数量;需要O(m)来初始化滑动窗口;需要判断n-m+1个滑动窗口中每种字母的数量是否与字符串p中每种字母的数量相同,每次判断需要O( ∑ \sum ∑)。因为s和p仅包含小写字母,所以 ∑ \sum ∑=26
- 空间复杂度:O( ∑ \sum ∑)。用于存储字符串p和滑动窗口中每种字符的数量
方法二:优化的滑动窗口
在方法一的基础上,不在分别统计滑动窗口和字符串p中每种字母的数量,而是统计滑动窗口和字符串p中每种字母数量的差;并引入变量differ来记录当前窗口与字符串p中数量不同的字母的个数,并在滑动窗口的过程中维护它。
在判断滑动窗口中每种字母的数量与字符串p中每种字母的数量是否相同时,只需要判断differ是否为0即可。
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<Integer> findAnagrams(String s, String p) {
ArrayList<Integer> list = new ArrayList<>();
int sLen = s.length();
int pLen = p.length();
if (sLen < pLen) {
return list;
}
//count数组用来记录 滑动窗口中的字符串s的各个字母的出现次数和字符串p各个字母出现次数的差值
int[] counts = new int[26];
//首先看最开始长度为pLen的字符串s的子串
for (int i = 0; i < pLen; i++) {
counts[s.charAt(i) - 'a']++;
counts[p.charAt(i) - 'a']--;
}
//differ用来记录,滑动窗口中的字符串的字母与字符串p中的字母相差的个数
int differ = 0;
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
differ++;
}
}
if (differ == 0) {
list.add(0);
}
//准备移动滑动窗口
for (int i = 0; i < sLen - pLen; i++) {
char c = s.charAt(i);
//马上会从滑动窗口中移出一个字符c
//如果原来滑动窗口中字符c的数量比字符串p中字符c的数量多1,那么移动以后就会刚好不差
if (counts[c - 'a'] == 1) {
differ--;
} else if (counts[c - 'a'] == 0) {
//如果原来刚好不差,那么移动以后就会少一个
differ++;
}
//滑动窗口向右移动一个字符,原来位置i上的字母被移出,所以
counts[c - 'a']--;
//马上会进来一个字符c
c = s.charAt(i + pLen);
//如果原来字符串p比它多了一个字符c,那么移动以后就会刚好不差
if (counts[c - 'a'] == -1) {
differ--;
} else if (counts[c - 'a'] == 0) {
differ++;
}
counts[c - 'a']++;
if (differ == 0) {
list.add(i + 1);
}
}
return list;
}
}
(简单)349. 两个数组的交集
我的思路:首先将两个数组nums1和nums2中的数分别放到set1和set2中去重,然后再遍历size较小的那个set,判断两个set是否有交集。因为不确定它们是否有交集,如果有的话,交集有多大都不能确定,则先使用ArrayList来存放交集中的元素,最后再用数组保存。
import java.util.ArrayList;
import java.util.HashSet;
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
HashSet<Integer> set1 = new HashSet<>();
HashSet<Integer> set2 = new HashSet<>();
ArrayList<Integer> list = new ArrayList<>();
for (int num : nums1) {
set1.add(num);
}
for (int num : nums2) {
set2.add(num);
}
if (set1.size() < set2.size()) {
for (Integer num : set1) {
if (set2.contains(num)) {
list.add(num);
}
}
} else {
for (Integer num : set2) {
if (set1.contains(num)) {
list.add(num);
}
}
}
int[] ans = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
ans[i] = list.get(i);
}
return ans;
}
}
复杂度分析:
- 时间复杂度:O(m+n),其中m和n分别是两个数组的长度。使用两个集合分别存储两个数组中的元素需要O(m+n)的时间,遍历较小的集合并判断元素是否在另一个集合中需要O(min(m,n))的时间,因此总时间复杂度是O(m+n)
- 空间复杂度:O(m+n),其中m和n分别是两个数组的长度。空间复杂度取决于两个集合。
官方提供的其他思路,排序+双指针
如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。
首先对两个数组进行排序,然后使用两个指针遍历两个数组。可以预见的是加入答案的数组的元素一定是递增的,为了保证加入元素的唯一性,需要额外记录变量pre表示上一次加入答案数组的元素。
初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数组的指针右移一位,如果两个数字相等,且该数字不等于pre,将该数字添加到答案并更新pre变量,同时将两个指针右移一位。当至少有一个指针超出数组范围时,遍历结束。
import java.util.ArrayList;
import java.util.Arrays;
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int p1 = 0;
int p2 = 0;
int len1 = nums1.length;
int len2 = nums2.length;
int pre = -1;
ArrayList<Integer> list = new ArrayList<>();
while (p1 < len1 && p2 < len2) {
if (nums1[p1] != nums2[p2]) {
if (nums1[p1] < nums2[p2]) {
p1++;
} else {
p2++;
}
} else {
if (nums1[p1] != pre) {
list.add(nums1[p1]);
pre = nums1[p1];
}
p1++;
p2++;
}
}
int[] ans = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
ans[i] = list.get(i);
}
return ans;
}
}
复杂度分析:
- 时间复杂度:O(mlogm+nlogn),其中m和n分别是两个数组的长度。对数组排序的时间复杂度是O(mlogm)和O(nlogn),双指针寻找交集元素的时间复杂度是O(m+n),因此,总的时间复杂度是O(mlogm+nlogn)
- 空间复杂度:O(logm+logn),其中m和n分别是两个数组的长度。空间复杂度主要取决于排序使用的额外空间。
(简单)350. 两个数组的交集||
我的思路:
首先,让nums1数组是长度较短的那个数组,nums2是长度较长的那个数组。
然后使用HashMap来记录nums2中的元素和元素出现的次数
再循环遍历nums1,当HashMap中存在nums1中的某元素,且次数大于0,则说明该元素是nums1和nums2的交集,将该元素加入ArrayList,HashMap中该元素的次数减一
最后将ArrayList中的值赋值到一个数组中,返回
import java.util.ArrayList;
import java.util.HashMap;
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
return intersect(nums2, nums1);
}
ArrayList<Integer> list = new ArrayList<>();
HashMap<Integer, Integer> map = new HashMap<>();
for (int num : nums1) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for (int num : nums2) {
if (map.containsKey(num) && map.get(num) > 0) {
list.add(num);
map.put(num, map.get(num) - 1);
}
}
int[] ans = new int[list.size()];
for (int i = 0; i < ans.length; i++) {
ans[i] = list.get(i);
}
return ans;
}
}
复杂度分析:
- 时间复杂度:O(m+n),其中m和n分别是两个数组的长度。需要遍历两个数组对哈希表进行操作,哈希表操作的时间复杂度是O(1),因此,总时间复杂度与两个数组的长度和呈线性关系
- 空间复杂度:O(min(n,m)),其中m和n分别是两个数组得到长度。对于较短的数组进行哈希表的操作,哈希表的大小不会超过较短的哈希表的长度。
我的另一种思路:是在看了349的官方题解以后,觉得这一题也可以用排序+双指针的做法来解题,和349题不同的地方在于这一题中不需要变量来记录上一次加入集合的是什么数字,因为这一题的答案需要包含重复的元素
import java.util.ArrayList;
import java.util.Arrays;
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int p1 = 0;
int p2 = 0;
int len1 = nums1.length;
int len2 = nums2.length;
ArrayList<Integer> list = new ArrayList<>();
while (p1 < len1 && p2 < len2) {
if (nums1[p1] < nums2[p2]) {
p1++;
} else if (nums1[p1] > nums2[p2]) {
p2++;
} else {
list.add(nums1[p1]);
p1++;
p2++;
}
}
int[] ans = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
ans[i] = list.get(i);
}
return ans;
}
}
复杂度分析:
- 时间复杂度:O(mlogm+nlogn),其中m和n分别是两个数组的长度。对两个数组进行排序的时间复杂度是O(mlogm+nlogn),遍历两个数组的时间复杂度是O(m+n),因此总的时间复杂度是O(mlogm+nlogn)
- 空间复杂度:O(min(m,n)),其中m和n分别是两个数组的长度。其中将符合的元素放在ArrayList中,ArrayList的长度不会超过较短的数组的长度。
如果nums2的元素存储在磁盘上,磁盘内存是有限的,并且你不能一次加载所有的元素到内存中。那么就无法对nums2进行排序,因此推荐使用哈希表的思路。在思路一中,nums2只关系到查询操作,因此每次读取nums2中的一部分数据,并进行处理。
(*简单)202. 快乐数
哈希表方法的复杂度分析
快慢指针的复杂度分析
我的思路:solve(n)方法就是计算数字每一位上的平方的和,将该结果赋值给n,一直到n<10为止,在判断n是否等于1,等于1则表示n是快乐数,在第一次提交的时候倒数第二个测试用例1111111
没对,手算以后发现,如果多次迭代后,n=7,那么原始的n也是快乐数
1+1+1+1+1+1+1=7
7^2 = 49
4^2+9^2=97
9^2+7^2=130
1^2+3^2+0^2 = 10
class Solution {
public boolean isHappy(int n) {
do {
n = solve(n);
} while (n >= 10);
return n == 1 || n == 7;
}
public int solve(int n) {
int sum = 0;
while (n != 0) {
sum += ((n % 10) * (n % 10));
n /= 10;
}
return sum;
}
}
其他思路,用哈希集合检测循环
49这个数是一个快乐数
116这个数不是快乐数
那是否会存在值越来越大,最后接近无穷大的情况?
数字位数 | 最大值 | 下一个 |
---|---|---|
1 | 9 | 81 |
2 | 99 | 162 |
3 | 999 | 243 |
4 | 9999 | 342 |
13 | 9999999999999 | 1053 |
对于3位数的数组,他不可能大于243。这意味着它要么被困在243以下的循环内,要么跌到1。4位或以上的数字在每一步都会丢失一位,直到降到3位为止。所以,最坏情况可能在243以下的所有数字上循环,然后回到它已经到过的一个循环或者回到1。但它不会无限期地进行下去,所以,不存在最后接近无穷大的情况。
所以,需要解决两个问题:
- 给一个数字n,计算它的下一个数字是什么
- 判断该数字是否已经进入循环,也就是该数字之前是否出现过,用哈希集合完成
- 如果它不在哈希集合中,则添加该数字到哈希集合中
- 如果它在哈希集合中,这就意味着我们处于一个循环中,此时返回false
使用哈希集合而不是向量、列表或数组的原因是因为需要反复检查其中是否存在某数组。检查数字是否在哈希集合中需要O(1)的时间,而对于其他数据结构,则需要O(n)的时间。选择正确的数据结构是解决这些问题的关键部分。
import java.util.HashSet;
class Solution {
public boolean isHappy(int n) {
HashSet<Integer> set = new HashSet<>();
while (n != 1 && !set.contains(n)) {
set.add(n);
n = getSum(n);
}
return n == 1;
}
public int getSum(int n) {
int sum = 0;
while (n != 0) {
sum += ((n % 10) * (n % 10));
n /= 10;
}
return sum;
}
}
其他思路:快慢指针法
通过反复调用getNext(n)得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但是数据仍然形成链表结构。起始数字是链表的头“节点”,链中的所有其他数字都是节点。next指针是通过调用getNext(n)函数获得。
意识到实际有个链表,那么问题就可以转换为检测一个链表是否有环。因此,在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑得快,一个跑得慢。
不管乌龟还是兔子在循环中从哪里开始,它们最终都会相遇。因为兔子每走一步,就向乌龟靠近一个节点。
算法:
不止跟踪链表中的一个值,而是跟踪两个值,一个由fast指针指向,另一个由slow指针指向,如果n是一个快乐数,即没有循环,那么fast指针会先到达数字1。如果n不是一个快乐数,那么fast指针和slow指针会在同一数字上相遇。
class Solution {
public boolean isHappy(int n) {
int slow = n;
int fast = getSum(n);
while (fast != 1 && slow != fast) {
fast = getSum(getSum(fast));
slow = getSum(slow);
}
return fast == 1;
}
public int getSum(int n) {
int sum = 0;
while (n != 0) {
sum += ((n % 10) * (n % 10));
n /= 10;
}
return sum;
}
}
(*简单)1. 两数之和
暴力法,二重循环
class Solution {
public int[] twoSum(int[] nums, int target) {
//暴力法
for (int i = 0; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] + nums[j] == target) {
return new int[]{i, j};
}
}
}
return new int[]{-1, -1};
}
}
另一种思路,哈希表法
上一种思路的时间复杂度较高,原因是寻找target-x的时间复杂度过高。因此,需要一种更优的方法能够快速寻找数组中是否存在目标元素,如果存在,则找出它的索引。
使用哈希表,可以将寻找target-x的时间复杂度从降低O(N)到O(1)。
创建一个哈希表,对于每一个x,首先查询哈希表中是否存在target-x,将x插入到哈希表中,即可保证不会让x和自己匹配。
import java.util.HashMap;
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (!map.containsKey(target - nums[i])) {
map.put(nums[i], i);
} else {
return new int[]{i, map.get(target - nums[i])};
}
}
return new int[]{-1, -1};
}
}
(中等)454. 四数相加 ||
将四个数组分成两部分,A和B为一组,C和D为另外一组。
对于A和B,使用二重循环对它们进行遍历,得到所有A[i]+B[j]的值并存入哈希映射中。对于哈希映射中的每个键值对,每个键表示一种A[i]+B[j],对应的值为A[i]+B[j]出现的次数。
对于C和D,我们同样使用二重循环对它们进行遍历。当遍历到C[k]+D[l]时,如果 -(C[k]+D[l]) 出现在哈希映射中,那么将 -(C[k]+D[l]) 对应的值累加近答案中。
最终即可得到满足A[i]+B[j]+C[k]+D[l]=0的四元素数目
一开始在写代码的时候,把cnt += map.get(-nums3[i] - nums4[j]) 写成了cnt++,如果有多个nums1[i]和nums2[j]的值相加是相等的,就代表了不同的组合,不能直接cnt++
import java.util.HashMap;
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int n = nums1.length;
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
map.put(nums1[i] + nums2[j], map.getOrDefault(nums1[i] + nums2[j], 0) + 1);
}
}
int cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (map.containsKey(-nums3[i] - nums4[j])) {
cnt += map.get(-nums3[i] - nums4[j]);
}
}
}
return cnt;
}
}
(*中等)15. 三数之和
题目要求找到所有【不重复】且【和为0】的三元组
需要三重循环,依次遍历,再使用哈希表去重,得到不包含重复三元组的最终答案,这种做法时间和空间的复杂度都很高。
可以做一点改变:
- 第二重循环枚举到的元素不小于当前第一重循环枚举到的元素
- 第三重循环枚举到的元素不小于当前第二重循环枚举到的元素
那么枚举三元组a<=b<=c,保证了只有(a,b,c)这个顺序能够被枚举到。为了实现这一点,将数组中的元素从小到大排序。
如果固定了前两重循环枚举到的元素a和b,那么只有唯一的c满足a+b+c=0,当二重循环往后继续枚举b以后,那么满足等式的c会相应的减小,也就是说,我们可以从小到大枚举b,同时从大到小枚举c,即第二重循环和第三重循环实际上是并列的关系。
有了这样的发现,可以保持第二重循环不变,而将第三重循环变成一个从数组最右边开始向左移动的指针。
这个方法就是双指针方法,当需要枚举数组的两个元素时,如果发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从O(N^2)减少至O(N)。因为在枚举的过程每一步中,【左指针】会向右移动一个为止(也就是题目中的b),而【右指针】会想做移动若干位置,这个与数组的元素有关,但知道它一共会移动的位置数为O(N),均摊下来,也就是每次也向左移动一个位置,因此时间复杂度是O(N)。
import java.util.*;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
//注意,这个变量k要写在第二层for循环的外面
//如果写在里面的话,那么j每加1,k都要从数组的最后一个元素重新遍历,导致执行时间能达到1000ms以上
int k = n - 1;
int target = -nums[i];
for (int j = i + 1; j < n; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
while (j < k && nums[j] + nums[k] > target) {
k--;
}
if (j == k) {
break;
}
if (nums[j] + nums[k] == target) {
ArrayList<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[k]);
res.add(list);
}
}
}
return res;
}
}
复杂度分析:
- 时间复杂度:O(N^2),其中N是数组nums的长度
- 空间复杂度:O(logN),忽略了存储答案的空间,额外的排序的空间复杂度为O(logN)。然而我们修改了输入的数组nums,在实际情况下不一定允许,因此也可以看成使用了一个额外的数组存储了nums的副本并进行排序,空间复杂度为O(N)。
代码随想录提供的思路,使用双指针
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
//题目要求不重复,那么就让三个数按从小到大的顺序输出,就不会重复
//首先对数组进行排序
Arrays.sort(nums);
int n = nums.length;
ArrayList<List<Integer>> ans = new ArrayList<>();
//假设a+b+c=0
for (int i = 0; i < n - 2; i++) {
//如果排序后的第一个元素已经大于0,那么后面的数字无论怎么组合,都不可能凑成一个合格的三元组
//直接返回答案即可
if (nums[i] > 0) {
return ans;
}
//去除重复的数字a
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
ans.add(Arrays.asList(nums[i], nums[left], nums[right]));
//去重逻辑应该放在找到一个三元组之后,对b和c去重
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
right--;
}
}
}
return ans;
}
}
(中等)18. 四数之和
可以借鉴三数之和中,代码随想录提供的思路,首先对数据进行排序,然后固定前两个数字,后两个数字的确定使用双指针的方法,但是每一个nums[i]的最大值是10^9,所以四个数相加会超过int的范围,所以需要使用long型进行接收
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
int n = nums.length;
ArrayList<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < n; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
long sum = (long)nums[i] + (long)nums[j] + (long)nums[left] + (long)nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.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 result;
}
}
对于上面的代码,在看了官方提供的代码之后,对上面代码进行了一些修改,多加了一些判断条件
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
ArrayList<List<Integer>> result = new ArrayList<>();
if (nums.length < 4) {
return result;
}
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 3; i++) {
//下面这两个if,确定范围别边界,看看从i开始的四个数字的和 和 target的大小关系
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
//判断当前nums[i]和最右边三个数的和 和 target的大小关系
if ((long) nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target) {
continue;
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < n - 2; j++) {
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
if ((long) nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target) {
continue;
}
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.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 result;
}
}