对于字符串相关的题目,双指针解法出现的频率非常高。
 
反转字符串II 相关题目:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public  String reverseStr (String s, int  k)   {    char [] chars = s.toCharArray();     for  (int  i = 0 ; i < chars.length; i += 2 *k) {         int  left = i, right = left + k - 1 ;         if  (chars.length - left < k) {             right = chars.length - 1 ;         }         while  (left < right) {             char  temp = chars[left];             chars[left] = chars[right];             chars[right] = temp;             left++;             right--;         }     }     return  String.valueOf(chars); } 
 
替换空格 相关题目:
第一种解法:暴力解法。
1 2 3 4 5 6 7 8 9 10 11 public  String replaceSpace (String s)   {    StringBuilder stringBuilder = new  StringBuilder();     for  (int  i = 0 ; i < s.length(); i++) {         if  (s.charAt(i) == ' ' ) {             stringBuilder.append("%20" );         } else  {             stringBuilder.append(s.charAt(i));         }     }     return  stringBuilder.toString(); } 
 
在 Java 中,StringBuilder 的 apend  方法中有对底层数组进行扩容的处理,如果发现添加字符后数组长度不够了,就会进行数组扩容的处理:创建一个新长度的数组,将原数组的元素复制过去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public  AbstractStringBuilder append (String str)   {    if  (str == null )         return  appendNull();     int  len = str.length();     ensureCapacityInternal(count + len);     str.getChars(0 , len, value, count);     count += len;     return  this ; } private  void  ensureCapacityInternal (int  minimumCapacity)   {         if  (minimumCapacity - value.length > 0 ) {         value = Arrays.copyOf(value,                 newCapacity(minimumCapacity));     } } 
 
如果字符长度比较长,使用上面的处理就会经历多次数组扩容。因此就有了第二种解法:数组填充法 。先预先给数组进行扩容 ,然后再将原数组的元素 从后向前  填充到扩容的新数组中。填充时使用双指针法进行填充,左指针指向原数组的尾端,右指针指向新数组的尾端。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public  String replaceSpace (String s)   {         int  spaceCount = 0 ;     for  (int  i = 0 ; i < s.length(); i++) {         if  (s.charAt(i) == ' ' ) {             spaceCount++;         }     }          char [] chars = new  char [s.length() + spaceCount * 2 ];     int  left = s.length() - 1 , right = chars.length - 1 ;     while  (left >= 0 ) {         if  (s.charAt(left) == ' ' ) {             chars[right] = '0' ;             chars[--right] = '2' ;             chars[--right] = '%' ;         } else  {             chars[right] = s.charAt(left);         }         left--;         right--;     }     return  String.valueOf(chars); } 
 
反转字符串中的单词 相关题目:
暴力解法,直接使用 Java 中的 split() 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public  String reverseWords (String s)   {         StringBuilder stringBuilder = new  StringBuilder();     String[] strArr = s.split(" " );     for  (int  i = strArr.length - 1 ; i >= 0 ; i--) {                  if  (!strArr[i].equals("" )) {                          if  (stringBuilder.length() > 0 ) {                 stringBuilder.append(" " );             }             stringBuilder.append(strArr[i]);         }     }     return  stringBuilder.toString(); } 
 
直接用 split() 函数有些胜之不武,下面用双指针法进行处理,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public  String reverseWords (String s)   {         StringBuilder stringBuilder = new  StringBuilder();          int  left = s.length() - 1 , right = s.length();     while  (left >= 0 ) {                           while  (left > 0  && (s.charAt(left) == ' '  || s.charAt(left - 1 ) != ' ' )) {                          if  (s.charAt(left) == ' '  && s.charAt(left - 1 ) != ' ' ) {                 right = left;             }             left--;         }                  if  (s.charAt(left) != ' ' ) {             if  (stringBuilder.length() > 0 ) {                 stringBuilder.append(' ' );             }                          stringBuilder.append(s, left, right);         }                  left--;     }     return  stringBuilder.toString(); } 
 
