344. 反转字符串
题目:力扣
只能够用O(1)的空间复杂度,那就是在原数组上进行修改字符 -->可以理解成按照中间轴进行左右翻转。
因此在找到中间值之后,进行循环翻转即可。
public void reverseString(char[] s) {
int length = s.length;
int middle;
if(length%2 == 0){
middle= s.length/2-1;//偶数中间值
}else{
middle = s.length/2;//奇数中间值
}
for(int i=0; i<= middle; i++){
char temp = s[i];
s[i] = s[length-1-i];
s[length-1-i] = temp;
}
}
另一种写法:
public void reverseString(char[] s) {
int length = s.length;
for(int i = 0; i < length/2; i++){
char temp = s[i];
s[i] = s[length - i -1];
s[length - i -1] = temp;
}
}
用while循环来写
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--;
}
}
参考资料:代码随想录
541. 反转字符串II
题目:力扣
和上一题的解题思路是一样的,但是需要注意的是在每次截取2k长度的字符并翻转前k个的时候的条件:
- 如果剩余字符少于
k
个,则将剩余字符全部反转。 - 如果剩余字符小于
2k
但大于或等于k
个,则反转前k
个字符,其余字符保持原样。
因此进行翻转的right指针所指向的位置可能不是 i+k可能是s的最后一个字符。
此外,使用for循环翻转字符串注意index的转换。
public String reverseStr(String s, int k) {
//计数
char[] sl = s.toCharArray();
for(int i=0; i<sl.length; i+=2*k){
reverse(sl, i, Math.min(i+k, s.length()));
}
return new String(sl);
}
public void reverse(char[] sl, int left, int right){
for(int i=left; i<left+(right-left)/2;i++){
char temp = sl[i];
sl[i] = sl[right - 1 - (i-left)];
sl[right -1 - (i-left)] = temp;
}
}
使用while循环的写法
public String reverseStr(String s, int k) {
//极端条件
if(s == null|| s.length() == 1 || k <= 1){
return s;
}
char[] cl = s.toCharArray();
//遍历 - 注意i的递增step,以及用最小值比较选取需要反转的地方
for(int i = 0; i < s.length(); i = i+2*k){
reverse(cl, i, Math.min(i+k, s.length())-1);
}
return new String(cl);
}
//使用左右指针进行操作
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.替换空格
第一想法是遍历s,然后把是空格的地方用“%20”替换,最后生成一个new String返回。
public String replaceSpace(String s) {
StringBuffer str = new StringBuffer();
for(int i=0; i<s.length(); i++){
if(s.charAt(i) == ' '){
str.append("%20");
}else{
str.append(s.substring(i,i+1));
}
}
return str.toString();
}
但是如果不考虑采用额外的空间的话;
首先扩充数组到每个空格替换成"%20"之后的大小。
然后从后向前替换空格,也就是双指针法,过程如下:
i指向新长度的末尾,j指向旧长度的末尾。
注意:
其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
这么做有两个好处:
- 不用申请新数组。
- 从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。
public String replaceSpace(String s) {
//先判断有多少个空格 - 需要在s后增加多少位 1个空格就增加2位
StringBuffer str = new StringBuffer();
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还没有扩容的最后一位
//把需要增加的位先放到s中
s = s + str.toString();
char[] sl = s.toCharArray();
int right = s.length()-1; //s扩容后的最后一位
//如果left位是空格则把%20替换进right,如果没有空格那么right就用left位替换
//然后再一起往左走,一直到left碰到最左侧
while(left >= 0){
if(sl[left] == ' '){
sl[right] = '0';
right--;
sl[right] = '2';
right --;
sl[right] = '%';
left --;
right --;
}else{
sl[right] = sl[left];
left--;
right --;
}
}
return new String(sl);
}
总结:这道题主要是熟悉了双指针进行从后往前数组填充的一个方法思路,但是实现上,其实还是第一种更加方便,只用遍历一次,并且两者都需要采用一个新的StringBuffer。
参考资料:代码随想录
151.翻转字符串里的单词
第一反应是使用split()分割字符串;
这里需要注意的是 单词之间不止有一个空格,且很可能首位也有空格
因此需要先去除首位空格,在写正则表达式的时候需要注意。(也可以直接使用list的内置api - reverse()把list进行翻转)
public String reverseWords(String s) {
//去除首位空格
s = s.trim();
//匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
//匹配任何空白字符,包括空格、制表符、换页符等等。 \s
String[] sl = s.split("\\s+");
int left = 0;
int right = sl.length-1;
while(left < right){
String temp = sl[left];
sl[left] = sl[right];
sl[right] = temp;
left ++;
right --;
}
return String.join(" ", sl);
}
那么如果不采用内置的api实现的话,那就和上一题一样,只能在原字符串改变。
所以解题思路如下:
- 移除多余空格
- 将整个字符串反转
- 将每个单词反转
代码实现运用的是之前题目中出现过的方法:
class Solution {
public String reverseWords(String s) {
//移除多余空格
char[] nsl = removeSpace(s);
//将整个字符串翻转 - 单词反了
reverseWhole(nsl);
//将每个单词翻转
reverseEachWord(nsl);
return new String(nsl);
}
public char[] removeSpace(String s){
//去除首位空格
int left = 0;
while(s.charAt(left) == ' '){
left++;
}
//去除末端空格
int right = s.length()-1;
while(s.charAt(right) == ' '){
right--;
}
//移除单词中多余空格
StringBuffer ns = new StringBuffer();
for(int i=left; i<=right; i++){
if(s.charAt(i) == ' '){
ns.append(s.substring(i, i+1));
//如果是空格,则向右移动
while(s.charAt(i) == ' '){
i++;
}
//此时移动到非空格处了,但是for循环有个i++,所以还要再退回来一步
i--;
}else{
ns.append(s.substring(i, i+1));
}
}
return ns.toString().toCharArray();
}
public void reverseWhole(char[] nsl){
int left = 0;
int right = nsl.length-1;
while(left < right){
char temp = nsl[left];
nsl[left] = nsl[right];
nsl[right] = temp;
left++;
right--;
}
}
public void reverseEachWord(char[] nsl){
int left = 0;
int index = 0;
while(index < nsl.length){
//找到一个单词的尾端
while(index < nsl.length && nsl[index] != ' '){
index++;
}
int right = index -1;
while(left < right){
char temp = nsl[left];
nsl[left] = nsl[right];
nsl[right] = temp;
left++;
right--;
}
//下一个单词的头端
index++;
left = index;
}
}
}
移除单词中的空格此处用的是StringBuffer,可以尝试使用27.移除元素中快慢指针的方法,把空间复杂度降到O(1)。
public char[] removeSpace(String s){
//去除首位空格
int left = 0;
while(s.charAt(left) == ' '){
left++;
}
//去除末端空格
int right = s.length()-1;
while(s.charAt(right) == ' '){
right--;
}
char[] sl = s.toCharArray();
//快慢指针
int slow = 0;
int fast = left;//注意快指针得指向去除首部空格的index
while(fast <=right){//快指针末端指向去除尾部空格的index
while(sl[fast] == ' ' && sl[fast+1] == ' '){//当发现有连续空格则快指针向后移动
fast++;
}
sl[slow] = sl[fast];//慢指针被快指针的index覆盖 - 其中一个空格也被保留覆盖
slow++;
fast++;
}
char[] nsl = new char[slow];
System.arraycopy(sl, 0, nsl, 0, slow);
return nsl;
}
剑指Offer58-II.左旋转字符串
题目:力扣
如果没有空间限制的话这道题很简单,遍历把前面到n的字符串作为新的字符串,把后面的也作为新的字符串,然后把前面的放到后面合并成一个新的字符串。
而如果限制在原字符串上改动的话就需要一些巧思。
可以采用局部翻转字符串+全局翻转字符串的方法:
- 翻转n前面的字符串
- 翻转n后面的字符串
- 翻转整个字符串
public String reverseLeftWords(String s, int n) {
if(n >= s.length()) return s;
char[] sl = s.toCharArray();
reverse(sl, 0, n-1);
reverse(sl, n, sl.length-1);
reverse(sl, 0, sl.length-1);
return new String(sl);
}
public void reverse(char[] sl, int left, int right){
while(left < right){
char temp = sl[left];
sl[left] = sl[right];
sl[right] =temp;
left++;
right--;
}
}
参考资料: 代码随想录
28. 实现 strStr() - 找出字符串中第一个匹配项的下标
题目:力扣
第一反应是使用java的内置函数 indexOf()。
class Solution {
public int strStr(String haystack, String needle) {
return haystack.indexOf(needle);
}
}
那么如果不用内置函数的话。
可以考虑采用暴力的办法:
- 从0处开始遍历字符串,当i位与模板串中的第一位相等时,那么开始往后一一匹配
- 若有一位不匹配,那么将i往后移动,直到与模板串中的第一位相等,再次匹配。
public int strStr(String haystack, String needle) {
if(needle == null || haystack == null || haystack.length() == 0){
return -1;
}
for(int i=0; i+ needle.length() <= haystack.length();i++){
boolean flag = true;
if(haystack.charAt(i) == needle.charAt(0)){
for(int j=0; j<needle.length();j++){
if(haystack.charAt(i+j) != needle.charAt(j)){
flag = false;
break;
}
}
if(flag){
return i;
}
}
}
return -1;
}
事实上,这道题就是典型的KMP题目。
KMP
为什么叫做KMP呢。
因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP。
KMP主要应用在字符串匹配上。
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
前缀表 - Next数组
前缀表有什么作用呢?
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
== 前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
举一个例子:要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时模式串就要从头匹配了。
但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。
前缀表是如何记录的呢?
记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
最长公共前后缀
= 最长相等前后缀
前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
在aabaaf中:
a,aa,aab,aaba,aabaa都是前缀
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
在aabaaf中:
f,af,aaf,baaf,abaaf都是后缀
前缀表要求的就是相同前后缀的长度。
例子: 字符串a的最长相等前后缀为0。 - 没有前后缀
字符串aa的最长相等前后缀为1。- 前缀a,后缀a
字符串aab的最长相等前后缀为0。 - 前缀 a,aa,后缀b,ab
字符串aabaa的最长相等前后缀为2。 - 前缀a,aa,aab,aaba,后缀a,aa,baa,abaa
字符串aabaaf的最长相等前后缀为0。- 前缀a,aa,aab,aaba,aabaa,后缀 f,af,aaf,baaf,abaaf
因此前缀表为 [0,1,0,1,2,0] - 对应aabaaf
为什么一定要用前缀表
按照前面的例子 - aabaabaafa 中查找是否出现过一个模式串:aabaaf。
aabaaf的前缀表为 [0,1,0,1,2,0]
aabaa【b】aafa 对比到这个位置的时候,发现b和模式串中的aabaa【f】不一样,而前面的部分是
aabaa
这个字符串的最长相等前后缀,在前缀表中为2
因此直接在这个模式串中跳到index为2的位置继续下一位和字符串进行匹配。
这样就不用从头开始匹配了。
时间复杂度分析
KMP算法 :n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
暴力算法 :O(n × m)
所以KMP在字符串匹配中极大的提高的搜索的效率。
如何代码实现前缀表(next数组)?
模式串:aabaaf
初始化next数组:
- next[0] = 0 //对于a子串来说,最长相等前后缀为0;
- int j = 0 - 后缀位置 - 对于aa子串,后缀为a
开始循环前缀位 - for(int i=1; I<s.length();i++) - 初始位i=1,对于aa子串,前缀为a
此时s[i]==s[j],则将j向右一位,j++,更新next[1] = j = 1 //对于aa子串来说,最长相等前后缀为1
j++,i++ //j=2,i=3 - 对于子串aab,判断前缀aa和后缀ab是否相等
此时s[i] !=s[j], 回退 j = next[j-1] = next[0] = 0 - 对于子串aab,继续判断前缀a和后缀b是否相等
此时s[i] !=s[j],但是j已经回到了初始位,因此更新next[2] = j = 0
...
通俗来讲:
一个子串的最长相等先后缀是这个子串中的某个前缀的最长相等前后缀+1。若s[i]==s[j],说明最长前后缀加一位;s[i]!=s[j],则在j之前的后缀字符中回退查找是否存在s[i]==s[j]。
这里用的其实是一个动态规划的思路。
- dp[i] = j, 若s[i]==s[j] j++ | s[i] !=s[j] while(j>0 && j=next[j-1] ){s[i]==s[j] j++}
- dp[i] = 0, s[i] !=s[j] when j=0
若j与i相等则更新next中的i位置为j位处+1;若不相等则回退j到next[j-1]对应的index,一直到初始位置;若仍然不相等,则为0。
public void getNext(int[] next, String s){
int j = -1;
next[0] = j;
for (int i = 1; i<s.length(); i++){
while(j>=0 && s.charAt(i) != s.charAt(j+1)){
j=next[j];
}
if(s.charAt(i)==s.charAt(j+1)){
j++;
}
next[i] = j;
}
}
整体代码实现
public int strStr(String haystack, String needle) {
if(needle.length() ==0){
return 0;
}
int[] next = new int[needle.length()];
getNext(next, needle);
int j =0;//next数组起始位置
for(int i=0; i<haystack.length(); i++){//从0开始遍历文本串
while(j>0 && haystack.charAt(i) != needle.charAt(j)){//不匹配
j = next[j-1];//寻找之前匹配的位置
}
if(haystack.charAt(i) == needle.charAt(j)){//匹配,j和i同时向后移动
j++;
//i在for循环里增加
}
if(j == needle.length()){ //此时i在第一个匹配项的末端
return i-needle.length()+1;//找到i的前端
}
}
return -1;
}
public void getNext(int[] next, String needle){
int j = 0;//初始化后缀位
next[0] = 0;//初始化前缀表
for(int i=1; i<needle.length();i++){//注意初始化前缀位为1
while(j>0 && needle.charAt(i)!= needle.charAt(j)){//不匹配
j = next[j-1];//寻找后缀前一位匹配的位置
}
if(needle.charAt(i) == needle.charAt(j)){//匹配,j和i同时向后移动
j++;
}
next[i] = j;//更新前缀表
//i在for循环里增加
}
}
459. 重复的子字符串
暴力法:
如果一个长度为 n 的字符串 s 可以由它的一个长度为 n' 的子串 s'重复多次构成,那么:
- n 一定是 n'的倍数;
- s一定是 s 的前缀;
- 对于任意的 i∈[n′,n),有 s[i]=s[i−n′]
也就是说,sss 中长度为 n′的前缀就是 s′,并且在这之后的每一个位置上的字符 s[i],都需要与它之前的第 n′ 个字符 s[i−n′]相同。
小优化是,因为子串至少需要重复一次,所以 n′不会大于 n 的一半,我们只需要在 [1,n/2] 的范围内枚举 n‘即可。
代码实现:
public boolean repeatedSubstringPattern(String s) {
for(int i=1; i*2 <= s.length(); i++){//这里的i是前缀位 - 也可以理解成重复子串的长度,也就是n’
//因为子串至少需要重复一次,所以 n' 不会大于 n 的一半
if(s.length()%i == 0){// 若重复子串的长度可以整除s的长度
boolean match = true;
for(int j=i; j<s.length(); j++){ //j的起始位置为第二个重复子串的首位
if(s.charAt(j) != s.charAt(j-i)){//判断相邻两个重复子串中的相应位置是否相等
match = false;
break;
}
}
if(match){
return true;
}
}
}
return false;
}
参考资料:力扣
字符串拼接解法 - 字符串匹配
底层思想是字符串移位算法。
S 包含一个重复的子字符串,那么这意味着可以通过多次 “移位和换行”`字符串,并使其与原始字符串匹配。
例如:abcabc
移位一次:cabcab
移位两次:bcabca
移位三次:abcabc
现在字符串和原字符串匹配了,所以可以得出结论存在重复的子串
基于这个思想,可以每次移动k个字符,直到匹配移动 length - 1 次。但是这样对于重复字符串很长的字符串,效率会非常低。
更高效的解法:
可以创建一个新的字符串 str
,它等于原来的字符串 S
再加上 S
自身,这样其实就包含了所有移动的字符串。
比如字符串:S = acd,那么 str = S + S = acdacd
acd 移动的可能:dac、cda。其实都包含在了 str 中了。
==> 一开始 acd (acd) ,移动一次 ac(dac)d,移动两次 a(cda)cd。循环结束
可以直接判断 str
中去除首尾元素之后,是否包含自身元素。如果包含。则表明存在重复子串。
比如字符串:S = acd, str = S + S = acdacd,去掉首尾 str = cdac并不包含acd - 说明acd中不存在重复子串
原因:从题目可以知道重复字符串至少重复n(n>=2)次才满足,用s+s则至少2n次重复,破环第一个和最后一个,在n>=2的前提下2n-2>=n 恒成立,所以中间至少有一个重复n次的字符串。
使用内置方法解题
public boolean repeatedSubstringPattern(String s) {
String ns = s+s;
return ns.substring(1, ns.length()-1).contains(s);
}
参考资料:力扣
使用KMP算法解题
可以发现这道题已经变成了从文本串cdac寻找是否有子串和模式串acd匹配。
==>这道题变成了一道字符串匹配的问题
因此可以使用上一题中的KMP算法。
对于字符串adsfadsfadsf,尝试求它的next数组 - [0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]
可以发现一个规律:
next数组最后一位为adsfadsf的最长相等前后缀长度,字符串长度减去 这个长度 12-8 = 4 - 也就是重复子串的长度。
因此
当一个字符串由重复子串组成的,最长相等前后缀不包含的子串就是最小重复子串。
根据重复子串的规律,在求出next数组之后,求出相应的子串长度「长度不能等于0」 - 判断字符串长度是否为子串的倍数。
代码实现:
public boolean repeatedSubstringPattern(String s) {
if(s.length() == 0){
return false;
}
int[] next = new int[s.length()];
getNext(next, s);
if(next[s.length()-1] != 0 && s.length()%(s.length() - next[s.length()-1])==0){//判断长度不等于0;字符串中的剩下长度可以整除s
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;
}
}
参考资料:代码随想录
总结
双指针法在数组,链表和字符串中很常用。 - 翻转字符串
注意使用局部和全部翻转的方式。 - 翻转单词,左旋字符串
其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。- 移除元素,翻转单词
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
使用KMP可以解决两类经典问题:1.匹配问题;2.重复子串问题