简介
字符串是若干字符组成的有限序列,也可以理解为是一个字符数组。在做字符串类的算法题时,API方法对我们的诱惑特别的大,因为主流语言都会有很多处理字符串的方法,我们的题目有时两三行API就能直接被解决了,这显然不是我们刷题的目标,就算只为了过面试也不能直接调API,这样只能回去等通知了。API也不是不能用,当我们觉得,我们的API不影响题目考察的重点时,就可以放心使用!
理论基础
字符串在存储上类似字符数组,所以它每一位的单个元素都是可以提取的,如s=“abcdefghij”,则s[1]=“b”,s[9]=“j”,这可以给我们提供很多方便,如高精度运算时每一位都可以转化为数字存入数组。觉见的高级语言都有很多处理字符串的方法如:连接、求子串、删除子串、插入子串、求长度、搜索子串位置、大写转换等。我们平时编程处理最多也是字符串,这里就不过多介绍了。最后做字符串类的题目,有时候会用到KMP算法,不了解的同学,可以参考这篇文章:一文读懂 KMP 字符串查找算法。
解题心得
- 有时可把字符串类的题看做char类型的数组题,很多问题就迎刃而解了。
- 解题时常会用到递归、双指针、滑动窗口、反转等技巧。
- 有时字符串算法题可能会用到KMP算法,我们需要重点了解该算法。
- 做字符串算法题时,不可太过依赖API方法。
- 字符串类的题目,往往想法很简单,实现起来很考验对代码的掌控能力。
算法题目
6. Z 字形变换
题目解析:根据Z字形生成规律,直接构造结果字符串。
代码如下:
/**
* 字符串
*/
class Solution {
public String convert(String s, int numRows) {
if (numRows == 1) {
return s;
}
StringBuilder ret = new StringBuilder();
int n = s.length();
int cycleLen = 2 * numRows - 2;
for (int i = 0; i < numRows; i++) {
// 每次加一个周期
for (int j = 0; j + i < n; j += cycleLen) {
ret.append(s.charAt(j + i));
// 除去第 0 行和最后一行
if (i != 0 && i != numRows - 1 && j + cycleLen - i < n) {
ret.append(s.charAt(j + cycleLen - i));
}
}
}
return ret.toString();
}
}
8. 字符串转换整数 (atoi)
题目解析:从左往右依次处理字符串,去掉无用字符,判断正负,确定数字,最后大小值控制在Integer.MAX_VALUE内即可。
代码如下:
/**
* 字符串
*/
class Solution {
public int myAtoi(String s) {
int sign = 1;
int ans = 0, pop = 0;
//代表是否开始转换数字
boolean hasSign = false;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '-' && !hasSign) {
sign = -1;
hasSign = true;
continue;
}
if (s.charAt(i) == '+' && !hasSign) {
sign = 1;
hasSign = true;
continue;
}
if (s.charAt(i) == ' ' && !hasSign) {
continue;
}
if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
hasSign = true;
pop = s.charAt(i) - '0';
if (ans * sign > Integer.MAX_VALUE / 10 || (ans * sign == Integer.MAX_VALUE / 10 && pop * sign > 7)) {
return 2147483647;
}
if (ans * sign < Integer.MIN_VALUE / 10 || (ans * sign == Integer.MIN_VALUE / 10 && pop * sign < -8)) {
return -2147483648;
}
ans = ans * 10 + pop;
} else {
return ans * sign;
}
}
return ans * sign;
}
}
14. 最长公共前缀
题目解析:所有字符串从左往右开始比较即可。
代码如下:
/**
* 字符串
*/
class Solution {
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0)
return "";
StringBuilder res = new StringBuilder();
for (int i = 0; i < strs[0].length(); i++) {
Character tmp = strs[0].charAt(i);
for (int j = 0; j < strs.length; j++) {
// 如果字符串长度不足或字符不相等,立即中止比较
if (strs[j].length() - 1 >= i && tmp == strs[j].charAt(i)) {
// 比较完一轮后,再添加字符
if (j == strs.length - 1) {
res.append(tmp);
}
} else {
break;
}
}
if (res.length() - 1 != i) {
break;
}
}
return res.toString();
}
}
28. 实现 strStr()
题目解析:这题看上去是道简单题,其实要用KMP算法,再这里骗,偷袭我们这些老师傅。用KMP算法可以提高效率到O(n + m)。不了解KMP算法,可以参考这篇文章:一文读懂 KMP 字符串查找算法。
代码如下:
/**
* 字符串
*/
class Solution {
/**
* 获取当前模式串的 next[] (回退表)
* 这里没有右移一位,如果需要右移,所有值减1即可
*/
public void getNext(int[] next, String s){
int j = 0;
next[0] = 0;
for(int i = 1; i < s.length(); i++) {
// j要保证大于0,因为下面有取j-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;
}
}
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;
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;
}
}
38. 外观数列
题目解析:采用递归,描述的时候注意多个相同数字要合并描述即可。
代码如下:
/**
* 递归
*/
class Solution {
public String countAndSay(int n) {
// 递归出口
if (n == 1) return "1";
// 递归调用
String result = countAndSay(n - 1);
StringBuilder temp = new StringBuilder();
for (int j = 0; j < result.length(); j++) {
int count = 1;
char c = result.charAt(j);
while (j < result.length() - 1 && c == result.charAt(j + 1)) {
// 计算该字符有多少个
count++;
// 指针向后移
j++;
c = result.charAt(j);
}
// 拼接
temp.append(count);
temp.append(c);
}
return temp.toString();
}
}
58. 最后一个单词的长度
题目解析:从后往前循环查找空格即可,这里需要注意字符串最末尾的空格需要排除。
代码如下:
/**
* 字符串
*/
class Solution {
public int lengthOfLastWord(String s) {
int res = 0;
// 标志是否已开始统计单词长度
boolean flag = false;
for (int i = s.length() - 1; i >= 0; i--) {
// 开始统计单词后,遇到的第一个空格即为单词分隔
if (flag && s.charAt(i) == ' ') {
break;
}
// 计数共有多少字母
if (s.charAt(i) != ' ') {
flag = true;
res++;
}
}
return res;
}
}
65. 有效数字
题目解析:确定有限状态自动机,太难了,直接看官方答案吧。
代码如下:
/**
* 确定有限状态自动机
*/
class Solution {
public boolean isNumber(String s) {
Map<State, Map<CharType, State>> transfer = new HashMap<State, Map<CharType, State>>();
Map<CharType, State> initialMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
put(CharType.CHAR_SIGN, State.STATE_INT_SIGN);
}};
transfer.put(State.STATE_INITIAL, initialMap);
Map<CharType, State> intSignMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_POINT, State.STATE_POINT_WITHOUT_INT);
}};
transfer.put(State.STATE_INT_SIGN, intSignMap);
Map<CharType, State> integerMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_INTEGER);
put(CharType.CHAR_EXP, State.STATE_EXP);
put(CharType.CHAR_POINT, State.STATE_POINT);
}};
transfer.put(State.STATE_INTEGER, integerMap);
Map<CharType, State> pointMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
}};
transfer.put(State.STATE_POINT, pointMap);
Map<CharType, State> pointWithoutIntMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
}};
transfer.put(State.STATE_POINT_WITHOUT_INT, pointWithoutIntMap);
Map<CharType, State> fractionMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_FRACTION);
put(CharType.CHAR_EXP, State.STATE_EXP);
}};
transfer.put(State.STATE_FRACTION, fractionMap);
Map<CharType, State> expMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
put(CharType.CHAR_SIGN, State.STATE_EXP_SIGN);
}};
transfer.put(State.STATE_EXP, expMap);
Map<CharType, State> expSignMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
}};
transfer.put(State.STATE_EXP_SIGN, expSignMap);
Map<CharType, State> expNumberMap = new HashMap<CharType, State>() {{
put(CharType.CHAR_NUMBER, State.STATE_EXP_NUMBER);
}};
transfer.put(State.STATE_EXP_NUMBER, expNumberMap);
int length = s.length();
State state = State.STATE_INITIAL;
for (int i = 0; i < length; i++) {
CharType type = toCharType(s.charAt(i));
if (!transfer.get(state).containsKey(type)) {
return false;
} else {
state = transfer.get(state).get(type);
}
}
return state == State.STATE_INTEGER || state == State.STATE_POINT || state == State.STATE_FRACTION || state == State.STATE_EXP_NUMBER || state == State.STATE_END;
}
public CharType toCharType(char ch) {
if (ch >= '0' && ch <= '9') {
return CharType.CHAR_NUMBER;
} else if (ch == 'e' || ch == 'E') {
return CharType.CHAR_EXP;
} else if (ch == '.') {
return CharType.CHAR_POINT;
} else if (ch == '+' || ch == '-') {
return CharType.CHAR_SIGN;
} else {
return CharType.CHAR_ILLEGAL;
}
}
enum State {
STATE_INITIAL,
STATE_INT_SIGN,
STATE_INTEGER,
STATE_POINT,
STATE_POINT_WITHOUT_INT,
STATE_FRACTION,
STATE_EXP,
STATE_EXP_SIGN,
STATE_EXP_NUMBER,
STATE_END
}
enum CharType {
CHAR_NUMBER,
CHAR_EXP,
CHAR_POINT,
CHAR_SIGN,
CHAR_ILLEGAL
}
}
127. 单词接龙
题目解析:从 beginWord 和 endWord 两边同时开始广度优先搜索,同时一层一层扩展,当发现某一时刻两边都访问过同一顶点时就停止搜索。
代码如下:
/**
* 双向广度优先搜索
*/
class Solution {
Map<String, Integer> wordId = new HashMap<String, Integer>();
List<List<Integer>> edge = new ArrayList<List<Integer>>();
int nodeNum = 0;
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
for (String word : wordList) {
addEdge(word);
}
addEdge(beginWord);
if (!wordId.containsKey(endWord)) {
return 0;
}
int[] disBegin = new int[nodeNum];
Arrays.fill(disBegin, Integer.MAX_VALUE);
int beginId = wordId.get(beginWord);
disBegin[beginId] = 0;
Queue<Integer> queBegin = new LinkedList<Integer>();
queBegin.offer(beginId);
int[] disEnd = new int[nodeNum];
Arrays.fill(disEnd, Integer.MAX_VALUE);
int endId = wordId.get(endWord);
disEnd[endId] = 0;
Queue<Integer> queEnd = new LinkedList<Integer>();
queEnd.offer(endId);
while (!queBegin.isEmpty() && !queEnd.isEmpty()) {
int queBeginSize = queBegin.size();
for (int i = 0; i < queBeginSize; ++i) {
int nodeBegin = queBegin.poll();
if (disEnd[nodeBegin] != Integer.MAX_VALUE) {
return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;
}
for (int it : edge.get(nodeBegin)) {
if (disBegin[it] == Integer.MAX_VALUE) {
disBegin[it] = disBegin[nodeBegin] + 1;
queBegin.offer(it);
}
}
}
int queEndSize = queEnd.size();
for (int i = 0; i < queEndSize; ++i) {
int nodeEnd = queEnd.poll();
if (disBegin[nodeEnd] != Integer.MAX_VALUE) {
return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;
}
for (int it : edge.get(nodeEnd)) {
if (disEnd[it] == Integer.MAX_VALUE) {
disEnd[it] = disEnd[nodeEnd] + 1;
queEnd.offer(it);
}
}
}
}
return 0;
}
public void addEdge(String word) {
addWord(word);
int id1 = wordId.get(word);
char[] array = word.toCharArray();
int length = array.length;
for (int i = 0; i < length; ++i) {
char tmp = array[i];
array[i] = '*';
String newWord = new String(array);
addWord(newWord);
int id2 = wordId.get(newWord);
edge.get(id1).add(id2);
edge.get(id2).add(id1);
array[i] = tmp;
}
}
public void addWord(String word) {
if (!wordId.containsKey(word)) {
wordId.put(word, nodeNum++);
edge.add(new ArrayList<Integer>());
}
}
}
165. 比较版本号
题目解析:字符串分割,然后分区比较,即可得到对比结果。
代码如下:
/**
* 字符串分割
*/
class Solution {
public int compareVersion(String version1, String version2) {
String[] v1 = version1.split("\\.");
String[] v2 = version2.split("\\.");
for (int i = 0; i < v1.length || i < v2.length; ++i) {
int x = 0, y = 0;
if (i < v1.length) {
x = Integer.parseInt(v1[i]);
}
if (i < v2.length) {
y = Integer.parseInt(v2[i]);
}
if (x > y) {
return 1;
}
if (x < y) {
return -1;
}
}
return 0;
}
}
214. 最短回文串
题目解析:此题也是用KMP算法,可以参考这篇文章:一文读懂 KMP 字符串查找算法。
代码如下:
/**
* KMP 算法
*/
class Solution {
public String shortestPalindrome(String s) {
int n = s.length();
int[] fail = new int[n];
Arrays.fill(fail, -1);
for (int i = 1; i < n; ++i) {
int j = fail[i - 1];
while (j != -1 && s.charAt(j + 1) != s.charAt(i)) {
j = fail[j];
}
if (s.charAt(j + 1) == s.charAt(i)) {
fail[i] = j + 1;
}
}
int best = -1;
for (int i = n - 1; i >= 0; --i) {
while (best != -1 && s.charAt(best + 1) != s.charAt(i)) {
best = fail[best];
}
if (s.charAt(best + 1) == s.charAt(i)) {
++best;
}
}
String add = (best == n - 1 ? "" : s.substring(best + 1));
StringBuffer ans = new StringBuffer(add).reverse();
ans.append(s);
return ans.toString();
}
}