上面还是使用了 Java 的内置函数 subString(),下面尝试一下完全不使用 Java 的内置函数进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public  String reverseWords (String s)   {              char [] chars = s.toCharArray();          int  slow = 0 ;     for  (int  fast = 0 ; fast < chars.length; fast++) {         if  (chars[fast] != ' ' ) {                          if  (slow > 0 ) {                 chars[slow++] = ' ' ;             }                          while  (fast < chars.length && chars[fast] != ' ' ){                 chars[slow++] = chars[fast++];             }         }     }          char [] newChars = new  char [slow];     System.arraycopy(chars, 0 , newChars, 0 , slow);          reverse(newChars, 0 , newChars.length - 1 );          int  left = 0 ;     for  (int  right = 0 ; right <= newChars.length; right++) {                  if  (right == newChars.length || newChars[right] == ' ' ) {                          reverse(newChars, left, right - 1 );                          left = right + 1 ;         }     }     return  new  String(newChars); } public  void  reverse (char [] chars, int  left, int  right)   {    while  (left < right) {                  chars[left] ^= chars[right];         chars[right] ^= chars[left];         chars[left] ^= chars[right];         left++;         right--;     } } 
 
左旋转字符串 相关题目:
直接用 Java 的内置函数 subString 完成暴力解法:
1 2 3 4 public  String reverseLeftWords (String s, int  n)   {         return  s.substring(n) + s.substring(0 , n); } 
 
尝试一下不用内置函数来解答,解题思路是 先翻转局部,再翻转整体 ,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public  String reverseLeftWords (String s, int  n)   {              char [] chars = s.toCharArray();          reverse(chars, 0 , n - 1 );          reverse(chars, n, s.length() - 1 );          reverse(chars, 0 , s.length() - 1 );     return  new  String(chars); } public  void  reverse (char [] chars, int  left, int  right)   {    while  (left < right) {         chars[left] ^= chars[right];         chars[right] ^= chars[left];         chars[left] ^= chars[right];         left++;         right--;     } } 
 
找出字符串中第一个匹配项的下标 相关题目:
先看下常规解法,一般称之为朴素解法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public  int  strStr (String haystack, String needle)   {                        for  (int  i = 0 ; i <= haystack.length() - needle.length(); i++) {                  int  a = i, b = 0 ;         while  (b < needle.length() && haystack.charAt(a) == needle.charAt(b)) {                          a++;             b++;         }                  if  (b == needle.length()) {             return  i;         }     }     return  -1 ; } 
 
此题引出一个新算法:KMP算法,这个算法的核心是PMT数组(部分匹配表)。 
KMP算法的核心思想是:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。 
而 在一个串中查找是否出现过另一个串  是KMP算法的看家本领,本题就是这类题目。
关于这个算法的概念可以看下这篇文章,讲的比较清晰:如何更好地理解和掌握 KMP 算法? 
下面说说我的理解:
  在原KMP算法的定义中,有一个PMT部分匹配表,它的定义是:下标i之前(包括i)的字符串中,[前缀集合] 和 [后缀集合] 的交集中最长元素的长度  
  假设现在有一个主字符Main,和一个待匹配字符Slave,遍历主字符的指针是i,遍历待匹配字符的指针是j。我们先为待匹配字符创建PMT数组,如果在进行字符匹配时,在主字符的第i位、PMT表的第j位发生了失配,那么就去找第j-1位对应的值PMT[j - 1],这个值记为flag:主字符串中从第i - flag到第i-1位的子字符串,一定与待匹配字符串的第0位到第flag位的子字符串相同。  
  在对PMT数组进行实际应用时,会对它做一个变式:上面在第j位失配,我们需要找第PMT[j-1]处的值,为了方便,这里直接将PMT数组向后偏移一位,记为next数组。next[0]定义为-1只是为了编程方便,这样之后如果在第j位失配,直接找第next[j]位的值即可算出指针需要前移多少位. 
 
下面看一下这题的解法,首先是解法1,对PMT数组实行向右偏移一位的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public  int  strStr (String haystack, String needle)   {              int [] next = getKMPNext(needle);     int  j = -1 ;     for  (int  i = 0 ; i < haystack.length(); i++) {                           while  (j >= 0  && haystack.charAt(i) != needle.charAt(j + 1 )) {             j = next[j];         }                  if  (haystack.charAt(i) == needle.charAt(j + 1 )) {             j++;         }                  if  (j == needle.length() - 1 ) {             return  (i - needle.length() + 1 );         }     }     return  -1 ; } public  int [] getKMPNext(String needle) {    int [] next = new  int [needle.length()];     int  j = -1 ;     next[0 ] = j;     for  (int  i = 1 ; i < next.length; i++) {                  while  (j >= 0  && needle.charAt(i) != needle.charAt(j + 1 )) {                          j = next[j];         }                  if  (needle.charAt(i) == needle.charAt(j + 1 )) {             j++;         }                  next[i] = j;     }     return  next; } 
 
