参考自 代码随想录
Java、Python的String字符串是不可变的
【参考:java 字符串 复制_Java字符串复制_cunchi4221的博客-CSDN博客】
请注意,对于任何不可变的对象,我们都可以将一个变量直接分配(b=a)
给另一个变量。 它不仅限于String对象。
但是,如果要将可变对象复制到另一个变量,则应执行Deep copy 。
实例:28. 实现 strStr - 力扣(LeetCode)
参考:有限状态机之 KMP 字符匹配算法 :: labuladong的算法小抄
用一个二维的 dp 数组(但空间复杂度还是 O(M)),重新定义其中元素的含义.
视频必看
参考:帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili
参考:帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili
前缀表不减一也不右移 容易理解
haystack为文本串, needle为模式串。
前缀
起始位置,i 指向后缀
起始位置。next[j]
就是记录着下标 j(包括j)之前的子串的最长相同前后缀的长度。
处理前后缀不相同的情况
遍历模式串s的循环下标 i 要从 1开始
处理前后缀相同的情况
那么就同时向后移动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的算法小抄】
最小覆盖子串(困难)
字符串的排列(中等)
找到字符串中所有字母异位词(中等)
无重复字符的最长子串(中等)
子串的下标[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 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
needs
和 window
相当于计数器,分别记录 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. 水果成篮【中等】 - 力扣(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 need = new HashMap();
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. 反转字符串 - 力扣(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 - 力扣(LeetCode)
参考:代码随想录# 541. 反转字符串II
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() 简单易懂效率高
双指针
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. 替换空格 - 力扣(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. 左旋转字符串 - 力扣(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 - 力扣(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. 重复的子字符串 - 力扣(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[j−i],如果到 j = n − 1 j=n-1 j=n−1处都相等,则表明有重复的子串
方法二:字符串匹配
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;
}
}
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. 翻转字符串里的单词 - 力扣(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. 字符串的排列 - 力扣(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. 找到字符串中所有字母异位词 - 力扣(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. 无重复字符的最长子串 - 力扣(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. 最长回文子串 - 力扣(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. 字母异位词分组 - 力扣(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 的子数组 - 力扣(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. 长度为三且各字符不同的子字符串 - 力扣(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. 最小覆盖子串 - 力扣(LeetCode)】
看前面 【滑动窗口】