当遇到需要快速判断一个元素是否出现在集合里面的时候,可以考虑哈希法,牺牲一定的空间换取查找的时间。
java常用的哈希表有HashMap、HashSet以及用数组去模拟哈希,这几种方法各有优劣。
数组模拟哈希
数组模拟需要在一开始就确定大小,如果key的值域范围很大,但是key的数量很少,就要开辟很大的数组空间存很少的key,造成空间的浪费。不过数组相比HashMap的优势在于map可能需要维护红黑树或者链表,而且还需要计算哈希函数,当数据量比较大的时候数组更能节省时间。
HashSet
基于 HashMap 来实现、无序(不会记录插入的顺序,可以用LinkedHashMap实现有序插入,也可以用TreeSet实现插入后元素的排序)、允许有 null 值、不允许有重复元素、不是线程安全的(用ConcurrentHashMap实现线程安全)。
HashMap
存储的内容是键值对(key-value)映射、最多允许一条记录的键为 null、无序(即不会记录插入的顺序)、不支持线程同步
原题链接
题目数据规定为小写字母,因此可以开辟一个26长度的数组模拟哈希。统计s每个字母出现的次数,再遍历t字符串,出现对应字母就将模拟的哈希数组对应数值-1。
代码如下:
class Solution {
public boolean isAnagram(String s, String t) {
int[] hash=new int[26];
for(int i=0;i<s.length();i++){
++hash[s.charAt(i)-'a'];
}
for(int i=0;i<t.length();i++){
--hash[t.charAt(i)-'a'];
}
for(int i=0;i<26;i++){
if(hash[i]!=0)return false;
}
return true;
}
}
原题链接
这题具体解题思路在力扣刷题记录-双指针解决数组问题中的滑动窗口部分。
代码如下:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen=s.length(),pLen=p.length();
List<Integer> res=new ArrayList<>();
int[] need=new int[26];
int[] window=new int[26];
int count=0;
for(int i=0;i<pLen;i++)++need[p.charAt(i)-'a'];
for(int i=0;i<26;i++){
if(need[i]!=0)count++;
}
int wStart=0,wEnd=0;
int valid=0;//window中能与need数量一样的字母数
while(wEnd<sLen){
char c=s.charAt(wEnd++);
//首先确定加入窗口的这个字符有没有在p中
if(need[c-'a']!=0){//如果该字符在p中
++window[c-'a'];//窗口内该字符数量+1
if(window[c-'a']==need[c-'a'])++valid;//当数量加到与p中相同
}
//符合题目要求的窗口长度一定和p长度相同,当长度超过时,需要从左边缩小
if(wEnd-wStart>pLen){
char l=s.charAt(wStart++);
//左边界字符要在p中,才需要修改窗口中有关异位词的数据
if(need[l-'a']!=0){
//如果移出窗口的字符在窗口中时,该字符数量和p中相同
//那么其移出之后,符合条件的字符数量-1
if(window[l-'a']==need[l-'a'])
--valid;
--window[l-'a'];//窗口内该字符数量-1
}
}
//只有长度相同,并且符合要求的字符数量相同时,窗口内才是异位词
if(wEnd-wStart==pLen&&valid==count)res.add(wStart);
}
return res;
}
}
原题链接
代码如下:
/**
这题考察的还是异位词的概念,ransomNote能由magazine里面的字符构成,一定要满足magazine中拥有ransomNote中的所有字符(magazine只会更多,而且可能有别的字符,即ransomNote中没有的字符),因此只需要判断magazine中,同ransomNote一样的字符的数量是否超过ransomNote即可。
*/
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int[] hash=new int[26];
for(char c:ransomNote.toCharArray()){
++hash[c-'a'];
}
for(char c:magazine.toCharArray()){
--hash[c-'a'];
}
for(int i=0;i<26;i++){
//如果magazine字符减完还有剩,说明其无法覆盖ransomNote
if(hash[i]>0)return false;
}
return true;
}
}
原题链接
查找所有单词的共用字符,其实就是将所有当次拆分成一个个字符,统计单词中所有字符的出现次数,找到所有单词中各个字符出现的最小次数。
代码如下:
class Solution {
public List<String> commonChars(String[] words) {
int[] hash=new int[26];
//遍历第一个单词,统计字母出现次数作为初始标准
for(char c:words[0].toCharArray()){
++hash[c-'a'];
}
for(int i=1;i<words.length;i++){
//统计其它单词字母出现次数
int[] otherWordHash=new int[26];
for(char c:words[i].toCharArray()){
++otherWordHash[c-'a'];
}
for(int j=0;j<26;j++){
//对所有单词,取各个字母出现的最小次数
hash[j]=Math.min(hash[j],otherWordHash[j]);
}
}
List<String> res=new ArrayList<>();
for(int i=0;i<26;i++){
//注意这里是while,字母出现次数可能会大于1,只要有几次就要返回几个
while(hash[i]!=0){
res.add(String.valueOf((char)(i+'a')));
--hash[i];
}
}
return res;
}
}
原题链接
可以使用数组模拟,也可以利用HashSet的不允许有重复元素的特点。
解法一(数组模拟):
//用两个数组模拟哈希,时间O(m+n+1001),空间O(1001)
//如果数组中数字值非常大,就不适合用这个,可以使用HashSet
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1=new int[1001];//数字范围0-1000
for(int num:nums1){
++hash1[num];
}
int[] hash2=new int[1001];
for(int num:nums2){
++hash2[num];
}
List<Integer> list=new ArrayList<>();
for(int i=0;i<1001;i++){
//当两个数组都有这个数字时
if(hash1[i]!=0&&hash2[i]!=0){
list.add(i);
}
}
int n=list.size();
int[] res=new int[n];
for(int i=0;i<n;i++){
res[i]=list.get(i);
}
return res;
}
}
解法二:(HashSet)
//用两个HashSet,因为HashSet基于 HashMap 来实现的,是一个不允许有重复元素的集合
//时间O(m+n),其中 m和n分别是两个数组的长度。使用两个集合分别存储两个数组中的元素需要
//O(m+n) 的时间,遍历较小的集合并判断元素是否在另一个集合中需要 O(min(m,n)) 的时间
//因此总时间复杂度是 O(m+n)
//空间空间复杂度:O(logm+logn),其中m和n分别是两个数组的长度。
//空间复杂度主要取决于排序使用的额外空间。
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new HashSet<>();
for(int num:nums1){
set1.add(num);
}//set1中无重复元素
for(int num:nums2){
if(set1.contains(num)){//set1中有nums2的数字,就是交集元素
set2.add(num);//set2存储交集元素
}
}
int[] res = new int[set2.size()];
int index=0;
for(int num:set2){
res[index++] = num;
}
return res;
}
}
原题链接
2023.05.30 三刷
思路:
1.题目只要求长度,可以用maxLen来记录遍历过程中的最大长度;
2.利用滑动窗口,窗口内的字符不重复,那么窗口大小就是不含有重复字符的最大长度
3.如何保证窗口内字符不重复?–可以用数组模拟hash,用来记录窗口内各种字符的数量
4.窗口扩张–模拟hash数组对应+1,窗口长度+1,窗口右边界+1
5.窗口收缩–当前面扩张时进入窗口的字符数量大于1收缩,窗口长度-1,模拟hash数组对应-1,左边界+1.
代码如下:
//时间O(n),空间O(字符集大小,一般为128)
class Solution {
public int lengthOfLongestSubstring(String s) {
int maxLen=0;//最大长度
int[] hash=new int[128];
int l=0,r=0;
int sLen=s.length();
while(r<sLen){
char c=s.charAt(r++);//窗口左闭右开(一开始r++)
++hash[c];
while(hash[c]>1){
char cc =s.charAt(l++);
--hash[cc];
}
maxLen=maxLen>r-l ? maxLen:r-l;//收缩之后窗口才符合没有重复字符的要求
}
return maxLen;
}
}
原题链接
这题主要考察对HashMap的使用,有两种方法。
第一种:对每个单词转成字符数组,再利用Arrays.sort()方法进行排序,再将排序后的字符数组转回字符串,用排序后的字符串作为hashmap的key值进行映射,value值为最初的单词字符串。
时间复杂度:O(nklogk),n为字符串数量,k为字符串最大长度,需要遍历n个字符串,对于每个字符串需要klogk的时间进行排序,哈希表更新时间复杂度为O(1),总时间复杂度为O(nklogk);
空间复杂度:O(nk),需要用哈希表存下所有的字符串。
第二种:统计单词中每个字母出现次数,并且把字母从小到大,每个字母后面跟着这个字母在这个单词中出现的次数,拼接成字符串,如acbccb–>a1b2c3。这样拼接后的字符串作为hashmap的key值,去存储value
解法一代码如下:
//解法1
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
//key存储
HashMap<String,List<String>> hashmap =new HashMap<>();
for(String str:strs){
char[] c=str.toCharArray();
Arrays.sort(c);//将字符数组按字母顺序重新排序
//重新排序后的字符数组转换为字符串,作为key值
String key=new String(c);
//需要获取这个key值对应的list,可能为空,为空就新创一个List
List<String> list= hashmap.getOrDefault(key,new ArrayList());
list.add(str);//这个str对应的是当前的key值,吧str加入key值对应的list
hashmap.put(key,list);//再把list放回hashmap
}
List<List<String>> res=new ArrayList<List<String>>();
for(List value:hashmap.values()){
res.add(value);
}
return res;
//上面的操作等价于:return new ArrayList>(hashmap.values());
}
}
解法二代码如下:
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
//key为单词中字母及其出现的次数拼接而成的字符串
HashMap<String,List<String>> hashmap =new HashMap<>();
for(String str:strs){
int n=str.length();
int[] count=new int[26];//存储当前字符串中各字母出现次数
for(int i=0;i<n;i++){
++count[str.charAt(i)-'a'];
}
//接下来需要拼接字符串,为了效率,使用StringBuilder
StringBuilder sb=new StringBuilder();
for(int i=0;i<26;i++){
if(count[i]!=0){
sb.append((char)(i+'a'));
sb.append(count[i]);
}
}
String key=sb.toString();
//需要获取这个key值对应的list,可能为空,为空就新创一个List
List<String> list= hashmap.getOrDefault(key,new ArrayList<String>());
list.add(str);//这个str对应的是当前的key值,吧str加入key值对应的list
hashmap.put(key,list);//再把list放回hashmap
}
List<List<String>> res=new ArrayList<List<String>>();
for(List value:hashmap.values()){
res.add(value);
}
return res;
//上面的操作等价于:return new ArrayList>(hashmap.values());
}
}
原题链接
2023/06/02 三刷
原题链接
2023/06/02 三刷
这题是相对复杂的滑动窗口题,掌握之后,套用模板再做后面的题就会比较容易了。
主要需要解决的问题:
1、什么时候应该移动 right 扩大窗口?窗口加入字符时,应该更新哪些数据?
2、什么时候窗口应该暂停扩大,开始移动 left 缩小窗口?从窗口移出字符时,应该更新哪些数据?
3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
关于窗口边界左闭右开的选择理由:
理论上可以设计两端都开或者两端都闭的区间,但设计为左闭右开区间是最方便处理的。因为这样初始化 left = right = 0 时区间 [0, 0) 中没有元素,但只要让 right 向右移动(扩大)一位,区间 [0, 1) 就包含一个元素 0 了。如果设置为两端都开的区间,那么让 right 向右移动一位后开区间 (0, 1) 仍然没有元素;如果你设置为两端都闭的区间,那么初始区间 [0, 0] 就包含了一个元素。这两种情况都会给边界处理带来不必要的麻烦。
代码如下:
//1.HashMap写法
class Solution {
public String minWindow(String s, String t) {
/*注意:里面value存储的类型是包装类,比较value时不能用==,而是要用equals*/
Map<Character,Integer> need=new HashMap<>();//存储t串字符,及对应字符出现次数
Map<Character,Integer> window=new HashMap<>();//存储窗口内字符,及对应出现次数
/** 最开始要先遍历t串,统计每个字符出现的次数(以键值对形式存储)*/
for(char c:t.toCharArray()){
//getOrDefault(c,0)作用:如果c这个键上有值,则获取其value;若无值,则赋0
//后面补上的+1则表示,遍历到c这个字符,就将c对应的value+1;
need.put(c,need.getOrDefault(c,0)+1);
}
int valid=0;//记录窗口中,符合需求的字符的个数
int wStart=0,wEnd=0;//窗口边界,左闭右开[start,end),初始[0,0)为空
int wLen=100001;//记录窗口大小
int subStart=0;//最后返回的子串的边界(substring,左闭右开)
/**开始滑动窗口代码 */
while(wEnd<s.length()){
/**①窗口扩张,右边界移动 */
char c=s.charAt(wEnd);//记录新加入窗口内的字符
wEnd++;//
/**②接下来更新窗口内数据*/
//判断当前字符c是不是t串中需要的,如果字符c是t串中需要的
if(need.containsKey(c)){
//就要把窗口内对应的字符数量+1
window.put(c,window.getOrDefault(c,0)+1);
//加完后需要看这个字符的数量达到要求了没,达到了说明满足要求的字符数量+1
if(window.get(c).equals(need.get(c)))valid++;
}
/**③然后就需要考虑窗口缩小问题了,即当窗口内元素符合要求时,左边界前进 */
//当valid值达到need中包含的字符数量时,说明窗口已经涵盖了t所有字符了
while(valid==need.size()){
//记录下当前窗口大小,如果更小
//本来wEnd-wStart+1才是窗口长度,但是在最前面wEnd已经向前了(左闭右开)
//所以这里不用+1就是真实窗口长度
if(wEnd-wStart<wLen){
wLen=wEnd-wStart;//就记录更小的窗口长度
subStart=wStart;//并且记录更小字串的起始位置(方便最后返回字串)
}
char l=s.charAt(wStart++);//记录下当前窗口左边界字符,然后窗口左边界缩小
/**④开始更新缩小后的窗口内数据 */
//窗口内字符可能不是t中需要的,就不用对window特别处理,是t需要的才进行处理
if(need.containsKey(l)){
//只有当window中字符l与need中字符l个数相同,去掉l才会导致valid-1
if(window.get(l).equals(need.get(l)))valid--;
//要去掉的l字符是t需要的,就要在window中将该字符的value-1
window.put(l,window.get(l)-1);
}
}
}
return wLen == 100001 ? "" : s.substring(subStart,subStart+wLen);
}
}
此外可以用数组模拟hash方法,思路与HashMap一样。代码如下:
// 2.数组模拟hash,字符集大小为k,时间O(tLen+sLen+k),空间O(k)
class Solution {
public String minWindow(String s, String t) {
int sLen=s.length(),tLen=t.length();
int[] need=new int[128];
for(int i=0;i<tLen;i++){
char c=t.charAt(i);
++need[c];
}
int tCount=0;
for(int i=0;i<128;i++){
if(need[i]!=0)++tCount;
}
int[] window=new int[128];
int wStart=0,wEnd=0,wCount=0;
int subLen=100001,subStart=0;
while(wEnd<sLen){
char r=s.charAt(wEnd++);
if(need[r]!=0){
++window[r];
if(window[r]==need[r])wCount++;
}
while(wCount==tCount){
if(wEnd-wStart<subLen){
subStart=wStart;
subLen=wEnd-wStart;
}
char l=s.charAt(wStart++);
if(need[l]!=0){
if(window[l]==need[l]){
--wCount;
}
--window[l];
}
}
}
return subLen==100001 ? "" : s.substring(subStart,subStart+subLen);
}
}
原题链接
这题对空间的要求会更严格一些,所以无法用数组模拟hash的方法解题。
解法一:
//解法一:HashMap方法
//时间:O(m+n),m、n分别为nums1和nums2长度(需要对nums1和nums2分别遍历一次,对nums2遍历中查询hashmap操作时间复杂度为O(1))
//空间:O(min(m,n)),只需要长度最小的数组的空间的hashmap
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
//始终让nums1位更短的数组
if(nums1.length>nums2.length){
return intersect(nums2,nums1);
}
Map<Integer,Integer> hashmap =new HashMap<>();
for(int num:nums1){//先遍历更短的数组,存入hashmap
hashmap.put(num,hashmap.getOrDefault(num,0)+1);
}
//res长度不能定义为hashmap.size(),有可能发生只有一个键,但是值很大的情况
//res长度定义为nums1长度,nums1为更短的数组,结果数组长度不会超过nums1的长度
int[] res=new int[nums1.length];
int index=0;
for(int num:nums2){
//只有当nums2元素在hashmap(nums1)中,且对应个数超过0,才记录这个元素
if(hashmap.getOrDefault(num,0)>0){
res[index++]=num;
hashmap.put(num,hashmap.get(num)-1);
}
}
//res为nums1长度,实际交集元素个数可能达不到,只要截取实际存入的那部分即可
return Arrays.copyOfRange(res,0,index);
}
}
解法二:
//解法二:先排序+双指针
//时间O(mlogm+nlogn):对1和2数组排序O(mlogm+nlogn),再双指针遍历O(min(m,n))
//空间O(min(m,n))
class Solution {
public int[] intersect(int[] nums1, int[] nums2) {
int m=nums1.length,n=nums2.length;
Arrays.sort(nums1);
Arrays.sort(nums2);
int index=0,index1=0,index2=0;
int[] res=new int[Math.min(m,n)];
//不相等时,小的前进一步,相等时,添加进结果数组,并且所有指针前进
//只要有一个数组遍历完就要结束
while(index1<m&&index2<n){
if(nums1[index1]==nums2[index2]){
res[index++]=nums1[index1];
++index1;
++index2;
}else if(nums1[index1]<nums2[index2]){
++index1;
}else if(nums1[index1]>nums2[index2]){
++index2;
}
}
return Arrays.copyOfRange(res,0,index);
}
}
如果给定的数组已经排好序呢?你将如何优化你的算法?
---- 双指针法
如果 nums1的大小比 nums2 小,哪种方法更优?
---- HashMap法,无序排序,只要分别遍历
如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
---- 如果nums2的元素存储在磁盘上,磁盘内存是有限的,并且不能一次加载所有的元素到内存中。那么就无法高效地对nums2进行排序,因此推荐使用方法一而不是方法二。在方法一中,nums2只关系到查询操作,因此每次读取 nums2中的一部分数据,并进行处理即可。
原题链接
用key存元素的大小,value存元素下标
在遍历数组过程中,去hashmap中查询有没有和当前元素匹配的key,如果有,就可以配对,把各自下标存入res。
代码如下:
//时间O(n),空间O(n)
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] res=new int[2];
Map<Integer,Integer> hashmap=new HashMap<>();
for(int i=0;i<nums.length;i++){
int tmp=target-nums[i];
if(hashmap.containsKey(tmp)){
res[0]=hashmap.get(tmp);
res[1]=i;
}
hashmap.put(nums[i],i);
}
return res;
}
}
原题链接
2023.05.31 一刷
前缀和+HashMap
遍历数组nums,计算从第0个元素到当前元素nums[i]的和,用哈希表保存出现过的累积和preSum的次数。如果preSum - k在哈希表中出现过,则代表从当前下标i往前有连续的子数组的和为k。
时间:只需要遍历nums一次–O(n)–用时22ms,击败89.7%
空间:需要用hashmap存储前缀和–O(n)–内存消耗44.9MB,击败48.71%
//前缀和+HashMap
class Solution {
public int subarraySum(int[] nums, int k) {
// key存前缀和,value存对应前缀合出现的次数
HashMap<Integer,Integer> hashmap=new HashMap<>();
int preSum=0;
int count=0;
hashmap.put(0,1);//这句很重要,原因看下面注释
for(int i=0;i<nums.length;i++){
// preSum记录nums[0~i]之间的和
preSum+=nums[i];
// preSum[i]-preSum[j-1]=k,包含preSum-k键值对说明nums[j~i]的区间和为k,此时需要看0~i之间有多少次前缀和为preSum[j-1],count加上对应次数即可
// put(0,1)补上了nums[0~i]区间和为k的情况(preSum=k),此时count+1
if(hashmap.containsKey(preSum-k)){
count+=hashmap.get(preSum-k);
}
hashmap.put(preSum,hashmap.getOrDefault(preSum,0)+1);
}
return count;
}
}
原题链接
注意这题的四个数来自四个独立的数组,和15.三数之和以及18.四数之和不一样
如果暴力4层for循环遍历四个数组会超时间,可以把ABCD分成两组,AB一组,CD一组
用两层for循环遍历AB数组,求A[i]+B[j],将A[i]+B[j]作为key值,它们出现的次数作为value值,每出现一次key值对应的value值就+1;
再用另外两层for循环遍历CD数组,求C[k]+D[l],在hashmap中寻找有没有【0-(C[k]+D[l])】这样的键,如果hashmap中存在这样的键,说明这它们组合起来相加为0,这时只要把A[i]+B[j]键对应的value值(A[i]+B[j]这样的值出现的次数)加入结果即可。
代码如下:
//分组+哈希:时间O(n^2),空间O(n^2)
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer,Integer> hashmap=new HashMap<>();
for(int num1:nums1){
for(int num2:nums2){
int tmp=num1+num2;
hashmap.put(tmp,hashmap.getOrDefault(tmp,0)+1);
}
}
int res=0;
for(int num3:nums3){
for(int num4:nums4){
int tmp=num3+num4;
if(hashmap.containsKey(0-tmp)){
res+=hashmap.get(0-tmp);
}
}
}
return res;
}
}
原题链接
题目中要求不能包含重复的三元组,所以就不能简单照搬454.四数之和Ⅱ的分组哈希做法
先将数组排序,用i作为索引遍历nums数组,对每一个i,left=i+1,right=nums.length-1;
left和right向中间收缩,当sum<0,说明当前三个数太小,nums[i]固定,只能增大left;同理sum>0,减小right。
另外在遍历的时候需要注意三元组的去重。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);//先排序才能用双指针
List<List<Integer>> res=new ArrayList<>();
for(int i=0;i<nums.length-2;i++){
//三元组第一个数都比0大,后面加上后两个数不可能等于0,所有后面的都不用考虑
if(nums[i]>0)break;
//当前数和前一个一样,那么得到的三元组也会和前一个数得到的三元组一样,直接跳过
if(i>0&&nums[i]==nums[i-1])continue;//去重
int left=i+1,right=nums.length-1;
while(left<right){
int sum=nums[i]+nums[left]+nums[right];
if(sum==0){
//符合条件,加入res,索引向中间移动
res.add(Arrays.asList(nums[i],nums[left++],nums[right--]));
//如果nums[left]和nums[left-1]一样,得到的三元组也会一样
//为了去重,直接跳过当前这个数。但是要在left
while(left<right&&nums[left]==nums[left-1])left++;
while(left<right&&nums[right]==nums[right+1])right--;
}else if(sum<0){
left++;
}else if(sum>0){
right--;
}
}
}
return res;
}
}
原题链接
其实就是在三数之和的基础上,再多一个指针j,三数之和中是nums[i]为确定值,这题里面就用nums[i]+nums[j]作为确定值,然后再利用首尾两个指针left和right向中间收缩。
中间有一些剪枝以及去重操作是需要注意的,可以很好提高代码效率
代码如下:
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res=new ArrayList<>();
Arrays.sort(nums);
int n=nums.length;
for(int i=0;i<n-3;i++){
//去重
if(i>0&&nums[i]==nums[i-1])continue;
//剪枝,当最小的4个数相加都超过,后面肯定找不到符合条件的
if((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target)break;
//剪枝,当当前最大的4个数都小于,当前nums[i]肯定不够,直接用下一个
if((long)nums[i]+nums[n-3]+nums[n-2]+nums[n-1]<target)continue;
for(int j=i+1;j<n-2;j++){
//与i同样道理,去重
if(j>i+1&&nums[j]==nums[j-1])continue;
//与i一样的道理,剪枝
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;
int left=j+1,right=n-1;
while(left<right){
//四个10亿相加会爆int
long sum=(long)nums[i]+nums[j]+nums[left]+nums[right];
if(sum==target){
res.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--;
}else if(sum<target){
left++;
}else if(sum>target){
right--;
}
}
}
}
return res;
}
}
原题链接
2023.05.28 一刷
思路:
想要找到以当前num为开始的最长序列,就判断从num开始,每次+1的数在不在set中,利用set.contains(curNum)方法可以在O(1)时间判断,直到curNum不在set为止,记录每个num对应的序列长度,每个num的序列长度与maxLen进行比较,取较大值。但是这样每个num都需要暴力遍历,时间复杂度高。
优化:
可以在遍历到每个num的时候,判断num-1在不在set中:
时间复杂度:O(n)。外层循环需要 O(n) 的时间复杂度,只有当一个数是连续序列的第一个数的情况下才会进入内层循环,然后在内层循环中匹配连续序列中的数,因此数组中的每个数只会进入内层循环一次。
空间复杂度:O(n)。HashSet存储数组中所有元素。
代码如下:
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> set=new HashSet<>();
//利用HashSet进行去重
for(int num:nums){
set.add(num);
}
//记录最大长度
int maxLen=0;
for(Integer num:set){
//如果num-1在set中,说明以num开始的序列不可能是最长的(从num-1开始的更长)
//如果num-1不在set中,则需要计算从num开始的序列的长度
if(!set.contains(num-1)){
int curLen=1;//目前以num开始的序列长度为1
int curNum=num;
//从num开始,每次+1,判断后面的数在不在,在的话就将序列长度+1
while(set.contains(curNum+1)){
++curLen;
++curNum;
}
maxLen=Math.max(curLen,maxLen);
}
}
return maxLen;
}
}
原题链接
法一:
//法一:HashSet,只要下一个数字在set中存在,说明有循环
//时间O(logn),空间O(logn)
class Solution {
public boolean isHappy(int n) {
Set<Integer> set=new HashSet<>();
//n=1就要返回true,set包含n说明无限循环了
while(n!=1&&!set.contains(n)){
set.add(n);
n=getNext(n);
}//退出循环有两种条件:n=1或set.contains(n)
return n==1;//如果n!=1,说明set.contains(n)
}
//获取n的下一个数字(逐位的平方和)
public int getNext(int n){
int sum=0;
while(n>0){
int mod=n%10;
sum+=mod*mod;
n/=10;
}
return sum;
}
}
法二:
//法二:快慢指针(判断有无环)
//时间O(logn),空间O(1)
class Solution {
public boolean isHappy(int n) {
int slow=n;
int fast=getNext(n);
//fast=1就要返回true,fast=slow说明存在逻辑上的环,会死循环
while(fast!=1&&fast!=slow){
slow=getNext(slow);
fast=getNext(getNext(fast));
}//退出循环有两种条件:fast=1或fast==slow
return fast==1;//如果fast!=1,说明fast==slow,有环
}
//获取n的下一个数字(逐位的平方和)
public int getNext(int n){
int sum=0;
while(n>0){
int mod=n%10;
sum+=mod*mod;
n/=10;
}
return sum;
}
}
原题链接
可以使用数组模拟,也可以利用HashSet的不允许有重复元素的特点。
解法一(数组模拟):
//用两个数组模拟哈希,时间O(m+n+1001),空间O(1001)
//如果数组中数字值非常大,就不适合用这个,可以使用HashSet
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
int[] hash1=new int[1001];//数字范围0-1000
for(int num:nums1){
++hash1[num];
}
int[] hash2=new int[1001];
for(int num:nums2){
++hash2[num];
}
List<Integer> list=new ArrayList<>();
for(int i=0;i<1001;i++){
//当两个数组都有这个数字时
if(hash1[i]!=0&&hash2[i]!=0){
list.add(i);
}
}
int n=list.size();
int[] res=new int[n];
for(int i=0;i<n;i++){
res[i]=list.get(i);
}
return res;
}
}
解法二:(HashSet)
//用两个HashSet,因为HashSet基于 HashMap 来实现的,是一个不允许有重复元素的集合
//时间O(m+n),其中 m和n分别是两个数组的长度。使用两个集合分别存储两个数组中的元素需要
//O(m+n) 的时间,遍历较小的集合并判断元素是否在另一个集合中需要 O(min(m,n)) 的时间
//因此总时间复杂度是 O(m+n)
//空间空间复杂度:O(logm+logn),其中m和n分别是两个数组的长度。
//空间复杂度主要取决于排序使用的额外空间。
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new HashSet<>();
for(int num:nums1){
set1.add(num);
}//set1中无重复元素
for(int num:nums2){
if(set1.contains(num)){//set1中有nums2的数字,就是交集元素
set2.add(num);//set2存储交集元素
}
}
int[] res = new int[set2.size()];
int index=0;
for(int num:set2){
res[index++] = num;
}
return res;
}
}