记录 LeetCode 刷题时遇到的字符串相关题目,第二篇
动态规划:二维数组dp,dp[i][j] (i >0 && j > 0) 表示对于word1中从第1到第i个字符段,要将其转换到和word2中从第1到第j个字符相同,所需的最少步数
0表示空串,即dp[0][0]表示word1空串部分要转换到和word2空串部分相同所需的最少步数,显然为0
dp[0][j]表示word1空串部分要转换到和word2中从第1到第j个字符这段区间的字符相同所需的步数,显然只能在空串的基础上做添加操作,所以dp[0][j] = j
dp[i][0]表示word1中从第1到第i个字符这段字符要转换到和word2中空串部分相同所需的步数,显然也一样只能做添加操作,所以dp[i][0] = i
以上就是边界情况。接下来考虑dp[i][j],如果word1中第i个字符(word1.charAt(i - 1))跟word2中第j个字符(word2.charAt(j - 1))恰好相同,那么就相当于在dp[i - 1][j - 1]的基础上两者分别加上了相同的第i跟第j个字符,所以要转换到的步数与dp[i - 1][j - 1]相同;如果word1第i个字符跟word2第j个字符不相同,那么可以在dp[i - 1][j - 1]的基础上将word1的第i个字符替换为word2的第j个字符,即dp[i - 1][j - 1] + 1;也可以先让word1第1到第i - 1字符段跟word2的第1到第j字符段相同,然后再删去 word1第i个字符,即dp[i - 1][j] + 1;还可以让word1第1到第j字符段跟word2第1到j - 1字符段相同,然后再在word1后面增加 word2的第j个字符,即dp[i][j - 1] + 1。至于选择哪个方案就看哪个方案的总步数更少
例如示例1的word1 = “horse”, word2 = “ros”,要求dp[5][3],由于word1的第5个字符跟word2的第3个字符不相等,所以可以:先让word1中"hors"变为“ro”,然后把"e"变为"s",这就是替换,dp[4][2] + 1;也可以先让“hors”转换到"ros",然后删除掉最后的"e",dp[4][3] + 1;还可以先让“horse”转换到"ro",然后在最后加多一个"s",dp[5][2] + 1
public int minDistance(String word1, String word2) {
int len1 = word1.length();
//对比之后这道题中,发现将字符串转化为字符数组然后在后面状态转移中使用c2[i - 1] == c1[j - 1]来判断
//要比不转化为字符数组直接word1.charAt(i - 1) == word2.charAt(j - 1) 快了那么1ms
char[] c1 = word1.toCharArray();
int len2 = word2.length();
char[] c2 = word2.toCharArray();
int[][] dp = new int[len2 + 1][len1 + 1];
dp[0][0] = 0;
for(int i = 1;i <= len1;i++){
dp[0][i] = i;
}
for(int i = 1;i <= len2;i++){
dp[i][0] = i;
}
for(int i = 1;i <= len2;i++){
for(int j = 1;j <= len1;j++){
if(c2[i - 1] == c1[j - 1]){
dp[i][j] = dp[i - 1][j - 1];
}else{
dp[i][j] = Math.min(dp[i - 1][j - 1],Math.min(dp[i - 1][j],dp[i][j - 1])) + 1;
}
}
}
return dp[len2][len1];
}
使用 String 的 split 方法以 ‘.’ 将字符串划分为不同部分,每一部分转化为数字再根据题意进行比较即可
public int compareVersion(String version1, String version2) {
String[] s1 = version1.split("\\.");
String[] s2 = version2.split("\\.");
int i = 0;
while(i < s1.length || i < s2.length){
int num1 = i < s1.length ? Integer.valueOf(s1[i]) : 0;
int num2 = i < s2.length ? Integer.valueOf(s2[i]) : 0;
if(num1 > num2) return 1;
if(num1 < num2) return -1;
i++;
}
return 0;
}
首先:long的范围为-9223372036854775808~9223372036854775807
根据题意以及案例模拟就可以了。要注意的点,题目说到计算结果可能会超出int的范围,所以在保存运算的中间结果时先用long类型的res变量来保存,对字符串处理完毕后对res判断是否超出int范围即可;还有一个点就是计算过程还可能超出long的范围,比如输入"9223372036854775808"这个样例,所以对每个原酸得到的中间变量都可以判断是否超出int范围,超出就不用继续往下算了,直接可以判断后返回结果,也就不用担心后面在超出long的范围了
public int myAtoi(String s) {
int length = s.length();
if(length == 0) return 0;
int index = 0;
//去除前导空格,注意不能用" ".equals(s.charAt(index))来判断
while(index < length && ' ' == (s.charAt(index))){
index++;
}
//去除空格后字符串就结尾了,返回0
if(index == length) return 0;
//检查下一个字符符号,是负号则结果应为负号;其他符号的话结果都为正号
//flag置为0表示负号,1表示正号
int flag;
if(s.charAt(index) == '-') flag = 0;
else flag = 1;
//开始读入字符
long res = 0L;
int temp = index;
//如果上一步检查到的符号是正号或负号,那就要从下一个字符开始读取数字;如果是数字或其他字符就直接从这一个字符开始读
if(s.charAt(temp) == '-' || s.charAt(temp) == '+') temp++;
while(temp < length){
char c = s.charAt(temp);
if(c >= '0' && c <= '9'){
res *= 10;
res += Integer.parseInt(String.valueOf(c));
//已经溢出int的范围不用继续计算
if(res > Integer.MAX_VALUE || res < Integer.MIN_VALUE) break;
temp ++;
}else{
//遇到非数字字符直接结束
break;
}
}
res = flag == 0 ? -res : res;
if(res > Integer.MAX_VALUE) return Integer.MAX_VALUE;
else if(res < Integer.MIN_VALUE) return Integer.MIN_VALUE;
return (int)res;
}
根据乘法运算的法则,将 num1 与 num2 的每一位都做一次乘法,会得到 num2.length() 个结果,将这些结果相加得到的就是答案。相加可以使用 415. 字符串相加 的做法
注意乘法运算的每一个结果都不能使用 int 或 long 直接保存,因为有些样例运算的结果甚至超过了 long 的范围。所以只有每一位的运算可以使用 int 进行,其余的运算都只能使用字符串
class Solution {
public String multiply(String num1, String num2) {
int l1 = num1.length();
int l2 = num2.length();
//判断是否有零,有则乘法运算结果为0
boolean num1IsZero = l1 == 1 && num1.charAt(0) == '0';
boolean num2IsZero = l2 == 1 && num2.charAt(0) == '0';
if(num1IsZero || num2IsZero) return "0";
char[] c1 = num1.toCharArray();
char[] c2 = num2.toCharArray();
int more = 0,cur = 0; //more表示每一位运算得到的进位,cur表示每一位运算的结果
//num1与num2的十位,百位,...等高位运算时得到的结果后面需要补0,表示10倍,100倍,...
StringBuilder tmpTimes = new StringBuilder();
StringBuilder tmp = null; //表示num1与num2每一位乘法运算时得到的结果
String res = ""; //保存num1与num2每一位乘法运算后的结果相加得到的累积结果
//外层循环为num2
for(int i = l2 - 1;i >= 0;i--){
more = 0;
tmp = new StringBuilder();
//内层循环为num1
for(int j = l1 - 1;j >= 0;j--){
cur = (c1[j] - '0') * (c2[i] - '0') + more; //乘法运算
more = cur / 10; //得到进位
cur = cur % 10; //减去进位得到该位的结果
//新一位的运算结果应放在tmp的最高位,所以需要反转一下
tmp = tmp.reverse().append(cur).reverse();
}
//最高位可能有进位不要忘了
if(more > 0) tmp = tmp.reverse().append(more).reverse();
//tmp补0后与原先的res相加
res = addStrings(tmp.append(tmpTimes).toString(),res);
tmpTimes.append("0"); //随着num2参与运算的数的位数增加,所需补的0也需增加
}
return (res);
}
public String addStrings(String num1, String num2) {
char[] c1 = num1.toCharArray();
int index1 = c1.length - 1; //转换为字符数组的话下标从大到小遍历
char[] c2 = num2.toCharArray();
int index2 = c2.length - 1;
int more = 0,cur = 0;
StringBuilder res = new StringBuilder("");
while(index1 >= 0 || index2 >= 0){
if(index1 < 0) cur = c2[index2--] - '0' + more;
else if(index2 < 0) cur = c1[index1--] - '0' + more;
else cur = c1[index1--] - '0' + c2[index2--] - '0' + more;
more = 0;
if(cur > 9){
more = 1;
cur -= 10;
}
res.append((char)(cur + '0'));
}
if(more > 0) res.append(more);
return res.reverse().toString();
}
}
这种嵌套结构很适合使用栈或者递归来做。这里采用递归来做:
定义一个递归方法 getStr(String s,int index),确保下标 index 所指向的是数字的前提下,返回从下标 index 开始最外层的 ‘[’ 和 ‘]’ 之间的字符串解码后的字符串,就算其中有嵌套其它的 ‘[’,‘]’ 也可以。所以入口函数 decodeString 只需调用 getStr 即可
从 index 开始遍历 s,遇到 ‘[’,就对这个 ‘[’ 后面的内容递归调用 getStr 方法获取这个 ‘[’ 对应的解码后字符串 str。根据题意,‘[’ 之前一定是个数字,所以在遇到这个 ‘[’ 之前我们应该事先能知道这个 ‘[’ 之前的数字 num,那么这个解码后字符串 str 就需要重复 num 次。在 getStr 返回后,我们需要接着这次 getStr 已经遍历到的位置往后遍历,因此 getStr 还需要返回方法返回时遍历到的位置下标。由于 getStr 是查找 ‘[’ 和 ‘]’ 之间的字符串,所以 getStr 最后遍历到的元素一定是 ‘]’
遇到数字字符,就找到后续连续的数字字符计算整个连续的字符序列所代表的数字
遇到 ‘]',说明本次 getStr 的任务已经完成,返回得到的解码后字符串和这个 ‘]’ 的下标
最后就是遇到小写英文字母,直接 append 到 res 即可,res 即我们维护的本次 getStr 所找到的解码后字符串
需要注意的是,字符串拼接应使用 StringBuilder 来 append
class Solution {
public String decodeString(String s) {
return getStr(s,0)[0];
}
public String[] getStr(String s,int index){
if(index == s.length()) return null;
StringBuilder res = new StringBuilder();
char[] cs = s.toCharArray();
int num = 0;
while(index < s.length()){
if(cs[index] == '['){
String[] strs = getStr(s,index + 1);
while(num-- > 0){
res.append(strs[0]);
}
//getStr返回的是']'的下标,继续遍历需要从下一个字符开始
index = Integer.parseInt(strs[1]) + 1;
}else if(cs[index] >= '0' && cs[index] <= '9'){
num = 0;
while(cs[index] >= '0' && cs[index] <= '9'){
num = num * 10 + cs[index++] - '0'; //数字字符转换为对应的数字直接减去'0'即可
}
}else if(cs[index] == ']'){
return new String[]{res.toString(),String.valueOf(index)};
}else{
res.append(cs[index++]);
}
}
return new String[]{res.toString(),String.valueOf(index)};
}
}
一开始没看到题意的 “同时不改变字符的顺序”,以为只要字母的个数对得上就可以
使用两个哈希表 mapS,mapT,使用数组的形式,大小都为 256,因为题目说到 “s 和 t 由任意有效的 ASCII 字符组成”,所以数组每个槽上表示一个字符对应于另一个字符串中所要映射的字符,如 mapS[‘a’] = ‘b’,表示 s 中映射到 t 中的字符为 b
遍历两个字符串,同时建立两个字符串间字符的映射关系,如果遍历到一个字符在另一个字符串中对应位置的字符与已经建立的映射关系不符,则说明两个字符串不是同构字符串
public boolean isIsomorphic(String s, String t) {
char[] mapS = new char[256];
char[] mapT = new char[256];
char[] charsS = s.toCharArray();
char[] charsT = t.toCharArray();
for(int i = 0;i < charsS.length;i++){
//判断映射关系是否被破坏,是则直接返回false
if(mapS[charsS[i]] != 0 && mapS[charsS[i]] != charsT[i] ||
mapT[charsT[i]] != 0 && mapT[charsT[i]] != charsS[i]){
return false;
}
mapS[charsS[i]] = charsT[i];
mapT[charsT[i]] = charsS[i];
}
return true;
}
初始化一个字符串 prefix 为 strs[0],然后遍历 strs 中剩下的每个字符串跟 prefix 得到新的最长公共前缀,对比完所有字符串后得到的 prefix 就是最后的最长公共前缀
在枚举对比的过程中,如果得到最长公共前缀已经为空串了,后面就不用继续对比了,最后的最长公共前缀一定就是空串了
class Solution {
public String longestCommonPrefix(String[] strs) {
//strs一定包含1个或以上个元素,不用判断是否为null或是否不含元素
String prefix = strs[0];
int count = strs.length;
for (int i = 1; i < count; i++) {
prefix = longestCommonPrefix(prefix, strs[i]);
//如果已经找到最长公共前缀为空串就不用继续找后面的字符串了
if (prefix.length() == 0) {
break;
}
}
return prefix;
}
public String longestCommonPrefix(String str1, String str2) {
int length = Math.min(str1.length(), str2.length());
int index = 0;
while (index < length && str1.charAt(index) == str2.charAt(index)) {
index++;
}
return str1.substring(0, index);
}
}