【leetcode】字符串(KMP、滑动窗口)算法

参考自 代码随想录

Java、Python的String字符串是不可变的

【参考:java 字符串 复制_Java字符串复制_cunchi4221的博客-CSDN博客
请注意,对于任何不可变的对象,我们都可以将一个变量直接分配(b=a)给另一个变量。 它不仅限于String对象。
但是,如果要将可变对象复制到另一个变量,则应执行Deep copy 。

KMP

实例:28. 实现 strStr - 力扣(LeetCode)

参考:有限状态机之 KMP 字符匹配算法 :: labuladong的算法小抄

用一个二维的 dp 数组(但空间复杂度还是 O(M)),重新定义其中元素的含义.


视频必看

参考:帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili
参考:帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili

前缀表不减一也不右移 容易理解

haystack为文本串, needle为模式串。

  1. 初始化:
    定义两个指针i 和 j,j 指向前缀起始位置,i 指向后缀起始位置

next[j] 就是记录着下标 j(包括j)之前的子串的最长相同前后缀长度

  1. 处理前后缀不相同的情况
    遍历模式串s的循环下标 i 要从 1开始

  2. 处理前后缀相同的情况
    那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。


	public void getNext(int[] next, String s) {
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(i) != s.charAt(j)) 
                j = next[j - 1]; // 回退
            if (s.charAt(i) == s.charAt(j)) 
                j++;
            next[i] = j; 
        }
    }


参考:多图预警👊🏻详解 KMP 算法 - 实现 strStr - 力扣(LeetCode)

写的非常好,图文并茂
在这里插入图片描述


参考:【宫水三叶】简单题学 KMP 算法 - 实现 strStr - 力扣(LeetCode)

从匹配串某个位置跳转下一个匹配位置这一过程是与原串无关的,我们将这一过程称为找 next 点。

显然我们可以预处理出 next 数组,数组中每个位置的值就是该下标应该跳转的目标位置( next 点)。

滑动窗口

【参考:我写了首诗,把滑动窗口算法算法变成了默写题 :: labuladong的算法小抄

  1. 最小覆盖子串(困难)

  2. 字符串的排列(中等)

  3. 找到字符串中所有字母异位词(中等)

  4. 无重复字符的最长子串(中等)

模板

子串的下标[left,right)

双指针滑动窗口,大循环是右指针的移动,内部小循环是左指针的移动

有时候只需使用window的长度作为判断窗口是否需要收缩的条件,不在需要need,valid变量 【904. 水果成篮】

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    unordered_map<char, int> need, window; // 初始化哈希表
    
    for (char c : t) need[c]++; // 初始化需要满足的条件
    
    int left = 0, right = 0;
    int valid = 0; // 满足要求的个数
    
    // [left,right) 左闭右开
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
	
}

题目 76. 最小覆盖子串 - 力扣(LeetCode)
在 S(source) 中找到包含 T(target) 中全部字母的一个子串,且这个子串一定是所有可能子串中最短的。

滑动窗口算法的思路是这样:

1、我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。

2、我们先不断地增加 right 指针扩大窗口[left, right),直到窗口中的字符串符合要求 (包含了 T 中的所有字符)。

3、此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。

4、重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。

这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。

needswindow 相当于计数器,分别记录 T 中字符需要出现的次数和「窗口」中的相应字符的出现次数。

valid 变量表示窗口中满足 need 条件的字符个数,如果 valid 和 need.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T。

在这里插入图片描述
在这里插入图片描述

当我们发现某个字符在 window 的数量满足了 need 的需要,就要更新 valid,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的

当 valid == need.size() 时,说明 T 中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。

移动 left 收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。

