字符串的题目难度不一,涉及到的考点有字符串处理、字符串匹配(自动机、正则)、模拟,以及递归、动态规划等算法。
难度 | 题目 | 知识点 |
---|---|---|
☆ | 02. 替换空格 | 从后往前 |
☆☆ | 27. 字符串的排列 | 回溯,String 和 char[] 相互转化, ArrayList判重复,排序 |
34. 第一个只出现一次的字符 | hash统计出现次数,模拟 | |
43. 左旋转字符串 | 应用模运算 | |
☆ | 44. 反转单词序列 | 字符串处理,特殊情况 |
☆☆ | 49. 把字符串转换为整数 | 模拟,细节,原码,补码 |
☆☆☆☆ | 52. 正则表达式匹配 | 正则表达式,递归,动态规划 |
☆☆☆ | 53. 表示数值的字符串 | 正则表达式,模拟自动机 |
☆ | 54. 字符流中第一个不重复的字符 | char字符范围,借助标记数组 |
02. 替换空格
从后往前
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
题意分析
实现统计好空格数目从后往前写。问题是 str 所指的空间会内存泄露吧?
注意补上 ' '(应该不会有憨憨如我补了一个'
'...
27. 字符串的排列 ++
回溯,String 和 char[] 相互转化, ArrayList判重复,排序
题目描述
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输入描述:
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。
题意分析
对于这个问题,我们可以把它分解成固定第一个字母,然后求后面所有字符的全排列的子问题。盗用一下别人的图,如下:
注意
- 判重复。可能出现重复排列,所以在加入新排列之前用
arrayList.contains()
判断一下是否重复; - String 和 char[] 的相互转化。回溯过程要改字符串中的字符排列,因此,将
String
用str.toCharArray()
转化成char []
便于操作。生成新排列字符串时,用String.valueOf(char [] param)
将char[]
转化为String
。 - Collections.sort(ans) 排序。
Java Code
import java.util.ArrayList;
import java.util.Collections;
public class Solution {
ArrayList ans = new ArrayList<>();
public ArrayList Permutation(String str) {
if (str == null || str.length() == 0) {
return ans;
}
Permutation(str.toCharArray(), 0);
Collections.sort(ans);
return ans;
}
public void Permutation(char[] str, int start) {
if (start == str.length - 1) {
String newStr = String.valueOf(str);
if (!ans.contains(newStr))
ans.add(newStr);
return;
}
for (int i = start; i < str.length; i++) {
swap(str, start, i);
Permutation(str, start + 1);
swap(str, start, i);
}
}
private void swap(char[] str, int i, int j) {
char tmp = str[i];
str[i] = str[j];
str[j] = tmp;
}
}
34. 第一个只出现一次的字符
hash 统计出现次数,遍历
题目描述
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).
题意分析
因为是统计字母出现次数,所以可以用hash表统计。遍历两遍字符串,第一遍统计各字符串的出现次数,第二遍找到第一个出现的不重复字符。
Java Code
public class Solution {
public int FirstNotRepeatingChar(String str) {
int[] hash = new int[100];// -1 重复了,0 没出现,1 出现了1次
for (int i = 0; i < str.length(); i++) {
int c = str.charAt(i) - 'A';
if (hash[c] > 0) hash[c] = -1;
else if (hash[c] == 0) hash[c] = 1;
}
for (int i = 0; i < str.length(); i++) {
int c = str.charAt(i) - 'A';
if (hash[c] == 1) return i;
}
return -1;
}
}
43. 左旋转字符串
应用模运算
题目描述
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
题意分析
应用模运算。
Java Code
public class Solution {
public String LeftRotateString(String str, int n) {
if (str == null || str.length() == 0) return "";
int len = str.length();
char[] ans = new char[len];
for (int i = 0; i < len; i++) {
ans[i] = str.charAt((i + n) % len);
}
return String.valueOf(ans);
}
}
44. 反转单词序列
简单模拟
题目描述
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
题意分析
题意是反转单词的顺序。“ ”时返回“ ”。
Java Code
public class Solution {
public String ReverseSentence(String str) {
if (str.trim().equals(""))
return str;//句子由空白字符组成时,原句返回
String[] strs = str.split("\s");
StringBuilder ans = new StringBuilder();
for (int i = strs.length - 1; i >= 0; i--) {
ans.append(strs[i]);
if (i != 0) ans.append(" ");
}
return ans.toString();
}
}
49. 把字符串转换为整数++
模拟,细节,原码,补码
题目描述
将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。不考虑指数表达形式。只有 int 范围内的数值合法
输入描述:
输入一个字符串,包括数字字母符号,可以为空。
输出描述:
如果是合法的数值表达则返回该数字,否则返回0
示例1
输入 +2147483647 1a33 输出 2147483647 0
题意分析
要考虑要如下问题:
- 输入可能为空
- 符号位可能存在
- 溢出的处理
尤其要考虑以下特殊情况,由于计算机负数采用补码形式,所以 n 位的有符号数的范围是 $ [-2n-1,2n-1] $,因此,当n = 8时,表示范围就是[-128, 127],但是如果我们只是记录符号位和数值的绝对值的话,-128的绝对值128会溢出,因此,我们记录数值而非绝对值,并且在程序中加入上下溢出预检查。
Java Code
public class Solution {
public int StrToInt(String str) {
if (str == null || str.length() == 0) return 0;
int neg = 1;// -1 表示为负数
char first = str.charAt(0);
int ans = 0, idx = 0;
if (IsAlphaNumber(first)) ans = first - '0';
else if (first != '-' && first != '+') {
return 0;
}// 字符非法
else {
if (str.length() == 1) return 0;//只有符号位即非法
if (first == '-') neg = -1;
first = str.charAt(++idx);
if (!IsAlphaNumber(first)) return 0;//符号位后不是数字即非法
ans = (first - '0') * neg;// ans中存了第一位数*neg
}
idx++;
// 至此,idx指向了第1个数字之后的字符,ans中存了第一位数*neg
if (str.length() - idx > 9) return 0;
while (idx < str.length()) {
char c = str.charAt(idx);
if (IsAlphaNumber(c)) {
int y = c - '0';
// 如果会产生溢出则非法
if (neg == 1 && ans * 10 > Integer.MAX_VALUE - y)
return 0;
else if (neg == -1 && ans * 10 < Integer.MIN_VALUE + y)
return 0;
ans = ans * 10 + neg * y;
} else return 0;
idx++;
}
return ans;
}
private boolean IsAlphaNumber(char c) {
return c >= '0' && c <= '9';
}
}
52. 正则表达式匹配 ++++
正则表达式,递归,动态规划
题目描述
请实现一个函数用来匹配包括'.'和''的正则表达式。模式中的字符'.'表示任意一个字符,而''表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配
题意分析
题目中要考虑到比较多的状态转移情况,最复杂的是可能当遇到 abbba
和 ab*bba
匹配这种情况时,由于 b*
可以匹配 0 个或1 个甚至多个b
,要对三种匹配情况分别进行讨论,看三种匹配情况下,后面的字符串是否能继续成功匹配。由此我们可以看出,这里需要将整个字符串是否能匹配的大问题分解成当前匹配情况影响下后面的字符串是否能继续匹配的子问题,需要递归求解。
另一种思路是动态规划。由上面递归解法的分析可知,当前位置字符串匹配问题的子问题是字符串后面部分能否匹配,状态转移的影响因素是当前b*
可以匹配 0 个、1 个还是多个b
。因此我们可以设置状态 dp[i][j]
为str[i:length-1]
与pattern[j:len-1]
匹配,从后往前进行状态更新。
递归代码
public class Solution {
public boolean match(char[] str, char[] pattern)
{
if(str==null || pattern==null)return false;
return matchHelper(str,pattern,0,0);
}
public boolean matchHelper(char[] str,char[] ptn,int p1,int p2){
if(p2>=ptn.length && p1>=str.length)return true;// ptn无 str无
if(p2>=ptn.length && p1< str.length)return false;// ptn无 str有
if(p2< ptn.length && p1>=str.length){// ptn有 str无,判断ptn是否为(a*)这种组合
int i=p2+1;
while(i < ptn.length && ptn[i]=='*')i+=2;
if(i==ptn.length) return false;
else return true;
}
// str 和 ptn 长度都大于 0
if(p2+1< ptn.length && ptn[p2+1]=='*'){//当前是 a*
if(ptn[p2] != '.' && str[p1]!= ptn[p2])// a* 不能匹配
return matchHelper(str,ptn,p1,p2+2);
else return matchHelper(str,ptn,p1,p2+2)// a*不匹配
|| matchHelper(str,ptn,p1+1,p2+2)// a*匹配一次
|| matchHelper(str,ptn,p1+1,p2);// a*匹配多次
}else{// 后面没有 *
if(ptn[p2]=='.' || str[p1]==ptn[p2])
return matchHelper(str,ptn,p1+1,p2+1);
else return false;
}
}
}
动规代码 错误代码如下,有空再调。。。
public class Solution {
public boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null) return false;
char[] pt = pattern;
boolean[][] dp = new boolean[str.length + 1][pt.length + 1];
dp[str.length][pt.length] = true;// 统一操作
for (int i = str.length; i >= 0; i--) {// 子串从空开始
for (int j = pt.length - 1; j >= 0; j--) {// 模式串从非空开始
if (pt[j] != '*') {
dp[i][j] = i != str.length && dp[i + 1][j + 1]
&& (pt[j] == '.' || pt[j] == str[i]);
} else {
j--;
if (i == str.length)
dp[i][j] = j == pt.length - 1 || dp[i][j + 2];
else if (pt[j] == '.' || pt[j] == str[i]) {// 匹配0个或多个
int ii = i;
while (ii < str.length && (pt[j] == '.' || pt[j] == str[ii])) {
if (dp[ii + 1][j + 2]) {
dp[ii][j] = true;
break;
}
ii++;
}
} else {// 不匹配
dp[i][j] = dp[i][j + 2];// pt[j-1]不匹配
}
}
}
}
return dp[0][0];
}
}
53. 表示数值的字符串 +++
正则表达式 ,模拟自动机
题目描述
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100", "5e2", "-123", "3.1416"和"-1E-16"都表示数值。但是"12e", "1a3.14", "1.2.3", "+-5"和"12e+4.3"都不是。
题意分析
本题重点在于考虑到所有的情况,能够写出表示数值的字符串模式,然后就可以根据正则表达式或者字符串匹配的相应方法来做。
表示数值的字符串遵循共同的模式:A[.[B]][e|EC]
或者.B[e|EC]
。
以上模式的含义是:A为数值的整数部分,B为跟在小数点之后的小数部分,C为跟在e或者E之后的指数部分。其中,A部分可以没有,比如小数.123代表0.123。如果一个数没有整数部分,那么小数部分必须有。
具体说来,A和C(也就是整数部分和指数部分)都是可能以"+"、"-"开头或者没有符号的数字串,B是数字序列,但前面不能有符号。
我们可以通过顺序扫描字符串来判断是否符合上述模式,首先尽可能多的扫描数字序列(开头可能有正负号),如果遇到小数点,那么扫描小数部分,遇到e或者E,则开始扫描指数部分。
除了顺序扫描以外,判断一个字符串是否满足某个模式,我们很容易想到的一个办法是使用正则表达式,以下给出这两种方法代码实现。
正则表达式的解法很简洁,关于正则表达式的语法参考另外一篇博文:正则表达式
以上分析出自gzshan。补充,第二个模式也可以以正负符号开头。
public class Solution {
public boolean isNumeric(char[] str) {
String string = String.valueOf(str);
return string.matches("[\+-]?[0-9]+(\.[0-9]*)?([eE][\+-]?[0-9]+)?")||
string.matches("[\+-]?(\.[0-9]*)(([eE][\+-]?[0-9]+)?)");
}
/*
为什么是两个反斜杠
首先字符串中的\被编译器解释为 ,然后作为正则表达式,.又被正则表达式引擎解释为. 如果在字符串里只写.的话,第一步就被直接解释为.,之后作为正则表达式被解释时就变成匹配任意字符了
*/
}
54. 字符流中第一个不重复的字符+
char字符范围,借助标记数组
题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。
如果当前字符流没有存在出现一次的字符,返回#字符。
题意分析
第一反应是hash数组统计次数,然后再遍历一边字符流,但显然题目的意思是,只过一遍字符流就要能够得到答案。
先统计每个字符第一次出现的位置,如果重复出现了则标记为-1,最后再遍历一边为字符统计的数组,找到答案;
- 字符流没有长度限制,而字符有限,面向字符统计避免了过大的空间要求;
- 虽然Java的char为16 bit,但是字符的范围是 0~255,猜想是因为只包括了ASCII中定义的字符;
Java Code
public class Solution {
int [] loc=new int[256];
int id=1;
//Insert one char from stringstream
public void Insert(char ch)
{
if(loc[ch]==0)loc[ch]=id;
else loc[ch]=-1;
id++;
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
char ans='#';
int minid=id;
for(int i=0;i < 256;i++){
if(loc[i] > 0 && loc[i] < minid){
ans=(char)i;
minid=loc[i];
}
}
return ans;
}
}