本文提到的题目部分来自《程序员算法面试指南》一书,这本书的题目可以在牛客网进行在线编程练习。部分来自LeetCode。
KMP算法
提到字符串问题,首先想到的就是KMP算法,它用于解决字符串匹配问题,在字符串str(长度为N)中查找子串match(长度为M)出现的位置。暴力的解法是从左到右遍历str的每一个字符,将当前字符作为第一个字符是否与match匹配。若与match匹配到一半发现不匹配了,则回到str的下一个字符重新出发,match也回到第一个字符重新匹配。时间复杂度为O(MN)。这是由于每次都从头开始检查,没有利用之前遍历检查得到的信息来优化下一次遍历。KMP算法利用了这一信息,使得时间复杂度降为O(N+M),空间复杂度为O(M)。其主要思想为计算子串match的信息保存下来,使得在匹配发生失败时,不必再去检查str串已经遍历过的部分,且子串match的位置也是一直向后滑动。
首先给出需要计算子串match什么样的信息、以及这个信息应该如何计算问题的解答。
计算子串match的next数组,next数组的长度与子串match长度一致。next[i]的含义是,在match[i]之前的字符串match[1..i-1]中以match[i-1]结尾的后缀子串,与match[0..i-2]中以match[0]开始的前缀子串的最大匹配长度。(注意,后缀子串和前缀子串掐头去尾)。举例说明,如match="aaaab",next[4]的值是多少。后缀子串match[1..3]="aaa",前缀子串match[0..2]=="aaa",即next[4]的值为这一匹配的长度3。有了这一数组后,当不匹配发生时,便知应将子串match向后移动多少来进行下一次匹配检查。
如上图在红叉处发生不匹配,我们已经计算了其之前字符串的前缀和后缀的匹配长度,即蓝圈部分。现在只需将match的下一个匹配位置改为蓝圈的后一个位置(也就是我们在next数组中存储的值),继续与str进行匹配即可。因为这一位置之前肯定是与str匹配的。
计算next数组,从左到右依次求解。将next[0]规定为-1,next[1]根据以上定义计算其值为0。在求解next[i]时,可以利用之前已经计算过的next[0..i-1]的值。如下图,若i-1处的字符和next[i-1]处的字符相同,即之前字符的匹配长度又增加1,则next[i]=next[i-1]+1。否则,继续比较i-1处字符与next[next[i-1]]处字符是否相同。当跳到最左位置,即next[0]=-1时,令next[i]=0。
public static int[] nextArray(StringBuilder match) {
if (match.length() == 1) {
return new int[] {-1};
}
int[] next = new int[match.length()];
next[0] = -1;
next[1] = 0;
for (int i=2;i<match.length();i++) {
int cn = next[i-1];
while(cn != -1 && match.charAt(cn) != match.charAt(i)) {
cn = next[cn];
}
if (cn == -1) {
next[i] = 0;
} else {
next[i] = next[cn] + 1;
}
}
return next;
}
有了next数组,下面便可以进行字符串匹配。在str和match上分别定义一个指针,代表当前匹配位置,若两指针指向位置字符相同,则都向后移动一个位置。否则,将match的指针移到next[matchp]处,若match的指针已经移到0的位置,则将str的指针向后移动一个位置。
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
StringBuilder str = new StringBuilder(sc.nextLine());
StringBuilder match = new StringBuilder(sc.nextLine());
int strp = 0;
int matchp = 0;
int[] next = nextArray(match);
while (strp < str.length() && matchp < match.length()) {
if (str.charAt(strp) == match.charAt(matchp)) {
strp++;
matchp++;
} else if (matchp == 0) {
strp++;
} else {
matchp = next[matchp];
}
}
if (matchp == match.length()) {
System.out.println(strp - matchp);
} else {
System.out.println(-1);
}
}
字符串的调整
给定一个字符串chas[],其中只含有字母字符和“*”字符,现在想把所有“*”全部挪到chas的左边,字母字符移到chas的右边。完成调整函数。要求时间复杂度为O(N),空间复杂度为O(1)。
举例:输入"12**345",输出"**12345"。
利用倒着复制的技巧,遇到字母字符就复制,遇到*则不复制,遍历完整个字符串后,将左半部分全部用*填充。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
StringBuilder sb = new StringBuilder(sc.nextLine());
int index = sb.length() - 1;
for (int i=sb.length()-1;i>=0;i--) {
if (sb.charAt(i) != '*') {
sb.setCharAt(index,sb.charAt(i));
index--;
}
}
for (index=index;index>=0;index--) {
sb.setCharAt(index,'*');
}
System.out.println(sb.toString());
}
}
125.验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
输入: "A man, a plan, a canal: Panama"
输出: true
输入: "race a car"
输出: false
验证回文串是回文串问题中最简单的。使用两个指针分别从两端向中间移动,遇到非字母数字字符跳过,否则判断对应位是否相等,不相等则返回false,相等继续向中间遍历。
class Solution {
public boolean isPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
s = s.toLowerCase();
while (left < right) {
if ( !(s.charAt(left) >= 'a' && s.charAt(left) <= 'z' || s.charAt(left) >= '0' && s.charAt(left) <= '9')) {
left ++;
continue;
}
if ( !(s.charAt(right) >= 'a' && s.charAt(right) <= 'z' || s.charAt(right) >= '0' && s.charAt(right) <= '9')) {
right --;
continue;
}
if (s.charAt(left) == s.charAt(right)) {
left++;
right--;
} else {
return false;
}
}
return true;
}
}
5.最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
输入: "babad"
输出: "bab"
输入: "cbbd"
输出: "bb"
给出两个解法。(1) 选取每一个字符和字符间隔一共n+n-1个位置作为中心,向两边扩展,判断是否为回文串;(2) 将原字符串倒转,查询这两个字符串的最长公共子串,将这个问题转化成了一个动态规划问题。
class Solution {
public String longestPalindrome(String s) {
if (s.length() == 0) {
return s;
}
String ret = s.substring(0,1);
for(int i=0;i<s.length();i++) {
// 字符为中心
int left = i - 1;
int right = i + 1;
// 越界
while (left>=0 && right<s.length()) {
if (s.charAt(left) == s.charAt(right)) {
left--;
right++;