public String minWindow(String s, String t) {
        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();

        char[] sc = s.toCharArray();
        char[] tc = t.toCharArray();

        for (char c : tc) {
            // 没有便赋值为0再加一,有就直接在原来的基础上加一
            need.put(c, need.getOrDefault(c, 0) + 1);
        }
        // [left,right)
        int left = 0, right = 0;
        int vaild = 0;
        // 记录最小覆盖子串的起始索引及长度
        int start = 0, len = Integer.MAX_VALUE;

        while (right < s.length()) {
            char c = sc[right];// c 是将移入窗口的字符

            right++;// 右移窗口
            // 进行窗口内数据的一系列更新
            if (need.containsKey(c)) { // need需要匹配这个字符
                window.put(c, window.getOrDefault(c, 0) + 1); // 移入窗口
                if (window.get(c).equals(need.get(c))) { // 窗口内字符c的个数达到need中的要求
                    vaild++;//满足一个条件
                }
            }
            // 判断左侧窗口是否要收缩
            while (vaild == need.size()) {
                // 在这里更新最小覆盖子串 
                if (right - left < len) {
                    start = left;
                    len = right - left;
                }
                char d = sc[left];// d 是将移出窗口的字符

                left++;// 左移窗口
                // 进行窗口内数据的一系列更新
                if (need.containsKey(d)) { // need需要匹配这个字符
                    // 移除d后窗口内字符d的个数会少于need中的要求
                    if (window.get(d).equals(need.get(d))) {
                        vaild--;//满足的条件减一
                    }
                    // window.get(d) - 1 也可以,这里只是为了和上面对称
                    // 移出窗口
                    window.put(d, window.getOrDefault(d, 0) - 1); // 和前面是对称的,不能调换
                }
            }
        }
        // 返回最小覆盖子串
        if (len == Integer.MAX_VALUE) {
            return "";
        } else {
            return s.substring(start, start + len);
        }

    }

904. 水果成篮

题目:【参考:904. 水果成篮【中等】 - 力扣(Leetcode)

官方:【参考:904. 水果成篮 - 力扣(Leetcode)
我的:【参考:904. 水果成篮 - 力扣(Leetcode)

注意,因为使用了getOrDefault,没有使用need,vaild,所以if判断不能省

class Solution {
    public int totalFruit(int[] fruits) {
        int n = fruits.length;
        // fruits[i] 是第 i 棵树上的水果 种类 
        // key:fruits[i],即水果种类;value:fruits[i]出现的次数,即同一种水果出现的次数
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();

        int left = 0, ans = 0;
        for (int right = 0; right < n; ++right) { // 右移窗口
            cnt.put(fruits[right], cnt.getOrDefault(fruits[right], 0) + 1);
            while (cnt.size() > 2) { // 窗口内的水果种类>2
                cnt.put(fruits[left], cnt.get(fruits[left]) - 1); // 删除最左边的水果
                // 下面if不可少,因为使用了getOrDefault
                if (cnt.get(fruits[left]) == 0) { // 出现次数减少为0,需要将对应的键值对从哈希表中移除
                    cnt.remove(fruits[left]);
                }
                ++left; // 左移窗口
            }
            ans = Math.max(ans, right - left + 1); // 最长的窗口长度即为答案
        }
        return ans;
    }
}

// 套模板
class Solution {
    public int totalFruit(int[] fruits) {
        int n = fruits.length;
        // fruits[i] 是第 i 棵树上的水果 种类 
        // key:fruits[i],即水果种类;value:fruits[i]出现的次数,即同一种水果出现的次数
        Map<Integer, Integer> window = new HashMap<Integer, Integer>();
        // 不需要need,只需统计window的长度即可判断是否符合条件
        // Map<Integer, Integer> need = new HashMap<Integer, Integer>();
        int ans = 0;
        int left = 0, right = 0;
        // int vaild = 0; // 不需要vaild,只需统计window的长度即可判断是否符合条件
        
        // 注意窗口:[left,right)
        while (right < n) {
            int c = fruits[right];// c 是将移入窗口的水果

            right++;// 右移窗口
            
            // 进行窗口内数据的一系列更新
            
            window.put(c, window.getOrDefault(c, 0) + 1); // 移入窗口

            // 判断左侧窗口是否要收缩
            while (window.size() > 2) {
                // d 是将移出窗口的水果
                int d = fruits[left];

                // 进行窗口内数据的一系列更新

                window.put(d, window.getOrDefault(d, 0) - 1); // 移出窗口
                // 下面if不可少,因为使用了getOrDefault
                if (window.get(d) == 0) { // 出现次数减少为0,需要将对应的键值对从哈希表中移除
                    window.remove(d);
                }
                // 左移窗口
                left++;

            }
            // 注意窗口:[left,right)
            ans = Math.max(ans, right - left); // 最长的窗口长度即为答案
        }

        return ans;
    }
}

简单

344. 反转字符串

参考:344. 反转字符串 - 力扣(LeetCode)

// 双指针
class Solution {
    public void reverseString(char[] s) {
        int left=0;
        int right=s.length-1;

        while(left<right){
            swap(s, left, right);
            left++;
            right--;
        }
    }
    
    public static void swap(char[] s, int i, int j) {
        char temp = s[i];
        s[i] = s[j];
        s[j] = temp;
    }
}