接着是解法2,对PMT数组保持原样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41  public  int  strStr (String haystack, String needle)   {               int [] next = getKMPNext(needle);     int  j = 0 ;     for  (int  i = 0 ; i < haystack.length(); i++) {                  while  (j > 0  && haystack.charAt(i) != needle.charAt(j)) {             j = next[j - 1 ];         }                  if  (haystack.charAt(i) == needle.charAt(j)) {             j++;         }                  if  (j == needle.length()) {             return  (i - needle.length() + 1 );         }     }     return  -1 ; } public  int [] getKMPNext(String needle) {    int [] next = new  int [needle.length()];     int  j = 0 ;     next[0 ] = j;     for  (int  i = 1 ; i < next.length; i++) {                  while  (j > 0  && needle.charAt(i) != needle.charAt(j)) {                          j = next[j - 1 ];         }                  if  (needle.charAt(i) == needle.charAt(j)) {             j++;         }                  next[i] = j;     }     return  next; } 
 
最长快乐前缀 相关题目:
「快乐前缀」 是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。
 
看下 KMP 算法中 PMT 表的定义:下标i之前(包括i)的字符串中,[前缀集合] 和 [后缀集合] 的交集中最长元素的长度 。再看看上面对快乐前缀的定义,要求的是最长的快乐前缀,那么就是说求的是 PMT 表中最后一个元素的值。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26  public  String longestPrefix (String s)   {               int [] next = getNextArr(s);          int  maxLength = next[next.length - 1 ];     if  (maxLength > 0 ) {         return  s.substring(0 , maxLength);     }     return  "" ; } public  int [] getNextArr(String s) {    int  j = 0 ;     int [] next = new  int [s.length()];     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;     }     return  next; } 
 
重复的子字符串 相关题目:
解法1:使用 KMP算法 。
思路:如果s满足条件,那么s+s也满足条件,将s+s的首尾字符去除,如果其中能找到一个s,说明匹配。这个进行的就是 在一个串中查找是否出现过另一个串  的操作,因此可以使用 KMP 算法处理,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public  boolean  repeatedSubstringPattern (String s)   {              int [] next = getNextArr(s);     int  j = 0 ;          String newStr = s + s;          for  (int  i = 1 ; i < newStr.length() - 1 ; i++) {         while  (j > 0  && newStr.charAt(i) != s.charAt(j)) {             j = next[j - 1 ];         }         if  (newStr.charAt(i) == s.charAt(j)) {             j++;         }         if  (j == s.length()) {             return  true ;         }     }     return  false ; } public  int [] getNextArr(String s) {    int [] next = new  int [s.length()];     int  j = 0 ;     next[0 ] = j;     for  (int  i = 1 ; i < next.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;     }     return  next; } 
 
字符串轮转 相关题目:
此题与上面一道 [459.重复的子字符串] 相似,解题思路是:如果s2是由s1旋转而成,那么s2+s2中肯定包含s1 。因此也可以用 KMP 算法处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public  boolean  isFlipedString (String s1, String s2)   {              if  (s1.length() != s2.length()) {         return  false ;     }     if  (s1.equals("" )) {         return  true ;     }     int [] next = getNext(s1);     String oStr = s2 + s2;     int  j = 0 ;     for  (int  i = 0 ; i < oStr.length(); i++) {         while  (j > 0  && oStr.charAt(i) != s1.charAt(j)) {             j = next[j - 1 ];         }         if  (oStr.charAt(i) == s1.charAt(j)) {             j++;         }         if  (j == s1.length()) {             return  true ;         }     }     return  false ; } public  int [] getNext(String s) {    int  j = 0 ;     int [] next = new  int [s.length()];     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;     }     return  next; } 
 
当然没必要整那么复杂,简单一点的解法:
1 2 3 4 5 6 7 8 9 10 public  boolean  isFlipedString (String s1, String s2)   {         if  (s1.length() != s2.length()) {         return  false ;     }     if  ((s2 + s2).contains(s1)) {         return  true ;     }     return  false ; } 
 
总结 关于字符串相关的题目,有以下几个关键点:
  如果题目中出现 旋转 、匹配子字符串返回下标 、一个字符串是否可以由另一个字符串构成  的描述,都可以思考它是否满足或者是否可以将其转换为 在一个串中查找是否出现过另一个串  的处理,如果满足或者转换后满足,那么就可以用 KMP 算法来解答。 
  双指针法在数组,链表和字符串中很常用。很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。 
  字符串翻转系列的题目,可以考虑对字符串实行 整体 + 部分  翻转的操作。