方法一:暴力求解(效率差):
使用两个 for 循环,一个 for 循环固定一个数字比如 m,另一个 for 循环从 m 的下一个元素开始累加,当和大于等于 s 的时候终止内层循环,顺便记录下最小长度
public int minSubArrayLen(int s, int[] nums) {
int min = Integer.MAX_VALUE;
for (int i = 0; i < nums.length; i++) {
int sum = nums[i];
if (sum >= s)
return 1;
for (int j = i + 1; j < nums.length; j++) {
sum += nums[j];
if (sum >= s) {
min = Math.min(min, j - i + 1);
break;
}
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
方法二:滑动窗口,使用双指针,left和right,分别用来向右缩小窗口和向右扩大窗口
思路:遇到题目求最小,可思考先预设一个最大值,然后不断更新
窗口包含数值总和大了就向右缩小窗口,小了就向右扩展窗口,不断试错,直至找到最小窗口,并且使所有值加起来超不过target
注意前后顺序
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int sum = 0;
int result = Integer.MAX_VALUE;//让窗口最大,这样就能不断更新最小值窗口
for(int right = 0;right < nums.length ;right++){
sum += nums[right];
while(sum >= target){
result = Math.min(result, right - left + 1);//让窗口永远保持最小
sum -= nums[left++];//左侧索引前移
}
}
//依照案例,如果所有值加起来都小于target,返回0,也就是MAX_VALUE一直都没变过
return result == Integer.MAX_VALUE ? 0 : result;
}
}
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new ArrayList<>();
int m = s.length();
int n = p.length();
if (m < n) {
// 如果s没p长,直接返回空列表
return ans;
}
int[] sCnt = new int[26];
int[] pCnt = new int[26];
for (int i = 0; i < n; i++) {
sCnt[s.charAt(i) - 'a']++;
pCnt[p.charAt(i) - 'a']++;
}
if (Arrays.equals(sCnt, pCnt)) {
// 初始窗口就相等
ans.add(0);
}
for (int i = 0; i < m - n; i++) {
// 窗口右移
sCnt[s.charAt(i) - 'a']--;
sCnt[s.charAt(i + n) - 'a']++;
if (Arrays.equals(sCnt, pCnt)) {
ans.add(i + 1);
}
}
return ans;
}
}
本题考察的算法思想时滑动窗口,对于滑动窗口,需要考虑窗口什么时候扩大、什么时候缩小窗口、什么时候结算窗口内的信息、窗口扩大及缩小的对应操作。
首先遍历t字符串,统计每个字母出现的频率。
定义valid代表已完成匹配字符的数量
r 为窗口右边界,l 代表窗口左边界
开始遍历 s 中的每个字符,
若当前字符在 t 中出现过,window 将这个字符收入,对应词频+1,
若window 与 need 中的这个字符频率相等,valid++,代表完成一个字符的匹配
若某时valid==need.size(),代表已经完成了匹配,需要结算窗口的信息
信息包含左右边界的位置,以及此时最小覆盖的字串长度是多少
r-l 代表此时窗口的大小, r-l < len 代表此时窗口小于上一次的窗口,
此时更新 要返回的子字符串的开始与结束位置
start = l , l 代表窗口左边界,将这种情况下的左边界赋值给 l
len = r-l , len 代表窗口大小
由于题目要求的是最小的包含 t 字符的字符串,所以需要不断缩小窗口来求出最小的窗口大小
need.containsKey( d ) 判断当前 l 位置的字符是否是need需要的,就是t中是否包含的字符
若window 与 need 中的这个字符频率相等,valid–,下一次重新开始匹配
window 需要将这个字符移除
若这个字符不在 need 中,也就是不需要的字符,则继续判断下一个左边界代表的字符。
最后返回之前判断len是否等于 Integer.MAX_VALUE
如果等于,则代表没能匹配成功,
如果不等,代表上面过程中有匹配成功的情况,而在上面过程中,我们总是不断缩小窗口大小,且每次都将左窗口赋值给 start , len代表着完成匹配的窗口大小。
所以最后截取的字符串为 s.substring(start, start + len) 。
此解法仅在匹配过程中线性扫描两个字符串,所以整体时间复杂度为O(n)。
public String minWindow( String s, String t ) {
String res = "";
if ( t.length() > s.length() ) {
return res;
}
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();
for ( int i = 0; i < t.length(); i++ ) {
char ch = t.charAt( i );
need.put( ch, need.getOrDefault( ch, 0 ) + 1 );
}
int l = 0;
int r = 0;
int valid = 0;
int len = Integer.MAX_VALUE;
int start = 0;
while ( r < s.length() ) {
char ch = s.charAt( r );
r++;
if ( need.containsKey( ch ) ) {
window.put( ch, window.getOrDefault( ch, 0 ) + 1 );
if ( window.get( ch ).equals( need.get( ch ) ) ) {
valid++;
}
}
// 判断左侧窗口是否要收缩
while ( valid == need.size() ) {
// 在这里更新最小覆盖子串
if ( r - l < len ) {
start = l;
len = r - l;
}
// d 是将移出窗口的字符
char d = s.charAt( l );
// 左移窗口
l++;
// 进行窗口内数据的一系列更新
if ( need.containsKey( d ) ) {
if ( window.get( d ).equals( need.get( d ) ) ) {
valid--;
}
window.put( d, window.get( d ) - 1 );
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring( start, start+len );
}
要注意什么时候向右扩大窗口,什么时候向右缩小窗口,什么时候结算窗口内的信息,如何更新窗口内的信息
class Solution {
public boolean checkInclusion(String s1, String s2) {
int l=0;
int r=0;
int []s1arr=new int[26];
int []s2arr=new int[26];
for(int i=0;i<s1.length();i++){
s1arr[s1.charAt(i)-'a']++;
}
while(r<s2.length()){
s2arr[s2.charAt(r)-'a']++;
if(r-l+1==s1.length()){
if(equalS1S2(s1arr,s2arr)) {
return true;
}else{
s2arr[s2.charAt(l)-'a']--;
l++;
}
}
r++;//r++位置要放好,不能直接放在s2arr[s2.charAt(r)-'a']++;下面
}
return false;
}
public boolean equalS1S2( int[] s1,int[] s2){
for(int i=0;i<s1.length;i++){
if(s1[i]!=s2[i])return false;
}
return true;
}
}
使用set,让left每次加一
/*时间复杂度:O(2n) = O(n),最坏的情况是 left 和 right 都遍历了一遍字符串*/
/*空间复杂度:O(n)*/
/*https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/jian-dan-yi-dong-javac-pythonjshua-dong-bff20/*/
public int lengthOfLongestSubstring2(String s) {
int n = s.length();
if (n <= 1) return n;
int maxLen = 1;
int left = 0, right = 0;
/*使用set集合记录窗口中出现的字符,这样可以保证 不重复,
*如果窗口中不包含右指针指向的字符,就把它加入set(并记录窗口最大值 ),如果存在就移除 ,并将left指针++,每次移动一次*/
Set<Character> window = new HashSet<>();
while (right < n) {
char rightChar = s.charAt(right);
while (window.contains(rightChar)) {
window.remove(s.charAt(left));
left++;
}
maxLen = Math.max(maxLen, right - left + 1);
window.add(rightChar);
right++;
}
return maxLen;
}
使用map,记录每个字符的索引,遇到重复字符时,直接跳到 重复字符的下一个 位置
/*上面的方法,出现重复字符时,是一个一个字符的缩小窗口
*可以通过map记录每个字符在字符串中出现的位置,当遇到重复字符时,把left指针跳到重复字符的下一个位置 */
/*/*力扣3,评论区
* https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/wu-zhong-fu-zi-fu-de-zui-chang-zi-chuan-by-leetc-2
* */*/
public static int lengthOfLongestSubstringWithoutRepeatingCharacters(String s) {
int len = s.length();
Map<Character, Integer> index = new HashMap<>();
int max = 0;
int left = 0;
for (int right = 0; right < len; right++) {
char c = s.charAt(right);
if (index.containsKey(c)) {
int lastIndex = index.get(c);
if (lastIndex + 1 > left) {
left = lastIndex + 1;
}
}
//因为左指针已经跳到重复字符的下一个位置了,当前右指针指向的字符跟去掉的字符一样,所以需要在map中更新一下位置
index.put(c, right);
max = Math.max(max, right - left + 1);
}
return max;
}