// 交换进阶
class Solution {
    public void reverseString(char[] s) {
        int l = 0;
        int r = s.length - 1;
        while (l < r) {
            s[l] ^= s[r];  //构造 a ^ b 的结果,并放在 a 中
            s[r] ^= s[l];  //将 a ^ b 这一结果再 ^ b ,存入b中,此时 b = a, a = a ^ b
            s[l] ^= s[r];  //a ^ b 的结果再 ^ a ,存入 a 中,此时 b = a, a = b 完成交换
            l++;
            r--;
        }
    }
}

// 官方
class Solution {
    public void reverseString(char[] s) {
        int n = s.length;
        for (int left = 0, right = n - 1; left < right; ++left, --right) {
            char tmp = s[left];
            s[left] = s[right];
            s[right] = tmp;
        }
    }
}

// 递归 来自《labuladong 的算法小抄》
class Solution {
    public void reverseString(char[] s) {
        reverseStr(s, 0, s.length - 1);
    }
    
    /*将n个问题的规模分解,第一次先将头尾两个交换即swap(lo,hi)
      剩下n-2的规模,面临的问题还是一样,采用递归的思想不断地减小
      问题的规模(减而治之)
    * */
    public static void reverseStr(char[] s, int lo, int hi) {
        if (lo > hi || lo == hi) {//问题规模的奇偶性不变,因为每次减少两个大小
            return;
        }
        if (lo < hi) {
            swap(s, lo, hi);
        }
        reverseStr(s, ++lo, --hi);//递归一次,规模就缩小两个
    }
    
    public static void swap(char[] s, int i, int j) {
        char temp = s[i];
        s[i] = s[j];
        s[j] = temp;
    }
}

541. 反转字符串 II ***

参考:541. 反转字符串 II - 力扣(LeetCode)
参考:代码随想录# 541. 反转字符串II

StringBuilder

StringBuilder 感觉比较麻烦
双指针

class Solution {
    public String reverseStr(String s, int k) {
        StringBuilder result = new StringBuilder();
        int n = s.length();
        // 1.每隔2k个字符的前k个字符进行反转
        // 2.剩余字符小于2k但大于或等于k个,则反转前k个字符
        // 3.如果剩余字符少于k个,则将剩余字符全部反转
        for (int i = 0; i < n; i += 2 * k) {
            // 第一个k,i+k的长度是否大于n,是则说明到了最后,否则firstK=i+k
            int firstK = (i + k > n) ? n : i + k; // 
            // 第二个k,i+2k的长度是否大于n,是则说明到了最后,否则secondK=i+2k
            int secondK = (i + 2 * k > n) ? n : i + 2 * k; 
            
            StringBuilder sb = new StringBuilder();
            String temp = s.substring(i, firstK); // 截取下标为[i,firstK)之间的字符
            sb.append(temp);
            sb.reverse(); // 反转
            result.append(sb);
            
            // 如果firstK到secondK之间有元素,这些元素直接放入result里即可。
            if (firstK < secondK) {
                // 此时剩余长度一定大于k。
                result.append(s.substring(firstK, secondK));
            }

        }
        return result.toString(); // 转成String
    }
}

toCharArray

toCharArray() 简单易懂效率高
双指针

class Solution {
    public String reverseStr(String s, int k) {
        int n = s.length();
        char[] arr = s.toCharArray(); // 转成字符串数组

        for (int i = 0; i < n; i += 2 * k) {
            int start = i;
            int end = Math.min(i + k, n) - 1; // 不够k个就取最后一个,-1是取下标
            reverse(arr, start, end);
        }
        return new String(arr);// 从数组中新建字符串
    }

    // 反转字符数组arr[left,right]
    public void reverse(char[] arr, int left, int right) {
        while (left < right) {
            char temp = arr[left];
            arr[left] = arr[right];
            arr[right] = temp;

            left++;
            right--;
        }
    }
}

剑指 Offer 05. 替换空格

参考:剑指 Offer 05. 替换空格 - 力扣(LeetCode)
正常方法:从前往后遍历,复制数组到新数组,其中遇到空格就把其变成%20再存入新数组中

class Solution {
    public String replaceSpace(String s) {
        int n=s.length();
        if(n==0) return "";

        StringBuilder sb=new StringBuilder();
        for(int i=0;i<n;i++){
            if(s.charAt(i)==' '){
                sb.append("%20");
            }else{
                sb.append(s.charAt(i));
            }
        }
        return sb.toString();
    }
}

