算法通关村第十二关——字符串:隐形的王者(青铜)
前言
关于字符串的算法题一般可以分为以下几种类型:
- 字符串遍历与操作:包括对字符串的遍历、字符替换、字符删除、字符插入等基本操作。
- 字符串匹配与查找:包括子串匹配、正则表达式匹配、模式匹配等相关算法。
- 字符串排序与比较:包括字符串的字典排序、按特定规则排序、字符串比较等相关算法。
- 字符串翻转与反转:包括字符串的翻转、单词反转等相关算法。
- 字符串拆分与连接:包括字符串的拆分、连接、格式化等相关算法。
- 字符串判定与验证:包括字符串的长度判断、是否包含特定字符、是否为回文串等相关算法。
- 字符串压缩与解压:包括字符串的压缩、解压缩等相关算法。
一般字符串相关的算法题都会使用一些基本的api进行辅助,下面是与之相关常用的方法:
- String.length():返回字符串的长度。
- String.charAt(int index):返回指定索引位置上的字符。
- String.toCharArray():将字符串转换为字符数组。
- String.valueOf(char[] data):将字符数组转换为字符串。
- StringBuilder.append(String str):向StringBuilder对象末尾添加字符串。
- StringBuilder.insert(int offset, String str):在指定位置插入字符串。
- StringBuilder.delete(int start, int end):删除指定区间的字符。
- StringBuilder.reverse():反转字符串。
- StringBuilder.toString():将StringBuilder对象转换为String对象。
- Arrays.sort(char[] a):对字符数组进行排序。
以下是对应每种类型的字符串算法题目示例:
- 字符串遍历与操作:
- 反转字符串:https://leetcode-cn.com/problems/reverse-string/
- 替换空格:https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/
- 翻转单词顺序:https://leetcode-cn.com/problems/reverse-words-in-a-string/
- 字符串匹配与查找:
- 实现 strStr() 函数:https://leetcode-cn.com/problems/implement-strstr/
- 正则表达式匹配:https://leetcode-cn.com/problems/regular-expression-matching/
- 最长公共前缀:https://leetcode-cn.com/problems/longest-common-prefix/
- 字符串排序与比较:
- 字符串的字典排序:https://leetcode-cn.com/problems/largest-number/
- 按特定规则排序:https://leetcode-cn.com/problems/reorder-data-in-log-files/
- 字符串比较:https://leetcode-cn.com/problems/compare-version-numbers/
- 字符串翻转与反转:
- 反转字符串 II:https://leetcode-cn.com/problems/reverse-string-ii/
- 反转字符串中的单词 III:https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/
- 字符串拆分与连接:
- 分割回文串:https://leetcode-cn.com/problems/palindrome-partitioning/
- 数组拆分 I:https://leetcode-cn.com/problems/array-partition-i/
- 重新格式化字符串:https://leetcode-cn.com/problems/reformat-the-string/
- 字符串判定与验证:
- 判断字符串是否是回文串:https://leetcode-cn.com/problems/valid-palindrome/
- 验证回文字符串 II:https://leetcode-cn.com/problems/valid-palindrome-ii/
- 字符串压缩与解压:
- 字符串的压缩:https://leetcode-cn.com/problems/string-compression/
- 解码方法:https://leetcode-cn.com/problems/decode-ways/
这些题目只是其中的一部分,通过解决这些题目可以熟悉不同类型的字符串算法,提高对字符串操作的理解和应用能力。
下面就用几道题目进行入门~~
1. 转换成小写字母
我们知道每个字母都是有确定的 ASCII的,因此我们可以根据 码表操作字符串即可。
常见ASCII范围是:a-z:97-122 A-Z:65-90 0-9:48-57
这个题可以先遍历整个字符串,然后对每一位字符进行判断,如果str[i]的值在A-Z之间,则需要在原来的基础上ASCII码加上32即可转换成对应小写:
class Solution {
public String toLowerCase(String s) {
char[] chars = s.toCharArray();
for(int i=0; i<s.length(); i++){
if(chars[i] >= 65 && chars[i] <= 90){
chars[i] += 32;
}
}
return new String(chars);
}
}
2. 字符串转换整数
这道题需要首先根据题意和例子罗列一些要点:要点:
-
根据示例 1,需要去掉前导空格;
-
根据示例 2,需要判断第 1 个字符为 + 和 - 的情况,因此,可以设计一个变量 sign,初始化的时候为 1,如果遇到 - ,将 sign 修正为 -1;
-
判断是否是数字,可以使用字符的 ASCII 码数值进行比较,即 ‘0’ <= c <= ‘9’,如果0在最前面,则应该将其去掉;
-
根据示例 3 和示例 4 ,在遇到第 1 个不是数字的字符的情况下,转换停止,退出循环;
-
根据示例 5,如果转换以后的数字超过了 int 类型的范围,需要截取。这里不能将结果 res 变量设计为 long 类型,注意:由于输入的字符串转换以后也有可能超过 long 类型,因此需要在循环内部就判断是否越界,只要越界就退出循环,这样也可以减少不必要的计算;
-
由于涉及下标访问,因此全程需要考虑数组下标是否越界的情况。
特别注意:
1、由于题目中说「环境只能保存 32 位整数」,因此这里在每一轮循环之前先要检查,具
体细节请见编码。
2、Java 、Python 和 C++ 字符串的设计都是不可变的,即使用 trim() 会产生新的变
量,因此我们尽量不使用库函数,使用一个变量 index 去做遍历,这样遍历完成以后就得
到转换以后的数值。
class Solution {
public int myAtoi(String str) {
int len = str.length();
char[] charArray = str.toCharArray();
// 1、去除前导空格
int index = 0;
while (index < len && charArray[index] == ' ') {
index++;
}
// 2、如果已经遍历完成(针对极端用例 " ")
if (index == len) {
return 0;
}
// 3、如果出现符号字符,仅第 1 个有效,并记录正负
int sign = 1;
char firstChar = charArray[index];
if (firstChar == '+') {
index++;
} else if (firstChar == '-') {
index++;
sign = -1;
}
// 4、将后续出现的数字字符进行转换
// 不能使用 long 类型,这是题目说的
int res = 0;
while (index < len) {
char currChar = charArray[index];
// 4.1 先判断不合法的情况
if (currChar > '9' || currChar < '0') {
break;
}
// 题目中说只能存储 32 位大小的有符号整数,下面两个if分别处理整数和负数越界的情况
// 提前判断乘以10以后是否越界,但res*10可能会越界,
// 所以这里使用Integer.MAX_VALUE / 10和Integer.MIN_VALUE / 10作为阈值进行判断
if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && (currChar - '0') > Integer.MAX_VALUE % 10)) {
return Integer.MAX_VALUE;
}
if (res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && (currChar - '0') > -(Integer.MIN_VALUE % 10))) {
return Integer.MIN_VALUE;
}
// 合法的情况下,才考虑转换,每一步都把符号位乘进去
res = res * 10 + sign * (currChar - '0');
index++;
}
return res;
}
}
不过还可以更优化一些,这段代码在判断溢出部分比较麻烦,可以进行修改如下:
class Solution {
public int myAtoi(String s) {
int sign = 1; // 符号位,默认为正数
int ans = 0; // 结果变量
int index = 0; // 字符串遍历的索引
char[] array = s.toCharArray(); // 将输入字符串转换为字符数组
// 去除前导空格
while (index < array.length && array[index] == ' ') {
index++;
}
// 判断是否出现符号字符,并记录正负号
if (index < array.length && (array[index] == '-' || array[index] == '+')) {
sign = array[index++] == '-' ? -1 : 1;
}
// 处理数字字符,直到遇到非数字字符或者遍历结束
while (index < array.length && array[index] <= '9' && array[index] >= '0') {
int digit = array[index++] - '0'; // 当前数字字符对应的数值
// 提前判断乘以10以后是否会越界
// 如果越界,则根据正负号返回Integer.MAX_VALUE或Integer.MIN_VALUE
if (ans > (Integer.MAX_VALUE - digit) / 10) {
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
// 更新结果变量,将当前数字字符转换为数值并累加到结果中
ans = ans * 10 + digit;
}
return ans * sign; // 返回带上符号位的结果
}
}
改完的这段代码巨快