1、反转字符串
题目:https://leetcode.cn/problems/reverse-string/ 思路:双指针,左指针从数组首部开始,右指针从数组末尾开始;每两个数的交换可以按常规的创建一个中间变量,也可以通过三次异或
a ^= b,此时a里面是 a^b
b ^= a,此时b里面是 b^(a^b) = a
a ^= b,此时a里面是 (a^b)^a = b
2、反转字符串Ⅱ
题目:https://leetcode.cn/problems/reverse-string-ii/submissions/ 思路:整体就是在反转字符串基础上逻辑判断 2k 一个循环,然后最后剩余的一部分要注意右指针的起始位置可能够k个可能不够
2k一循环,通过循环变量 i ,每次循环 i += 2*k,即可 每个大循环内反转的右指针起始位置,可以通过 i + k - 1 和 s.length() - 1 中的小值选择
3、替换空格
题目:https://leetcode.cn/problems/ti-huan-kong-ge-lcof/ 思路:先扩容、后移动替换
先统计原字符串里面的空格个数,每一个空格在原字符串长度的基础上追加两个空格(可以使用StringBuilder统计追加的空格,再和原字符串拼接); 进行替换的时候使用双指针法,要从后往前从原字符串选择字符串,在扩容后的字符串末尾从后往前移动或者替换为"%20",如果从前往后双指针操作当替换为"%20"的时候需要移动后面的字符造成不必要的操作。 代码:
class Solution {
public String replaceSpace ( String s) {
if ( s == null || s. length ( ) == 0 ) {
return s;
}
StringBuilder sb = new StringBuilder ( ) ;
for ( int i = 0 ; i < s. length ( ) ; ++ i) {
if ( s. charAt ( i) == ' ' ) {
sb. append ( " " ) ;
}
}
int left = s. length ( ) - 1 ;
char [ ] ch = ( s + sb) . toCharArray ( ) ;
int right = ch. length - 1 ;
while ( left >= 0 ) {
if ( s. charAt ( left) == ' ' ) {
ch[ right-- ] = '0' ;
ch[ right-- ] = '2' ;
ch[ right-- ] = '%' ;
} else {
ch[ right-- ] = s. charAt ( left) ;
}
-- left;
}
return new String ( ch) ;
}
}
4、翻转字符串里的单词
题目:https://leetcode.cn/problems/reverse-words-in-a-string/ 思路:先整体反转,再局部反转每个单词,反转完每个单词后移动单词从而去除多余的空格,注意最后一个单词的处理
整体反转,双指针封装一个反转字符串的自定义方法 局部反转每个单词,利用一个变量i,遇到空格跳过后遇到的第一个非空格字符记录为一个单词的起点,然后一直找到下一个空格位置记录这个单词的结束位置;调用自定义方法实现这个单词的反转 移动:设置一个全局变量k控制最终的结果中的索引,局部变量j从反转后的单词中取字符放到k控制的位置并同时向后移动,每个单词结束以后,控制k追加一个空格;但最后一个字符要注意,如果字符串中没有删除多余空格就不应该追加多余的空格,避免k索引越界; 返回结果:因为最后可能多追加了一个空格,也可能之前没有删除空格恰好没追加,此时就应该控制返回的截止位置是去除一个空格后的k-1,还是不去除的k?只需要判断k是否恰好处在原字符串最后一个位置并且这个位置不为空格就可以了,满足就返回到k;否则删除一个索引返回到k-1. 代码:
class Solution {
public String reverseWords ( String s) {
char [ ] ch = s. toCharArray ( ) ;
reverseString ( ch, 0 , ch. length - 1 ) ;
int k = 0 , wordEndIndex = 0 ;
for ( int i = 0 ; i < s. length ( ) ; ++ i) {
if ( ch[ i] == ' ' ) {
continue ;
}
int wordStartIndex = i;
while ( i < ch. length && ch[ i] != ' ' ) {
++ i;
}
wordEndIndex = i - 1 ;
reverseString ( ch, wordStartIndex, wordEndIndex) ;
for ( int j = wordStartIndex; j <= wordEndIndex; ++ j) {
ch[ k++ ] = ch[ j] ;
}
if ( k < ch. length) {
ch[ k++ ] = ' ' ;
}
}
return new String ( ch, 0 , ( k - 1 == ch. length - 1 ) && ( ch[ k - 1 ] != ' ' ) ? k : k - 1 ) ;
}
public void reverseString ( char [ ] ch, int left, int right) {
while ( left < right) {
ch[ left] ^= ch[ right] ;
ch[ right] ^= ch[ left] ;
ch[ left] ^= ch[ right] ;
++ left;
-- right;
}
}
}
5、左旋转字符串
题目:https://leetcode.cn/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ 思路:先整体反转整个字符串,再分别局部反转后面n个字符和前面剩下的字符即可
双指针法反转整个字符串,此时前n个到后面n位,后面剩下的到了前面,但前后的顺序都是反的; 双指针局部反转后n个,调回其原来字符顺序 局部反转前面剩下的,回到原来字符顺序 代码实现:
class Solution {
public String reverseLeftWords ( String s, int n) {
char [ ] ch = s. toCharArray ( ) ;
int length = ch. length;
reverseString ( ch, 0 , length - 1 ) ;
reverseString ( ch, length - n, length - 1 ) ;
reverseString ( ch, 0 , length - n - 1 ) ;
return new String ( ch) ;
}
public void reverseString ( char [ ] ch, int left, int right) {
while ( left < right) {
ch[ left] ^= ch[ right] ;
ch[ right] ^= ch[ left] ;
ch[ left] ^= ch[ right] ;
left++ ;
right-- ;
}
}
}
6、实现strStr()
题目:https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/ 思路:KMP模式匹配算法 !!!避免重复遍历情况,当模式串与文本串匹配的过程中,当前面匹配了一部分,到某一个字符不匹配了,并不是要回退到两个串本轮匹配起始的下一个位置重新开始匹配。固定文本串不匹配位置索引;前缀表用来记录下一字符匹配不上时,以当前字符结尾的子串如果有最长相等前后缀,这里面的后缀和前缀相等,后缀又和文本串里面匹配上了,那么前缀自然也是匹配的,没必要再重复过来判断,直接判断前缀的下一位和文本串固定着的不匹配位置就ok了;如果匹配上了,各自向后移动一位再判定就是了,注意判断是否完全匹配完了。
先构建前缀表 初始化模式串匹配起始索引 循环文本串字符,判断是否匹配 不匹配:根据前缀表回退 匹配:模式串索引后移一位等待与文本串下一字符的匹配判断 判断模式串是否已经全部匹配上,是返回结果 前缀表next构建:
前缀指的是当前串不包括末尾字符的所有以第一个字符开始的子串,后缀指不包括当前串首个字符在内的所有以最后一个字符结尾的子串; 构建步骤:首先初始化数据,next[0] = 0,只有一个字符的串根据前后缀定义不包含该字符,直接为0,初始化判定相等前后缀中控制前缀的索引 j; 循环模式串的每个字符,统计其最长相等前后缀 如果没匹配上,要回退 j,j 一定要回退到起始位置重新来判断吗?这里个人感觉又是一遍KMP模式匹配算法的体现,以模式串 "abcabdabcabx"
为例,当计算x的前缀表的时候,j刚进来应该处在d的位置,这时候相当于在拿前面的"abcabd"
去匹配"abcabx"
了,同样的道理,d和x没匹配上,但是这个时候d前面的"abcab"
和后面的"abcab"
是匹配上了,我们这个时候回退 j,也应该考虑他前面的最长相等前后缀,d 前面紧挨的 ab 和 x 前面的 ab 是匹配的,那么前一个 ab 的相等前缀就也和 x 前面的 ab 匹配,直接判定 c 和 x 是否匹配就行了,即 j 回退到 next[j - 1] 如果匹配上了,j 后移以为,i 通过 for 循环控制后移以为,等待下一位匹配判断。 代码实现:
class Solution {
public int strStr ( String haystack, String needle) {
if ( haystack == null ) {
return - 1 ;
}
int [ ] next = new int [ needle. length ( ) ] ;
getNext ( needle, next) ;
int j = 0 ;
for ( int i = 0 ; i < haystack. length ( ) ; ++ i) {
while ( j > 0 && needle. charAt ( j) != haystack. charAt ( i) ) {
j = next[ j - 1 ] ;
}
if ( needle. charAt ( j) == haystack. charAt ( i) ) {
++ j;
}
if ( j == needle. length ( ) ) {
return i - needle. length ( ) + 1 ;
}
}
return - 1 ;
}
public void getNext ( String s, int [ ] next) {
next[ 0 ] = 0 ;
int j = 0 ;
for ( int i = 1 ; i < s. length ( ) ; ++ i) {
while ( j > 0 && s. charAt ( j) != s. charAt ( i) ) {
j = next[ j - 1 ] ;
}
if ( s. charAt ( j) == s. charAt ( i) ) {
j++ ;
}
next[ i] = j;
}
}
}
7、重复的子字符串
题目:https://leetcode.cn/problems/repeated-substring-pattern/ 思路:使用的是KMP模式匹配算法思路,重点是分析出来如何将判断的条件结合起来了KMP——如果一个字符串由子串重复构成,那么最后一个元素的最长前缀表起始体现了这个子串,如 "abcabcabcabc"
,最长相等前后缀中前缀是前三个 abc,后缀是后三个 abc,可以看到整个字符串抛出后缀后,就是重复那个子串!此时,整个字符串的长度是 n*x,x是那个子串的长度,那么最长相等前后缀的长度就是 (n - 1) * x,所以有 n*x % x == 0
,x可以通过字符串长度减去最长相等前后缀长度获得,而最长相等前后缀长度其实就是最后一个字符的前缀表值(前缀的下一位)。
构建前缀表 计算字符串和最长相等前后缀长度的差值; 取余判断,注意避免字符串中没有相等前后缀的情况(在构建前缀表以后直接排除掉)。 代码实现:
class Solution {
public boolean repeatedSubstringPattern ( String s) {
int [ ] next = new int [ s. length ( ) ] ;
getNext ( s, next) ;
if ( next[ s. length ( ) - 1 ] == 0 ) {
return false ;
}
int diff = s. length ( ) - next[ s. length ( ) - 1 ] ;
System . out. println ( diff) ;
if ( s. length ( ) % diff == 0 ) {
return true ;
}
return false ;
}
public void getNext ( String s, int [ ] next) {
next[ 0 ] = 0 ;
int j = 0 ;
for ( int i = 1 ; i < s. length ( ) ; ++ i) {
while ( j > 0 && s. charAt ( j) != s. charAt ( i) ) {
j = next[ j - 1 ] ;
}
if ( s. charAt ( j) == s. charAt ( i) ) {
j++ ;
}
next[ i] = j;
}
}
}
8、总结
双指针法:反转字符串,替换空格(注意提前扩容,从后往前操作) 反转字符串:整体反转和局部反转结合 KMP:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