不使用额外的辅助空间(因为Java、Python的String是不可变的,所以无法实现

参考:代码随想录# 题目:剑指Offer 05.替换空格

首先扩充数组到每个空格替换成"%20"之后的大小。

然后从后向前替换空格,也就是双指针法,i指向新长度的末尾,j指向旧长度的末尾。

//方式二:双指针法
public String replaceSpace(String s) {
    if(s == null || s.length() == 0){
        return s;
    }
    //扩充空间,空格数量2倍
    StringBuilder str = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
        if(s.charAt(i) == ' '){
            str.append("  ");
        }
    }
    //若是没有空格直接返回
    if(str.length() == 0){
        return s;
    }
    //有空格情况 定义两个指针
    int left = s.length() - 1;//左指针:指向原始字符串最后一个位置
    s += str.toString();
    int right = s.length()-1;//右指针:指向扩展字符串的最后一个位置
    char[] chars = s.toCharArray();
    while(left>=0){
        if(chars[left] == ' '){
            chars[right--] = '0';
            chars[right--] = '2';
            chars[right] = '%';
        }else{
            chars[right] = chars[left];
        }
        left--;
        right--;
    }
    return new String(chars);
}

剑指 Offer 58 - II. 左旋转字符串

参考:剑指 Offer 58 - II. 左旋转字符串 - 力扣(LeetCode)

// 正常思路
class Solution {
    public String reverseLeftWords(String s, int n) {
        StringBuilder res = new StringBuilder();
        for(int i = n; i < s.length(); i++)
            res.append(s.charAt(i));
        for(int i = 0; i < n; i++)
            res.append(s.charAt(i));
        return res.toString();
    }
}
// 取余 就相当于上面的
class Solution {
    public String reverseLeftWords(String s, int n) {
        StringBuilder res = new StringBuilder();
        for(int i = n; i < n + s.length(); i++)
            char c=s.charAt(i % s.length());
            res.append(c);
        return res.toString();
    }
}

// API
class Solution {
    public String reverseLeftWords(String s, int n) {
        return s.substring(n, s.length()) + s.substring(0, n);
    }
}

不能申请额外空间,只能在本串上操作。

参考:代码随想录# 题目:剑指Offer58-II.左旋转字符串

用整体反转+局部反转就可以实现,反转单词顺序的目的。
和第151题类似
这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。

具体步骤为:
反转区间为前n的子串
反转区间为n到末尾的子串
反转整个字符串


	public String reverseLeftWords(String s, int n) {
        String str1 = new StringBuilder(s.substring(0,n)).reverse().toString();
        String str2 = new StringBuilder(s.substring(n)).reverse().toString();
        return new StringBuilder(str1+str2).reverse().toString();
	}

// 双指针法
class Solution {
    public String reverseLeftWords(String s, int n) {
        int len=s.length();
        StringBuilder sb=new StringBuilder(s);
        reverseString(sb,0,n-1);
        reverseString(sb,n,len-1);
        return sb.reverse().toString();
    }
     public void reverseString(StringBuilder sb, int start, int end) {
        while (start < end) {
            char temp = sb.charAt(start);
            sb.setCharAt(start, sb.charAt(end));
            sb.setCharAt(end, temp);
            start++;
            end--;
            }
        }
}

28. 实现 strStr ***

参考:28. 实现 strStr - 力扣(LeetCode)

class Solution {
    public int strStr(String haystack, String needle) {
        int n=haystack.length(),m=needle.length();
        if(m==0){ // 匹配空串
            return 0; 
        }
        int[] next=new int[m];
        getNext(next,needle);

        int j=0;
        for(int i=0;i<n;i++){ // 这里从0开始
            while(j>0&&haystack.charAt(i)!=needle.charAt(j)){
                j=next[j-1]; // 查表
            }
            if(haystack.charAt(i)==needle.charAt(j)){
                j++;
            }
            if(j==m){ // 匹配成功
                return i-m+1;
            }
        }
        return -1;
    }

    public void getNext(int[] next,String s){
        int j=0;
        next[0]=0;
        for(int i=1;i<s.length();i++){ // 这里从1开始
            while(j>0&&s.charAt(i)!=s.charAt(j)){
                j=next[j-1];
            }
            if(s.charAt(i)==s.charAt(j)){
                j++;
            }
            next[i]=j;
        }
    }
}

459. 重复的子字符串

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。

参考:459. 重复的子字符串 - 力扣(LeetCode)

参考:重复的子字符串 - 重复的子字符串 - 力扣(LeetCode)

  • 方法一:枚举
    下面的理解有问题,待定
    子串至少重复一次, i ∈ [ 0 , n / 2 ] i \in[0,n/2] i[0,n/2]代表子串开始的位置 j ∈ [ i + 1 , n ) j\in[i+1,n) j[i+1,n)代表子串开始重复的位置 循环判断 s [ i ] = = s [ j − i ] s[i] == s[j-i] s[i]==s[ji],如果到 j = n − 1 j=n-1 j=n1处都相等,则表明有重复的子串

  • 方法二:字符串匹配
    s = " a b a b " , S = s + s = " a b a b a b a b " s="abab", S=s+s="abababab" s="abab",S=s+s="abababab"
    S S S移除第一个和最后一个字符变成 S ′ S' S,那么得到的字符串一定包含 s,即 s 是它的一个子串。
    S ′ = " b a b a b a " 包含 a b a b ,即包含 s S'="bababa"包含abab,即包含s S="bababa"包含abab,即包含s
    代码参考:简单明了!!关于java两行代码实现的思路来源 - 重复的子字符串 - 力扣(LeetCode)

// 方法一
	public boolean repeatedSubstringPattern(String s) {
        int n = s.length();
        for (int i = 1; i * 2 <= n; ++i) { // i就相当于周期数
            if (n % i == 0) {
                boolean match = true;
                for (int j = i; j < n; ++j) {
                    if (s.charAt(j) != s.charAt(j - i)) {
                        match = false;
                        break;
                    }
                }
                if (match) {
                    return true;
                }
            }
        }
        return false;
    }
    
// 方法二
class Solution {
    public boolean repeatedSubstringPattern(String s) {
        String str = s + s;
        return str.substring(1, str.length() - 1).contains(s);
    }
}

// 方法三

利用周期性
参考:【妙解】重复的子字符串 - 拓海藤原 - 博客园
在这里插入图片描述

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int n=s.length();
        int i=0;
        for(int t=1;t<=n/2;t++){ // 周期从1开始
            if(n%t!=0) continue; // 有余数,一定不满足周期
            for(i=t;i<n;i++){
                if(s.charAt(i)!=s.charAt(i%t)){ // f(x)=f(x+t)
                    break;
                }                    
            }
            if(i==n){ // 遍历到了最后一个,说明全部满足周期性
                return true;
            }
        }
        return false;
    }
}

KMP解法

参考《代码随想录——跟着Carl学算法》第107页
在这里插入图片描述

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int n = s.length();
        if (n == 0)
            return false;

        int[] next = new int[n];
        getNext(next, s);

        int cycle = n - next[n - 1]; // 周期,也就是重复子串的长度

        if (next[n - 1] != 0 && n % cycle == 0) {
            return true;
        }
        return false;
    }

    public void getNext(int[] next, String s) {
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < s.length(); i++) {
            while (j > 0 && s.charAt(i) != s.charAt(j))
                j = next[j - 1]; // 回退
            if (s.charAt(i) == s.charAt(j))
                j++;
            next[i] = j;
        }
    }
}

中等

151. 翻转字符串里的单词

参考:151. 翻转字符串里的单词 - 力扣(LeetCode)

  • 方法一:
    解题思路如下:
    移除多余空格
    将整个字符串反转
    将每个单词反转
    整体反转,再局部反转,反反得正
    举个例子,源字符串为:"the sky is blue "
    移除多余空格 : “the sky is blue”
    字符串反转:“eulb si yks eht”
    单词反转:“blue is sky the”

  • 方法二:
    直接先全部翻转(空格都不用去掉),然后从头开始遍历,进行局部翻转(这时候注意空格条件即可)


// 官方
class Solution {
    public String reverseWords(String s) {
        // 除去开头和末尾的空白字符
        s = s.trim();
        // 正则匹配连续的空白字符作为分隔符分割
        List<String> wordList = Arrays.asList(s.split("\\s+"));
        Collections.reverse(wordList);
        return String.join(" ", wordList);
    }
}

// 方法一
class Solution {
    public String reverseWords(String s) {
        // StringBuilder是引用类型
        StringBuilder sb = removeSpaces(s); // 去除多余的空格
        // 反转字符串
        reverse(sb, 0, sb.length() - 1);
        // 反转每个单词
        reverseEachWord(sb);
        return sb.toString();
    }

    // 去掉多余的空格
    public StringBuilder removeSpaces(String s) {
        int left = 0;
        int right = s.length() - 1;
        // 去掉最左边的
        while (left < right && s.charAt(left) == ' ')
            left++;
        // 去掉最右边的
        while (left < right && s.charAt(right) == ' ')
            right--;
        StringBuilder sb = new StringBuilder();
        // 去掉中间的
        while (left <= right) {
            char c = s.charAt(left);
            if (c != ' ') {
                sb.append(c);
            } else if (c == ' ' && s.charAt(left - 1) != ' ') { // s[left]是空格但前一个不是
                sb.append(c);
            }
            left++;
        }
        return sb;
    }

    // 翻转字符串(复用)
    public void reverse(StringBuilder sb, int left, int right) {
        while (left < right) {
            char temp = sb.charAt(left);
            sb.setCharAt(left, sb.charAt(right));
            sb.setCharAt(right, temp);
            left++;
            right--;
        }
    }

    // 翻转字符串中的每个单词
    public void reverseEachWord(StringBuilder sb) {
        int n = sb.length();
        int start = 0;
        int end = 0;
        while (start < n) {
            // 循环至单词的末尾
            while (end < n && sb.charAt(end) != ' ') {
                ++end;
            }
            // 翻转单词
            reverse(sb, start, end - 1);
            // 更新start,去找下一个单词
            start = end + 1;
            ++end;
        }
    }
}

567. 字符串的排列

【参考:567. 字符串的排列 - 力扣(LeetCode)

【参考:我写了首诗,把滑动窗口算法算法变成了默写题 :: labuladong的算法小抄

相当给你一个 S 和一个 T,请问你 S 中是否存在一个子串,包含 T 中所有字符且不包含其他字符?

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();

        char[] sc = s2.toCharArray();
        char[] tc = s1.toCharArray(); // 子串

        for (char c : tc) {
            // 没有便赋值为0再加一,有就直接在原来的基础上加一
            need.put(c, need.getOrDefault(c, 0) + 1);
        }
        // [left,right)
        int left = 0, right = 0;
        int vaild = 0;
        
        while (right < s2.length()) {
            char c = sc[right];// c 是将移入窗口的字符

            right++;// 右移窗口
            // 进行窗口内数据的一系列更新
            if (need.containsKey(c)) { // need需要匹配这个字符
                window.put(c, window.getOrDefault(c, 0) + 1);
                if (window.get(c).equals(need.get(c))) { // 窗口内字符c的个数达到need中的要求
                    vaild++;//满足一个条件
                }
            }
            // 判断左侧窗口是否要收缩
            while (vaild== need.size()) {
                // 子串是连续的
                if((right-left) == s1.length()) 
                    return true;

                char d = sc[left];// d 是将移出窗口的字符

                left++;// 左移窗口
                // 进行窗口内数据的一系列更新
                if (need.containsKey(d)) { // need需要匹配这个字符
                    // 移除d后窗口内字符d的个数会少于need中的要求
                    if (window.get(d).equals(need.get(d))) {
                        vaild--;//满足的条件减一
                    }
                    window.put(d, window.getOrDefault(d, 0) - 1); // 和前面是对称的,不能调换
                }
            }
        }
        return false;

    }
}

方法二
1、本题移动 left 缩小窗口的时机是窗口大小大于 t.size() 时,应为排列嘛,显然长度应该是一样的。

2、当发现 valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true。

			// 判断左侧窗口是否要收缩
            while ((right-left) == s1.length()) {
                // 在这里判断是否找到了合法的子串
                if(vaild== need.size()) 
                    return true;

438. 找到字符串中所有字母异位词

【参考:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

class Solution {
    public List<Integer> findAnagrams(String s, String t) {
        List<Integer> list=new ArrayList<>();
        Map<Character, Integer> need = new HashMap<>();
        Map<Character, Integer> window = new HashMap<>();

        char[] sc = s.toCharArray();
        char[] tc = t.toCharArray();

        for (char c : tc) {
            // 没有便赋值为0再加一,有就直接在原来的基础上加一
            need.put(c, need.getOrDefault(c, 0) + 1);
        }
        // [left,right)
        int left = 0, right = 0;
        int vaild = 0;

        while (right < s.length()) {
            char c = sc[right];// c 是将移入窗口的字符

            right++;// 右移窗口
            // 进行窗口内数据的一系列更新
            if (need.containsKey(c)) { // need需要匹配这个字符
                window.put(c, window.getOrDefault(c, 0) + 1);
                if (window.get(c).equals(need.get(c))) { // 窗口内字符c的个数达到need中的要求
                    vaild++;//满足一个条件
                }
            }
            // 判断左侧窗口是否要收缩
            while (vaild == need.size()) {
                // 子串是连续的
                if(right-left==t.length())// [left,right)
                    list.add(left);// 在这里更新记录子串的起始索引
                    
                char d = sc[left];// d 是将移出窗口的字符

                left++;// 左移窗口
                // 进行窗口内数据的一系列更新
                if (need.containsKey(d)) { // need需要匹配这个字符
                    // 移除d后窗口内字符d的个数会少于need中的要求
                    if (window.get(d).equals(need.get(d))) {
                        vaild--;//满足的条件减一
                    }
                    window.put(d, window.getOrDefault(d, 0) - 1); // 和前面是对称的,不能调换
                }
            }
        }
        return list;

    }
}

下面这样也行

			// 判断左侧窗口是否要收缩
            while ((right-left)==t.length()) { // [left,right)
                // 子串是连续的
                if(vaild == need.size())
                    list.add(left);// 在这里更新记录子串的起始索引

3. 无重复字符的最长子串

【参考:3. 无重复字符的最长子串 - 力扣(LeetCode)

【参考:我写了首诗,把滑动窗口算法算法变成了默写题 :: labuladong的算法小抄

当 window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了嘛。

在收缩窗口完成后更新 res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> window = new HashMap<>();

        char[] sc = s.toCharArray();
        
        int left = 0, right = 0;
        int result = 0;

        while (right < s.length()) {
            char c = sc[right];// c 是将移入窗口的字符

            right++;// 右移窗口
            // 进行窗口内数据的一系列更新
            window.put(c, window.getOrDefault(c, 0) + 1);
            
            // 判断左侧窗口是否要收缩
            while (window.get(c)>1) {
                char d = sc[left];// d 是将移出窗口的字符
                left++;// 左移窗口
                window.put(d, window.getOrDefault(d, 0) - 1); 
            }
            // 更新结果
            result=Math.max(result,right-left);
        }
        return result;

    }
}

5. 最长回文子串 ***

【参考:5. 最长回文子串 - 力扣(LeetCode)

  • 中心扩散法
class Solution {
    public String longestPalindrome(String s) {
        
        int res1 = 0,res2=0;
        int res=0;
        int len = s.length();

        if(len==1) return s;

        // 对称轴是第i个字符 最长对称子串的长度是奇数
        for (int i = 0; i < len; i++) {
            int sLen  = 1; // 从1开始,奇数最少为1 即对称子串长度为1
            int x = i - 1;
            int y = i + 1;
            // 先判断下标是否合法 再判断相等 比较方便
            while ( x >= 0 && y < len && s.charAt(x) == s.charAt(y) ) {
                x--;
                y++;
                sLen+=2;
            }
            if (sLen > res) {
                res=sLen;
                // 记录下标
                res1=x+1; 
                res2=y-1;
            }
        }
        // 对称轴是第i,i+1个字符 最长对称子串的长度是偶数
        for (int i = 0; i < len; i++) {
            int sLen = 0; // 从0开始,偶数最少为0 即没有对称子串
            int x = i;
            int y = i + 1;
            while (x >= 0 && y < len && s.charAt(x) == s.charAt(y)) {
                x--;
                y++;
                sLen+=2;
            }
            if (sLen > res) {
                res=sLen;
                res1=x+1;
                res2=y-1;
            }
        }

        return s.substring(res1,res2+1);
    }
}
  • 动态规划

思路:【参考:动态规划、中心扩散。详细注释版 - 最长回文子串 - 力扣(LeetCode)

在这里插入图片描述

代码: https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zui-chang-hui-wen-zi-chuan-by-leetcode-solution/407018 下面的代码有删改

class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        // 特判
        if (len < 2){
            return s;
        }

        int maxLen = 0;
        int begin  = 0,end = 0;

        // 1. 状态定义
        // dp[i][j] 表示s[i...j] 是否是回文串

        // 2. 初始化
        boolean[][] dp = new boolean[len][len];
        for (int i = 0; i < len; i++) {
            dp[i][i] = true;
        }

        char[] chars = s.toCharArray();
        // i 是起点 j是终点
        // 3. 状态转移
        // 注意:先填左上角
        // 填表规则:先一列一列的填写 j ,再一行一行的填 i,保证左上方的单元格先进行计算
        for (int j = 1;j < len;j++){
            for (int i = 0; i < j; i++) {
                // 头尾字符不相等,不是回文串
                if (chars[i] != chars[j]){
                    dp[i][j] = false;
                }else {
                    // 相等的情况下
                    // 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,肯定是回文串 "aba" "aa"
                    if (j - i < 3){
                        dp[i][j] = true;
                    }else {
                        // 状态转移
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要dp[i][j] == true 成立,表示s[i...j] 是否是回文串
                // 此时更新记录回文长度和起始位置
                if (dp[i][j] && j - i > maxLen){
                    maxLen = j - i; 
                    begin = i;
                    end = j ;
                }
            }
        }
        // 4. 返回值
        return s.substring(begin,end + 1);
    }
}

49. 字母异位词分组

【参考:49. 字母异位词分组 - 力扣(LeetCode)

【参考:字母异位词分组 - 字母异位词分组 - 力扣(LeetCode)

方法一:排序

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            char[] array = str.toCharArray();
            Arrays.sort(array); // 字符排序
            
            String key = new String(array);
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<List<String>>(map.values());
    }
}
  • 方法二:计数

将每个字母出现的次数使用字符串表示,作为哈希表的键

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 (int i = 0; i < str.length(); i++) {
                counts[str.charAt(i) - 'a']++;
            }

            // 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 26; i++) {
                if (counts[i] != 0) {
                    sb.append((char) 'a' + i); // 字母
                    sb.append(counts[i]); // 出现的次数
                }
            }
            String key = sb.toString();
            // 获取键对应的值 list里面存放的就是 每个字母出现的次数一致 的所有字符串
            List<String> list = map.getOrDefault(key, new ArrayList<>());
            list.add(str);
            map.put(key, list);// 更新值
        }
        return new ArrayList<List<String>>(map.values()); // 返回map的值
    }
}

713. 乘积小于 K 的子数组

【参考:713. 乘积小于 K 的子数组 - 力扣(LeetCode)

【参考:713.官方思路秒懂○注释详细○双指针滑窗 【附通用滑窗模板】 - 乘积小于 K 的子数组 - 力扣(LeetCode)

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) {
            return 0;
        }

        int left=0,right=0;
        int res=0;
        int mul=1; // 窗口内的数字的乘积
        // 注意,这里是左闭右开 [left,right)
        while(right<nums.length){

            mul*=nums[right];
             //右移窗口
            right++;

            while(mul>=k){ // 需要收缩
                // 因为是左闭右开,所以需要先处理窗口内的数据再移动
                mul/=nums[left];
                left++;    // 左移窗口            
            }

            res+=right-left;      
        }

        return res;
    }
}
class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) {
            return 0;
        }

        int left=0,right=0;
        int res=0;
        int mul=1; // 窗口内的数字的乘积
        // 注意,这里是闭区间 [left,right]
        while(right<nums.length){

            mul*=nums[right];

            while(mul>=k){ // 需要收缩
                // 因为是闭区间,所以需要先处理窗口内的数据再移动
                mul/=nums[left];
                left++;    // 左移窗口            
            }

            res+=right-left+1;

            //右移窗口
            right++;
        }

        return res;
    }
}

1876. 长度为三且各字符不同的子字符串

【参考:1876. 长度为三且各字符不同的子字符串 - 力扣(Leetcode)

这道题可以暴力遍历

【参考:1876. 长度为三且各字符不同的子字符串 - 力扣(Leetcode)- 题解 - Java+滑动窗口

class Solution {
    public int countGoodSubstrings(String s) {
        Set<Character> set = new HashSet<>();// 窗口内单独字符的个数
        char[] arr = s.toCharArray();
        int left = 0, right = 0;
        int res = 0;
        // [left,right)
        while (right < arr.length) {
            char c = arr[right];
            right++;
            // // 如果右指针当前的字符在window中已存在,说明出现了重复字符,此时左指针要前进,收缩window,直到window中没有与右指针重复的字符
            while (set.contains(c) && left < right) {
                set.remove(arr[left]);
                left++;
            }
            // 此时将右指针字符加入window,肯定不会有重复
            set.add(c);
            // 判断window中已经满3个字符了,而且肯定不会重复
            if (set.size() == 3) {
                res++;
                // System.out.println(set.toString());
                char d = arr[left];
                left++;
                set.remove(d);
            }
        }
        return res;
    }
}

困难

76. 最小覆盖子串

【参考:76. 最小覆盖子串 - 力扣(LeetCode)

看前面 【滑动窗口】

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值