目录
干货分享,感谢您的阅读!
一、基本知识认识
当谈论字符串时,通常指的是由字符组成的文本序列。在计算机科学和编程中,字符串是一种常见的数据类型,用于存储和处理文本数据。
- 字符串的表示方式:字符串可以用单引号或双引号括起来表示,例如 'Hello' 或者 "World"。在大多数编程语言中,这两种表示方式没有本质区别,只是一种约定。
- 字符串的不可变性:在许多编程语言中,字符串是不可变的,这意味着一旦创建了一个字符串,就不能再更改它。每次对字符串进行修改时,实际上是创建了一个新的字符串,而不是修改原始字符串。
- 字符串的索引和访问:字符串中的每个字符都有一个对应的索引,通常从0开始。例如,字符串 "Hello" 中,字符 'H' 的索引是0,字符 'e' 的索引是1,依此类推。可以使用索引来访问字符串中的单个字符,例如 str[2] 表示访问字符串 str 中的第三个字符。
- 字符串的长度:字符串的长度是指字符串中字符的个数。在许多编程语言中,可以使用内置的函数或方法来获取字符串的长度,例如 str.length()。
- 字符串的连接:将两个字符串连接起来,可以使用字符串连接操作符或字符串连接方法。例如,str1 + str2 或者 str1.concat(str2) 可以将字符串 str1 和 str2 连接起来。
- 字符串的比较:判断两个字符串是否相等,可以使用相等运算符(== 或 equals 方法)。在一些编程语言中,字符串的比较可能需要使用 equals 方法,因为相等运算符可能比较的是引用而不是字符串的内容。
- 字符串的常见操作:字符串支持许多常见的操作,如查找子串、截取子串、替换字符、大小写转换等。
字符串在编程中是非常重要的数据类型,用于处理文本和字符串处理任务。掌握字符串的基本知识对于开发和编程非常有用,可以让我们更轻松地处理文本数据。
二、框架中的应用
在计算机科学和软件开发中,字符串是一种非常常见的数据类型,因此在各种框架和编程库中都有广泛的应用。
以下是框架中字符串的一些常见应用分析:
- 用户界面 (UI) 开发:在图形用户界面 (GUI) 开发中,字符串用于显示文本标签、按钮标签、错误消息、警告消息等。用户界面中的多语言支持也会用到字符串,通过将不同语言的字符串存储在不同的资源文件中,以实现国际化和本地化。
- 数据库操作:在数据库操作中,字符串常用于构建 SQL 查询语句,表示数据库表名、字段名、条件等。ORM(对象关系映射)框架也经常使用字符串来表示数据库字段和实体类属性之间的映射关系。
- 网络通信:在网络通信中,字符串是最常用的数据格式之一。例如,HTTP 请求和响应中的报文内容通常是字符串格式的。
- 配置文件:许多框架使用配置文件来存储应用程序的配置信息。配置文件中的键值对通常是字符串形式的,例如 key=value。
- 日志记录:在日志记录中,字符串用于记录日志消息、异常信息等。
- 序列化和反序列化:将对象转换为字符串表示,或将字符串表示转换为对象,是序列化和反序列化的过程。这在网络通信、持久化存储等场景中非常常见。
- 正则表达式:字符串的正则表达式匹配是许多框架中常用的功能,用于检查字符串是否符合某种模式。
- 加密和哈希:在安全领域,字符串常用于加密和哈希算法的输入和输出。
总体而言,字符串在框架中的应用非常广泛,涵盖了各个领域。它们是处理文本数据、配置信息、日志记录、网络通信等关键部分。因此,对于开发者来说,熟练掌握字符串的操作和处理技巧是非常重要的。
三、主要练习题
1、字符串反转
题目描述:编写一个算法来反转一个字符串。输入字符串以字符数组 char[] 的形式给出。
示例 1:输入: ["h","e","l","l","o"]. 输出: ["o","l","l","e","h"]
示例 2:输入: ["H","a","n","n","a","h"]. 输出: ["h","a","n","n","a","H"]
解题思路
最优解法是使用双指针来实现字符串反转。双指针方法可以在O(n)的时间复杂度内完成字符串反转,并且不需要使用额外的空间。
以下是具体的步骤分析:
- 使用两个指针 left 和 right,分别指向字符串的首尾字符。
- 不断交换 left 和 right 指针所指向的字符,然后将指针向中间移动。直到 left 指针超过或等于 right 指针,完成字符串反转。
- 字符串反转完成后,原来的字符串就会被修改为反转后的字符串。
由于这个方法只涉及一次遍历,因此时间复杂度是 O(n)。同时,它只使用了常数级的额外空间,即两个指针,所以空间复杂度是 O(1)。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 编写一个算法来反转一个字符串。输入字符串以字符数组 char[] 的形式给出。
* 示例 1:
* 输入: ["h","e","l","l","o"]
* 输出: ["o","l","l","e","h"]
* 示例 2:
* 输入: ["H","a","n","n","a","h"]
* 输出: ["h","a","n","n","a","H"]
* @date 2022/1/1 23:01
*/
public class ReverseString {
/**
* 最优解法是使用双指针来实现字符串反转。双指针方法可以在O(n)的时间复杂度内完成字符串反转,并且不需要使用额外的空间。
* 以下是具体的步骤分析:
* 使用两个指针 left 和 right,分别指向字符串的首尾字符。
* 不断交换 left 和 right 指针所指向的字符,然后将指针向中间移动。直到 left 指针超过或等于 right 指针,完成字符串反转。
* 字符串反转完成后,原来的字符串就会被修改为反转后的字符串。
* 由于这个方法只涉及一次遍历,因此时间复杂度是 O(n)。同时,它只使用了常数级的额外空间,即两个指针,所以空间复杂度是 O(1)。
*/
public void reverseString(char[] s) {
int left = 0;
int right = s.length - 1;
// 不断交换 left 和 right 指针所指向的字符
while (left < right) {
char temp = s[left];
s[left] = s[right];
s[right] = temp;
// 将指针向中间移动
left++;
right--;
}
}
public static void main(String[] args) {
char[] s1 = {'h', 'e', 'l', 'l', 'o'};
char[] s2 = {'H', 'a', 'n', 'n', 'a', 'h'};
// 反转字符数组 s1 和 s2
ReverseString reverse = new ReverseString();
reverse.reverseString(s1);
reverse.reverseString(s2);
// 打印反转后的结果
System.out.println("反转后的字符数组 s1:");
// 输出结果:o,l,l,e,h
printArray(s1);
System.out.println("\n反转后的字符数组 s2:");
// 输出结果:h,a,n,n,a,H
printArray(s2);
}
private static void printArray(char[] arr) {
for (char c : arr) {
System.out.print(c + " ");
}
}
}
2、字符串匹配
题目描述:要求实现字符串匹配算法,用于查找一个字符串在另一个字符串中的位置。
解题思路
常见的字符串匹配算法包括 KMP 算法和 Boyer-Moore 算法,它们都可以高效地找到一个字符串在另一个字符串中的位置。
KMP 算法是一种改进的字符串匹配算法,它利用已经匹配的部分信息来避免不必要的回溯,从而提高匹配效率。Boyer-Moore 算法是另一种高效的字符串匹配算法,它采用从右向左的模式匹配策略,根据模式串中的字符与匹配串中的字符的对应关系,跳过一些不必要的比较。
这两种算法在实际应用中都有很好的性能,它们可以在较短的时间内找到一个字符串在另一个字符串中的位置。
在字符串匹配问题中,KMP 算法和 Boyer-Moore 算法都是常见且高效的解法,它们分别采用不同的策略来加速匹配过程。
KMP 算法:
- KMP 算法通过预处理模式串(待查找的子串),构建部分匹配表(Partial Match Table,PMT),用来在匹配过程中快速跳过不必要的比较。这个表告诉算法在失配时应该将模式串向右移动多少位,而不是一位一位地移动。
KMP 算法的核心思想在于不回溯主串的指针,而是利用部分匹配表的信息来调整模式串的位置。这样,可以大幅减少比较的次数,提高匹配效率。
KMP 算法的时间复杂度是 O(m + n),其中 m 是主串的长度,n 是模式串的长度。KMP 算法的预处理部分时间复杂度为 O(n),匹配部分时间复杂度为 O(m)。
Boyer-Moore 算法:
- Boyer-Moore 算法通过预处理模式串,构建坏字符表(Bad Character Table)和好后缀表(Good Suffix Table),用来在匹配过程中跳过一些不必要的比较。
坏字符表用于在主串中找到失配字符在模式串中的对应位置,从而决定模式串的移动距离。好后缀表用于在模式串中找到最长的好后缀,再将模式串移动到主串中与好后缀对齐的位置。
Boyer-Moore 算法的核心思想在于从右向左比较,以及通过坏字符表和好后缀表来快速决定模式串的移动距离。
Boyer-Moore 算法的时间复杂度通常是 O(m + n),但在实际应用中,由于其预处理部分效率较高,它通常比 KMP 算法更快。
选择 KMP 算法还是 Boyer-Moore 算法取决于实际情况,两者都是非常优秀的字符串匹配算法,可以根据具体的应用场景和字符串长度来做出选择。
具体代码展示
提供 KMP 算法的代码实现
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 要求实现字符串匹配算法,用于查找一个字符串在另一个字符串中的位置。
* @date 2021/9/1 23:13
*/
public class KMPAlgorithm {
/**
* KMP 算法通过预处理模式串(待查找的子串),构建部分匹配表(Partial Match Table,PMT),用来在匹配过程中快速跳过不必要的比较。
* 这个表告诉算法在失配时应该将模式串向右移动多少位,而不是一位一位地移动。
* KMP 算法的核心思想在于不回溯主串的指针,而是利用部分匹配表的信息来调整模式串的位置。
* 这样,可以大幅减少比较的次数,提高匹配效率。
* KMP 算法的时间复杂度是 O(m + n),其中 m 是主串的长度,n 是模式串的长度
* 。KMP 算法的预处理部分时间复杂度为 O(n),匹配部分时间复杂度为 O(m)。
*/
public static int strStr(String haystack, String needle) {
if (needle.isEmpty()) {
return 0;
}
char[] txt = haystack.toCharArray();
char[] pat = needle.toCharArray();
int[] lps = computeLPSArray(pat);
// txt 指针
int i = 0;
// pat 指针
int j = 0;
while (i < txt.length) {
if (txt[i] == pat[j]) {
i++;
j++;
}
if (j == pat.length) {
return i - j;
} else if (i < txt.length && txt[i] != pat[j]) {
if (j != 0) {
j = lps[j - 1];
} else {
i++;
}
}
}
return -1;
}
/**
* 计算部分匹配表(lps 表)
*/
private static int[] computeLPSArray(char[] pat) {
int[] lps = new int[pat.length];
// 当前匹配的长度
int len = 0;
int i = 1;
while (i < pat.length) {
if (pat[i] == pat[len]) {
len++;
lps[i] = len;
i++;
} else {
if (len != 0) {
len = lps[len - 1];
} else {
lps[i] = 0;
i++;
}
}
}
return lps;
}
public static void main(String[] args) {
String haystack = "hello";
String needle = "ll";
int result = strStr(haystack, needle);
// 输出结果:Needle "ll" found at index: 2
System.out.println("Needle \"" + needle + "\" found at index: " + result);
}
}
3、正则表达式匹配
题目描述:实现字符串的正则表达式匹配,支持通配符和正则符号的处理,要求高效且正确。
正则表达式中通常包含特殊符号和通配符,例如 . 表示匹配任意单个字符,* 表示匹配前一个字符零次或多次,+ 表示匹配前一个字符一次或多次等。要求实现一个高效且正确的算法来实现字符串的正则表达式匹配。
解题思路
正则表达式匹配问题可以使用动态规划来解决,这是一个高效且正确的解法。动态规划的思想是将大问题划分为小问题,并将小问题的解保存起来,避免重复计算,从而提高计算效率。
假设原始字符串为 s,正则表达式为 p,我们可以定义一个二维数组 dp 来表示匹配状态,其中 dp[i][j] 表示原始字符串的前 i 个字符和正则表达式的前 j 个字符是否匹配。
根据题目要求,我们需要考虑以下几种情况来更新 dp 数组的值:
- 如果 s[i-1] 和 p[j-1] 相同,或者 p[j-1] 是通配符 .,则 dp[i][j] 的值取决于 dp[i-1][j-1] 的值。
- 如果 p[j-1] 是通配符 *,则有两种情况:
- a. * 表示前一个字符可以匹配零次,即 dp[i][j] 取决于 dp[i][j-2] 的值。
- b. * 表示前一个字符至少匹配一次,即 dp[i][j] 取决于 dp[i-1][j] 的值,前提是 s[i-1] 和 p[j-2] 匹配。
基于以上分析,我们可以得到动态规划的转移方程:
if s[i-1] == p[j-1] or p[j-1] == '.':
dp[i][j] = dp[i-1][j-1]
elif p[j-1] == '*':
dp[i][j] = dp[i][j-2] or (dp[i-1][j] and (s[i-1] == p[j-2] or p[j-2] == '.'))
边界条件为 dp[0][0] = True,表示空字符串和空正则表达式是匹配的。此外,空字符串和正则表达式的匹配状态可以根据特定规则计算出,作为初始化。
最终,当 dp[s.length()][p.length()] 为 True 时,表示原始字符串和正则表达式完全匹配。
这个动态规划解法的时间复杂度是 O(mn),其中 m 和 n 分别是原始字符串和正则表达式的长度。由于使用了二维数组来存储中间结果,空间复杂度也是 O(mn)。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 实现字符串的正则表达式匹配,支持通配符和正则符号的处理,要求高效且正确。
* 正则表达式中通常包含特殊符号和通配符,
* 例如 . 表示匹配任意单个字符,* 表示匹配前一个字符零次或多次,+ 表示匹配前一个字符一次或多次等
* 要求实现一个高效且正确的算法来实现字符串的正则表达式匹配。
* @date 2020/2/1 23:22
*/
public class RegularExpressionMatching {
/**
* 假设原始字符串为 s,正则表达式为 p,
* 我们可以定义一个二维数组 dp 来表示匹配状态,其中 dp[i][j] 表示原始字符串的前 i 个字符和正则表达式的前 j 个字符是否匹配。
* 根据题目要求,我们需要考虑以下几种情况来更新 dp 数组的值:
* 如果 s[i-1] 和 p[j-1] 相同,或者 p[j-1] 是通配符 .,则 dp[i][j] 的值取决于 dp[i-1][j-1] 的值。
* 如果 p[j-1] 是通配符 *,则有两种情况:
* a. * 表示前一个字符可以匹配零次,即 dp[i][j] 取决于 dp[i][j-2] 的值。
* b. * 表示前一个字符至少匹配一次,即 dp[i][j] 取决于 dp[i-1][j] 的值,前提是 s[i-1] 和 p[j-2] 匹配。
* 基于以上分析,我们可以得到动态规划的转移方程:
* if s[i-1] == p[j-1] or p[j-1] == '.':
* dp[i][j] = dp[i-1][j-1]
* elif p[j-1] == '*':
* dp[i][j] = dp[i][j-2] or (dp[i-1][j] and (s[i-1] == p[j-2] or p[j-2] == '.'))
* 边界条件为 dp[0][0] = True,表示空字符串和空正则表达式是匹配的
* 此外,空字符串和正则表达式的匹配状态可以根据特定规则计算出,作为初始化。
* 最终,当 dp[s.length()][p.length()] 为 True 时,表示原始字符串和正则表达式完全匹配。
* 这个动态规划解法的时间复杂度是 O(mn),其中 m 和 n 分别是原始字符串和正则表达式的长度。
* 由于使用了二维数组来存储中间结果,空间复杂度也是 O(mn)。
*/
public static boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
// 空字符串和空正则表达式是匹配的
dp[0][0] = true;
// 初始化第一行,空正则表达式与非空字符串的匹配状态
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '*') {
dp[0][j] = dp[0][j - 2];
}
}
// 动态规划,计算匹配状态
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char sc = s.charAt(i - 1);
char pc = p.charAt(j - 1);
if (sc == pc || pc == '.') {
dp[i][j] = dp[i - 1][j - 1];
} else if (pc == '*') {
dp[i][j] = dp[i][j - 2] || (dp[i - 1][j] && (sc == p.charAt(j - 2) || p.charAt(j - 2) == '.'));
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
String s1 = "aa";
String p1 = "a*";
boolean result1 = isMatch(s1, p1);
// 输出结果:true
System.out.println("Result for s1 and p1: " + result1);
String s2 = "mississippi";
String p2 = "mis*is*p*.";
boolean result2 = isMatch(s2, p2);
// 输出结果:false
System.out.println("Result for s2 and p2: " + result2);
}
}
4、最长公共子串
题目描述:找到两个字符串中最长的公共子串(Longest Common Substring)。公共子串是指在两个字符串中同时出现的连续字符序列。
示例:
输入: "ABABC", "BABCA"
输出: "BABC"
解题思路
这个问题要求我们找到两个字符串中最长的连续公共子串。为了得到最长的公共子串,我们可以使用动态规划来解决。
动态规划的思想是通过构建一个二维数组来存储中间结果,然后根据当前字符是否相同来更新数组的值,从而找到最长的公共子串。具体的动态规划算法如下:
- 创建一个二维数组 dp,其中 dp[i][j] 表示以第一个字符串的第 i 个字符和第二个字符串的第 j 个字符结尾的公共子串的长度。
- 初始化边界条件,即当第一个字符串的第 i 个字符和第二个字符串的第 j 个字符不相同时,dp[i][j] 的值为 0。
- 对于其余情况,如果第一个字符串的第 i 个字符和第二个字符串的第 j 个字符相同,那么 dp[i][j] 的值等于 dp[i-1][j-1] + 1,表示当前字符构成的公共子串的长度是前一个字符构成的公共子串长度加上当前字符。
- 在动态规划的过程中,记录最长的公共子串的长度和对应的结束位置,从而得到最长的公共子串。
- 最后,根据记录的最长公共子串的结束位置,从第一个字符串中提取出最长公共子串并返回。
这样,我们就可以找到两个字符串中最长的公共子串。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 找到两个字符串中最长的公共子串(Longest Common Substring)。公共子串是指在两个字符串中同时出现的连续字符序列。
* 示例:
* 输入: "ABABC", "BABCA"
* 输出: "BABC"
* @date 2021/2/2 23:28
*/
public class LongestCommonSubstring {
/**
* 这个问题要求我们找到两个字符串中最长的连续公共子串。为了得到最长的公共子串,我们可以使用动态规划来解决。
* 动态规划的思想是通过构建一个二维数组来存储中间结果,然后根据当前字符是否相同来更新数组的值,从而找到最长的公共子串。具体的动态规划算法如下:
* 1.创建一个二维数组 dp,其中 dp[i][j] 表示以第一个字符串的第 i 个字符和第二个字符串的第 j 个字符结尾的公共子串的长度。
* 2.初始化边界条件,即当第一个字符串的第 i 个字符和第二个字符串的第 j 个字符不相同时,dp[i][j] 的值为 0。
* 3.对于其余情况,如果第一个字符串的第 i 个字符和第二个字符串的第 j 个字符相同,
* 那么 dp[i][j] 的值等于 dp[i-1][j-1] + 1,表示当前字符构成的公共子串的长度是前一个字符构成的公共子串长度加上当前字符。
* 4.在动态规划的过程中,记录最长的公共子串的长度和对应的结束位置,从而得到最长的公共子串。
* 5.最后,根据记录的最长公共子串的结束位置,从第一个字符串中提取出最长公共子串并返回。
* 这样,我们就可以找到两个字符串中最长的公共子串。
*/
public static String longestCommonSubstring(String str1, String str2) {
int m = str1.length();
int n = str2.length();
int[][] dp = new int[m + 1][n + 1];
// 最长公共子串的长度
int maxLength = 0;
// 最长公共子串在 str1 中的结束索引
int endIndex = 0;
// 动态规划,计算公共子串的长度
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > maxLength) {
maxLength = dp[i][j];
// 更新最长公共子串的结束索引
endIndex = i;
}
}
}
}
// 根据最长公共子串的结束索引提取出最长公共子串
String longestCommon = str1.substring(endIndex - maxLength, endIndex);
return longestCommon;
}
public static void main(String[] args) {
String str1 = "ABABC";
String str2 = "BABCA";
String longestCommon = longestCommonSubstring(str1, str2);
// 输出结果:Longest Common Substring: BABC
System.out.println("Longest Common Substring: " + longestCommon);
}
}
5、最长回文子串
题目描述:找到一个字符串中最长的回文子串(Longest Palindromic Substring)。回文子串是指正着读和倒着读都一样的连续字符序列。
示例:输入: "babad" 输出: "bab" 或 "aba"
解题思路
这个问题要求我们找到一个字符串中的最长回文子串。为了得到最长回文子串,我们可以使用动态规划或中心扩展法来解决。
- 动态规划解法:
- 创建一个二维数组 dp,其中 dp[i][j] 表示从第 i 个字符到第 j 个字符是否构成回文子串。
- 初始化边界条件,即单个字符和相邻两个字符都是回文子串,即 dp[i][i] = true 和 dp[i][i+1] = true。
- 对于长度大于 2 的子串,dp[i][j] 的值取决于 dp[i+1][j-1] 和 s.charAt(i) == s.charAt(j)。
- 在动态规划的过程中,记录最长回文子串的起始位置和长度,从而得到最长的回文子串。
- 中心扩展法解法:
- 从字符串的每个字符和每两个字符之间开始,向两边扩展判断是否构成回文子串。
- 对于奇数长度的回文子串,从单个字符开始向两边扩展。
- 对于偶数长度的回文子串,从相邻两个字符之间开始向两边扩展。
- 在扩展的过程中,记录最长回文子串的起始位置和长度,从而得到最长的回文子串。
无论使用哪种解法,最终我们都能找到一个字符串中最长的回文子串。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 找到一个字符串中最长的回文子串(Longest Palindromic Substring)。回文子串是指正着读和倒着读都一样的连续字符序列。
* 示例:
* 输入: "babad"
* 输出: "bab" 或 "aba"
* @date 2021/1/2 23:53
*/
public class LongestPalindromicSubstring {
/**
* * 动态规划解法:
* 创建一个二维数组 dp,其中 dp[i][j] 表示从第 i 个字符到第 j 个字符是否构成回文子串。
* 初始化边界条件,即单个字符和相邻两个字符都是回文子串,即 dp[i][i] = true 和 dp[i][i+1] = true。
* 对于长度大于 2 的子串,dp[i][j] 的值取决于 dp[i+1][j-1] 和 s.charAt(i) == s.charAt(j)。
* 在动态规划的过程中,记录最长回文子串的起始位置和长度,从而得到最长的回文子串。
*/
public static String longestPalindrome(String s) {
int n = s.length();
// 二维数组用于存储回文子串的信息
boolean[][] dp = new boolean[n][n];
// 最长回文子串的长度
int maxLength = 1;
// 最长回文子串的起始位置
int start = 0;
// 单个字符是回文子串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 相邻两个字符相同的话是回文子串
for (int i = 0; i < n - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
dp[i][i + 1] = true;
maxLength = 2;
start = i;
}
}
// 动态规划,从长度为 3 的子串开始判断是否为回文子串
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
// 子串的结束位置
int j = i + len - 1;
// 判断子串是否为回文子串
if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) {
dp[i][j] = true;
// 更新最长回文子串的长度
maxLength = len;
// 更新最长回文子串的起始位置
start = i;
}
}
}
// 根据最长回文子串的起始位置和长度提取出最长回文子串
String longestPalindrome = s.substring(start, start + maxLength);
return longestPalindrome;
}
public static void main(String[] args) {
String s = "babad";
String longestPalindrome = longestPalindrome(s);
// 输出结果:Longest Palindromic Substring: bab
System.out.println("Longest Palindromic Substring: " + longestPalindrome);
}
}
6、判断回文字符串
题目描述:编写一个算法来判断一个给定的字符串是否是回文字符串(Palindrome),即正着读和倒着读都相同。
示例:
输入: "racecar"
输出: true
解题思路
对于判断一个字符串是否是回文字符串的问题,最优解法是使用双指针法。
双指针法的思想是,使用两个指针分别从字符串的开头和结尾开始,向中间移动,同时比较对应位置的字符是否相同。如果对应字符都相同,继续移动指针,直到两个指针相遇或交叉,说明整个字符串都是回文字符串。
这种解法的时间复杂度是 O(n/2) = O(n),其中 n 是字符串的长度。由于只需要进行一次遍历,因此时间复杂度是线性的,非常高效。
此外,这种解法不需要额外的空间,只需要常数级别的额外空间来存储两个指针,所以空间复杂度是 O(1),也是非常优秀的。
因此,使用双指针法是判断一个字符串是否是回文字符串的最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 编写一个算法来判断一个给定的字符串是否是回文字符串(Palindrome),即正着读和倒着读都相同。
* 示例:
* 输入: "racecar"
* 输出: true
* @date 2021/1/21 23:57
*/
public class PalindromeString {
/**
* 对于判断一个字符串是否是回文字符串的问题,最优解法是使用双指针法。
* 双指针法的思想是,使用两个指针分别从字符串的开头和结尾开始,向中间移动,同时比较对应位置的字符是否相同。
* 如果对应字符都相同,继续移动指针,直到两个指针相遇或交叉,说明整个字符串都是回文字符串。
* 这种解法的时间复杂度是 O(n/2) = O(n),其中 n 是字符串的长度。由于只需要进行一次遍历,因此时间复杂度是线性的,非常高效。
* 此外,这种解法不需要额外的空间,只需要常数级别的额外空间来存储两个指针,所以空间复杂度是 O(1),也是非常优秀的。
* 因此,使用双指针法是判断一个字符串是否是回文字符串的最优解法,具有高效和节省空间的特点。
*/
public static boolean isPalindrome(String s) {
// 将字符串转换为全小写,去除非字母和数字字符
s = s.toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
// 双指针法,一个从字符串开头,一个从结尾
int left = 0;
int right = s.length() - 1;
// 比较对应位置的字符是否相同,直到两个指针相遇或交叉
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
public static void main(String[] args) {
String str1 = "racecar";
String str2 = "hello";
String str3 = "A man, a plan, a canal, Panama!";
// 输出结果:Is "racecar" a palindrome? true
System.out.println("Is \"" + str1 + "\" a palindrome? " + isPalindrome(str1));
// 输出结果:Is "hello" a palindrome? false
System.out.println("Is \"" + str2 + "\" a palindrome? " + isPalindrome(str2));
// 输出结果:Is "A man, a plan, a canal, Panama!" a palindrome? true
System.out.println("Is \"" + str3 + "\" a palindrome? " + isPalindrome(str3));
}
}
7、最小分割次数
题目描述:将一个字符串分割成一些子串,使得每个子串都是回文字符串,求最小的分割次数(Minimum Cuts)。即找到最少分割次数,将原始字符串划分为若干个回文子串。
示例:
输入: "aab"
输出: 1
解释: 将字符串划分为 "aa" 和 "b" 两个回文子串,最少分割次数为 1。
解题思路
这个问题属于动态规划问题。要求最小分割次数,我们可以使用动态规划来解决。
动态规划的思路是:
- 创建一个一维数组 dp,其中 dp[i] 表示从字符串的第 i 个字符到末尾的最小分割次数。
- 初始化数组 dp,dp[i] 的初始值为从第 i 个字符到末尾的最大分割次数,即 dp[i] = len - i - 1,其中 len 是字符串的长度。
- 从右往左遍历字符串的每个字符,对于每个字符,从该字符开始向右遍历,判断以该字符为起点到右边任意字符位置的子串是否为回文字符串,如果是回文字符串,更新 dp[i] 为 dp[j+1] + 1,其中 j 是回文字符串右边界的索引。
- 最终,dp[0] 就是最小分割次数。
这样,我们就可以得到最小的分割次数,将原始字符串划分为若干个回文子串。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 将一个字符串分割成一些子串,使得每个子串都是回文字符串,求最小的分割次数(Minimum Cuts)。
* 即找到最少分割次数,将原始字符串划分为若干个回文子串。
* 示例:
* 输入: "aab"
* 输出: 1
* 解释: 将字符串划分为 "aa" 和 "b" 两个回文子串,最少分割次数为 1。
* @date 2021/1/24 14:01
*/
public class PalindromePartitioning {
/**
* 这个问题属于动态规划问题。要求最小分割次数,我们可以使用动态规划来解决。
* 动态规划的思路是:
* * 创建一个一维数组 dp,其中 dp[i] 表示从字符串的第 i 个字符到末尾的最小分割次数。
* * 初始化数组 dp,dp[i] 的初始值为从第 i 个字符到末尾的最大分割次数,即 dp[i] = len - i - 1,其中 len 是字符串的长度。
* * 从右往左遍历字符串的每个字符,对于每个字符,从该字符开始向右遍历,
* 判断以该字符为起点到右边任意字符位置的子串是否为回文字符串,
* 如果是回文字符串,更新 dp[i] 为 dp[j+1] + 1,其中 j 是回文字符串右边界的索引。
* * 最终,dp[0] 就是最小分割次数。
*/
public static int minCut(String s) {
int len = s.length();
int[] dp = new int[len];
// 初始化 dp 数组,dp[i] 表示从第 i 个字符到末尾的最大分割次数
for (int i = 0; i < len; i++) {
dp[i] = len - i - 1;
}
// 从右往左遍历字符串的每个字符
for (int i = len - 1; i >= 0; i--) {
// 从该字符开始向右遍历,判断以该字符为起点到右边任意字符位置的子串是否为回文字符串
for (int j = i; j < len; j++) {
if (isPalindrome(s, i, j)) {
// 如果是回文字符串,更新 dp[i] 为 dp[j+1] + 1,其中 j 是回文字符串右边界的索引
if (j == len - 1) {
// 如果整个子串是回文字符串,不需要分割
dp[i] = 0;
} else {
dp[i] = Math.min(dp[i], dp[j + 1] + 1);
}
}
}
}
// 最终,dp[0] 就是最小分割次数
return dp[0];
}
/**
* 判断字符串 s 中从 left 到 right 的子串是否是回文字符串
*/
private static boolean isPalindrome(String s, int left, int right) {
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
public static void main(String[] args) {
String s = "aab";
int minCut = minCut(s);
// 输出结果:Minimum Cuts: 1
System.out.println("Minimum Cuts: " + minCut);
}
}
8、回文排列
题目描述:判断一个字符串是否能够通过重新排列成回文字符串(Palindrome Permutation)。回文字符串是指正着读和倒着读都相同的字符串。
示例:
输入: "code"
输出: false
解题思路
最优解法是使用一个 HashSet 来记录字符串中出现的字符,当遍历字符串时,将字符添加到 HashSet 中。如果字符已经在 HashSet 中存在,则说明这个字符出现了偶数次,我们可以将它从 HashSet 中删除。如果字符不在 HashSet 中,说明这个字符第一次出现,我们可以将它添加到 HashSet 中。
最后,判断 HashSet 的大小,如果大小小于等于 1,则说明字符串中的字符都是出现偶数次或者只有一个字符出现奇数次(中心字符),返回 true,否则返回 false。
这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。在一次遍历中,我们就可以完成字符出现次数的统计,因此是非常高效的。
此外,这种解法只需要常数级别的额外空间来存储 HashSet,所以空间复杂度是 O(1),也是非常优秀的。
综上所述,使用 HashSet 来判断一个字符串是否能够通过重新排列成回文字符串是最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
import java.util.HashSet;
/**
* @author yanfengzhang
* @description 判断一个字符串是否能够通过重新排列成回文字符串(Palindrome Permutation)。
* 回文字符串是指正着读和倒着读都相同的字符串。
* 示例:
* 输入: "code"
* 输出: false
* @date 2021/1/23 23:08
*/
public class PalindromePermutation {
/**
* 最优解法是使用一个 HashSet 来记录字符串中出现的字符,当遍历字符串时,将字符添加到 HashSet 中。
* 如果字符已经在 HashSet 中存在,则说明这个字符出现了偶数次,我们可以将它从 HashSet 中删除。
* 如果字符不在 HashSet 中,说明这个字符第一次出现,我们可以将它添加到 HashSet 中。
* 最后,判断 HashSet 的大小,如果大小小于等于 1,则说明字符串中的字符都是出现偶数次或者只有一个字符出现奇数次(中心字符),返回 true,否则返回 false。
* 这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。在一次遍历中,我们就可以完成字符出现次数的统计,因此是非常高效的。
* 此外,这种解法只需要常数级别的额外空间来存储 HashSet,所以空间复杂度是 O(1),也是非常优秀的。
* 综上所述,使用 HashSet 来判断一个字符串是否能够通过重新排列成回文字符串是最优解法,具有高效和节省空间的特点。
*/
public static boolean canPermutePalindrome(String s) {
HashSet<Character> charSet = new HashSet<>();
// 遍历字符串,统计每个字符的出现次数
for (char c : s.toCharArray()) {
// 如果字符已经在 HashSet 中存在,则从 HashSet 中删除
if (charSet.contains(c)) {
charSet.remove(c);
} else {
// 否则将字符添加到 HashSet 中
charSet.add(c);
}
}
// 判断 HashSet 的大小,如果大小小于等于 1,则返回 true,否则返回 false
return charSet.size() <= 1;
}
public static void main(String[] args) {
String str1 = "code";
String str2 = "aab";
String str3 = "carerac";
// 输出结果:Can "code" be permuted to a palindrome? false
System.out.println("Can \"" + str1 + "\" be permuted to a palindrome? " + canPermutePalindrome(str1));
// 输出结果:Can "aab" be permuted to a palindrome? true
System.out.println("Can \"" + str2 + "\" be permuted to a palindrome? " + canPermutePalindrome(str2));
// 输出结果:Can "carerac" be permuted to a palindrome? true
System.out.println("Can \"" + str3 + "\" be permuted to a palindrome? " + canPermutePalindrome(str3));
}
}
9、回文子串个数
题目描述:给定一个字符串,计算其中有多少个回文子串(Palindromic Substrings)。回文子串是指正着读和倒着读都相同的连续字符序列。
示例:
输入: "abc"
输出: 3
解释: 三个回文子串分别是 "a", "b", "c"。
解题思路
这个问题可以通过动态规划或中心扩展法来解决。
- 动态规划解法:
- 创建一个二维数组 dp,其中 dp[i][j] 表示从第 i 个字符到第 j 个字符是否构成回文子串。
- 初始化边界条件,即单个字符和相邻两个字符都是回文子串,即 dp[i][i] = true 和 dp[i][i+1] = true。
- 对于长度大于 2 的子串,dp[i][j] 的值取决于 dp[i+1][j-1] 和 s.charAt(i) == s.charAt(j)。
- 在动态规划的过程中,每次找到 dp[i][j] 为 true 时,说明从第 i 个字符到第 j 个字符构成回文子串,回文子串的个数加 1。
- 中心扩展法解法:
- 从字符串的每个字符和每两个字符之间开始,向两边扩展判断是否构成回文子串。
- 对于奇数长度的回文子串,从单个字符开始向两边扩展。
- 对于偶数长度的回文子串,从相邻两个字符之间开始向两边扩展。
- 在扩展的过程中,每次找到回文子串时,回文子串的个数加 1。
无论使用哪种解法,最终我们都能得到给定字符串中回文子串的个数。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 给定一个字符串,计算其中有多少个回文子串(Palindromic Substrings)。
* 回文子串是指正着读和倒着读都相同的连续字符序列。
* 示例:
* 输入: "abc"
* 输出: 3
* 解释: 三个回文子串分别是 "a", "b", "c"。
* @date 2021/1/25 23:16
*/
public class PalindromicSubstrings {
/**
* * 动态规划解法:
* 创建一个二维数组 dp,其中 dp[i][j] 表示从第 i 个字符到第 j 个字符是否构成回文子串。
* 初始化边界条件,即单个字符和相邻两个字符都是回文子串,即 dp[i][i] = true 和 dp[i][i+1] = true。
* 对于长度大于 2 的子串,dp[i][j] 的值取决于 dp[i+1][j-1] 和 s.charAt(i) == s.charAt(j)。
* 在动态规划的过程中,每次找到 dp[i][j] 为 true 时,说明从第 i 个字符到第 j 个字符构成回文子串,回文子串的个数加 1。
*/
public static int countSubstrings(String s) {
int n = s.length();
// 二维数组用于存储回文子串的信息
boolean[][] dp = new boolean[n][n];
// 回文子串的个数
int count = 0;
// 单个字符是回文子串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
count++;
}
// 相邻两个字符相同的话是回文子串
for (int i = 0; i < n - 1; i++) {
if (s.charAt(i) == s.charAt(i + 1)) {
dp[i][i + 1] = true;
count++;
}
}
// 动态规划,从长度为 3 的子串开始判断是否为回文子串
for (int len = 3; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
// 子串的结束位置
int j = i + len - 1;
// 判断子串是否为回文子串
if (s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1]) {
dp[i][j] = true;
// 更新回文子串的个数
count++;
}
}
}
return count;
}
public static void main(String[] args) {
String s = "abc";
int count = countSubstrings(s);
// 输出结果:Number of Palindromic Substrings: 3
System.out.println("Number of Palindromic Substrings: " + count);
}
}
10、回文子串拆分
题目描述:将一个字符串拆分成若干回文子串,返回所有可能的拆分方案(Palindrome Partitioning)。回文子串是指正着读和倒着读都相同的连续字符序列。
示例:
输入: "aab"
输出: [["a","a","b"],["aa","b"]]
解题思路
这个问题可以通过回溯算法来解决。
回溯算法的思路是:
- 创建一个结果集 List<List<String>>,用于存储所有的拆分方案。
- 创建一个临时列表 List<String>,用于记录当前拆分方案。
- 从字符串的第一个字符开始,遍历字符串的所有子串。
- 对于每个子串,判断是否是回文子串。如果是回文子串,则将其加入临时列表,然后递归处理剩余子串。
- 递归处理剩余子串的过程中,重复步骤 3 和步骤 4,直到遍历完整个字符串。
- 如果临时列表中的字符串组成了一个有效的拆分方案,将其加入结果集。
- 回溯到上一层,尝试其他的拆分方案。
这样,我们就可以得到所有可能的拆分方案。
具体代码展示
package org.zyf.javabasic.letcode.string;
import java.util.ArrayList;
import java.util.List;
/**
* @author yanfengzhang
* @description 将一个字符串拆分成若干回文子串,返回所有可能的拆分方案(Palindrome Partitioning)。
* 回文子串是指正着读和倒着读都相同的连续字符序列。
* 示例:
* 输入: "aab"
* 输出: [["a","a","b"],["aa","b"]]
* @date 2020/4/2 22:20
*/
public class PalindromePartitionings {
/**
* 这个问题可以通过回溯算法来解决。
* 回溯算法的思路是:
* * 创建一个结果集 List<List<String>>,用于存储所有的拆分方案。
* * 创建一个临时列表 List<String>,用于记录当前拆分方案。
* * 从字符串的第一个字符开始,遍历字符串的所有子串。
* * 对于每个子串,判断是否是回文子串。如果是回文子串,则将其加入临时列表,然后递归处理剩余子串。
* * 递归处理剩余子串的过程中,重复步骤 3 和步骤 4,直到遍历完整个字符串。
* * 如果临时列表中的字符串组成了一个有效的拆分方案,将其加入结果集。
* * 回溯到上一层,尝试其他的拆分方案。
* 这样,我们就可以得到所有可能的拆分方案。
*/
public static List<List<String>> partition(String s) {
// 结果集,用于存储所有的拆分方案
List<List<String>> result = new ArrayList<>();
// 临时列表,用于记录当前拆分方案
List<String> current = new ArrayList<>();
// 使用回溯算法得到所有可能的拆分方案
backtrack(s, 0, current, result);
return result;
}
private static void backtrack(String s, int start, List<String> current, List<List<String>> result) {
// 如果起始位置等于字符串的长度,说明已经遍历完整个字符串,将当前拆分方案加入结果集
if (start == s.length()) {
result.add(new ArrayList<>(current));
return;
}
// 从起始位置开始,遍历字符串的所有子串
for (int end = start + 1; end <= s.length(); end++) {
// 当前子串
String substring = s.substring(start, end);
// 判断当前子串是否是回文子串
if (isPalindrome(substring)) {
// 如果是回文子串,将其加入临时列表
current.add(substring);
// 递归处理剩余子串
backtrack(s, end, current, result);
// 回溯到上一层,尝试其他的拆分方案
current.remove(current.size() - 1);
}
}
}
/**
* 判断字符串是否是回文子串
*/
private static boolean isPalindrome(String s) {
int left = 0;
int right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
public static void main(String[] args) {
String s = "aab";
List<List<String>> result = partition(s);
// 输出结果:All Palindromic Partitions: [[a, a, b], [aa, b]]
System.out.println("All Palindromic Partitions: " + result);
}
}
11、字符串压缩
题目描述:编写一个算法来压缩字符串,将连续出现的字符替换为字符和出现次数的组合。如果压缩后的字符串没有变短,则返回原始字符串。
示例 1:
输入: "aabcccccaaa"
输出: "a2b1c5a3"
示例 2:
输入: "abcd"
输出: "abcd"
解释:压缩后的字符串 "a2b1c5a3" 比原始字符串 "aabcccccaaa" 更短,返回压缩后的字符串。
解题思路
最优解法是使用双指针来遍历字符串,同时在遍历过程中进行压缩字符的操作。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 创建一个 StringBuilder,用于存储压缩后的字符。
- 使用两个指针 i 和 j 分别表示当前遍历的字符位置和连续出现字符的起始位置,初始值为 0。
- 在循环中,让指针 j 向右移动直到找到不同的字符为止,此时 j 指向连续出现字符的结束位置。
- 计算连续出现字符的个数 count,将字符和个数的组合添加到 StringBuilder 中。
- 更新指针 i 为 j + 1,准备处理下一个字符。
- 最后,如果压缩后的字符串没有变短,则返回原始字符串,否则返回压缩后的字符串。
这种解法只需遍历一次字符串,通过双指针来统计连续出现字符的个数,并将字符和个数的组合添加到 StringBuilder 中,最终得到压缩后的字符串。
这种解法的空间复杂度是 O(1),因为只使用了常数级别的额外空间。
综上所述,使用双指针遍历字符串进行压缩字符的操作是最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 编写一个算法来压缩字符串,将连续出现的字符替换为字符和出现次数的组合。如果压缩后的字符串没有变短,则返回原始字符串。
* 示例 1:
* 输入: "aabcccccaaa"
* 输出: "a2b1c5a3"
* 示例 2:
* 输入: "abcd"
* 输出: "abcd"
* 解释:压缩后的字符串 "a2b1c5a3" 比原始字符串 "aabcccccaaa" 更短,返回压缩后的字符串。
* @date 2020/5/2 23:25
*/
public class StringCompression {
/**
* 最优解法是使用双指针来遍历字符串,同时在遍历过程中进行压缩字符的操作。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 创建一个 StringBuilder,用于存储压缩后的字符。
* * 使用两个指针 i 和 j 分别表示当前遍历的字符位置和连续出现字符的起始位置,初始值为 0。
* * 在循环中,让指针 j 向右移动直到找到不同的字符为止,此时 j 指向连续出现字符的结束位置。
* * 计算连续出现字符的个数 count,将字符和个数的组合添加到 StringBuilder 中。
* * 更新指针 i 为 j + 1,准备处理下一个字符。
* * 最后,如果压缩后的字符串没有变短,则返回原始字符串,否则返回压缩后的字符串。
* 这种解法只需遍历一次字符串,通过双指针来统计连续出现字符的个数,并将字符和个数的组合添加到 StringBuilder 中,最终得到压缩后的字符串。
* 这种解法的空间复杂度是 O(1),因为只使用了常数级别的额外空间。
* 综上所述,使用双指针遍历字符串进行压缩字符的操作是最优解法,具有高效和节省空间的特点。
*/
public static String compressString(String s) {
int n = s.length();
StringBuilder compressedString = new StringBuilder();
// 指针 i 表示当前遍历的字符位置
int i = 0;
// 指针 j 表示连续出现字符的起始位置
int j = 0;
while (i < n) {
// 让指针 j 向右移动直到找到不同的字符为止
while (j < n && s.charAt(j) == s.charAt(i)) {
j++;
}
// 计算连续出现字符的个数
int count = j - i;
compressedString.append(s.charAt(i));
compressedString.append(count);
// 更新指针 i 为 j,准备处理下一个字符
i = j;
}
// 如果压缩后的字符串没有变短,则返回原始字符串,否则返回压缩后的字符串
return compressedString.length() >= n ? s : compressedString.toString();
}
public static void main(String[] args) {
String s1 = "aabcccccaaa";
String s2 = "abcd";
// 输出结果:Compressed String for "aabcccccaaa": a2b1c5a3
System.out.println("Compressed String for \"" + s1 + "\": " + compressString(s1));
// 输出结果:Compressed String for "abcd": abcd
System.out.println("Compressed String for \"" + s2 + "\": " + compressString(s2));
}
}
12、字符串编辑距离
题目描述:编辑距离(Edit Distance),也称为Levenshtein距离,是指计算两个字符串之间的最小操作次数,使得一个字符串转换成另一个字符串。允许的操作包括插入一个字符、删除一个字符和替换一个字符。
示例:
输入: word1 = "horse", word2 = "ros"
输出: 3
解释: horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
具体解法见动态规划总结与编程练习_动态规划 编程_张彦峰ZYF的博客-CSDN博客中的第11题。
13、字符串排列
题目描述:给定两个字符串 s1 和 s2,判断 s2 是否包含 s1 的排列组合。
排列组合是指通过改变字符串中字符的顺序,得到新的字符串。例如,"abc" 的排列组合有 "abc"、"acb"、"bac"、"bca"、"cab" 和 "cba"。
示例 1:
输入: s1 = "abc", s2 = "eidbacooo"
输出: true
解释: s2 包含 s1 的排列组合 "bac"。
示例 2:
输入: s1 = "ab", s2 = "eidboaoo"
输出: false
解题思路
最优解法是使用滑动窗口和计数数组来解决字符串排列组合问题。这种解法的时间复杂度是 O(n),其中 n 是字符串 s2 的长度。
具体的最优解法步骤如下:
- 创建两个长度为 26 的计数数组 count1 和 count2,用于分别统计字符串 s1 和 s2 中字符出现的次数。
- 遍历字符串 s1,统计其中每个字符出现的次数,并更新计数数组 count1。
- 使用滑动窗口,遍历字符串 s2,首先统计窗口大小为 s1.length() 的字符个数,并更新计数数组 count2。
- 然后,将计数数组 count2 与 count1 进行比较,如果相等,则说明 s2 包含 s1 的排列组合。
- 移动滑动窗口,继续比较下一个窗口。
这种解法只需遍历一次字符串 s2,通过滑动窗口和计数数组来统计字符出现的次数,从而判断 s2 是否包含 s1 的排列组合。
这种解法的空间复杂度是 O(1),因为计数数组的长度为固定的 26。
综上所述,使用滑动窗口和计数数组解决字符串排列组合问题是最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
import java.util.Arrays;
/**
* @author yanfengzhang
* @description 给定两个字符串 s1 和 s2,判断 s2 是否包含 s1 的排列组合。
* 排列组合是指通过改变字符串中字符的顺序,得到新的字符串。例如,"abc" 的排列组合有 "abc"、"acb"、"bac"、"bca"、"cab" 和 "cba"。
* 示例 1:
* 输入: s1 = "abc", s2 = "eidbacooo"
* 输出: true
* 解释: s2 包含 s1 的排列组合 "bac"。
* 示例 2:
* 输入: s1 = "ab", s2 = "eidboaoo"
* 输出: false
* @date 2021/1/26 22:49
*/
public class StringPermutation {
/**
* 最优解法是使用滑动窗口和计数数组来解决字符串排列组合问题。这种解法的时间复杂度是 O(n),其中 n 是字符串 s2 的长度。
* 具体的最优解法步骤如下:
* * 创建两个长度为 26 的计数数组 count1 和 count2,用于分别统计字符串 s1 和 s2 中字符出现的次数。
* * 遍历字符串 s1,统计其中每个字符出现的次数,并更新计数数组 count1。
* * 使用滑动窗口,遍历字符串 s2,首先统计窗口大小为 s1.length() 的字符个数,并更新计数数组 count2。
* * 然后,将计数数组 count2 与 count1 进行比较,如果相等,则说明 s2 包含 s1 的排列组合。
* * 移动滑动窗口,继续比较下一个窗口。
* 这种解法只需遍历一次字符串 s2,通过滑动窗口和计数数组来统计字符出现的次数,从而判断 s2 是否包含 s1 的排列组合。
* 这种解法的空间复杂度是 O(1),因为计数数组的长度为固定的 26。
* 综上所述,使用滑动窗口和计数数组解决字符串排列组合问题是最优解法,具有高效和节省空间的特点。
*/
public static boolean checkInclusion(String s1, String s2) {
int n = s1.length();
int m = s2.length();
if (n > m) {
return false;
}
// 计数数组用于统计字符串 s1 中字符出现的次数
int[] count1 = new int[26];
// 计数数组用于统计字符串 s2 中字符出现的次数
int[] count2 = new int[26];
// 统计字符串 s1 中每个字符出现的次数,更新计数数组 count1
for (char c : s1.toCharArray()) {
count1[c - 'a']++;
}
// 使用滑动窗口,遍历字符串 s2
for (int i = 0; i < m; i++) {
// 窗口的大小为字符串 s1 的长度,统计窗口中字符出现的次数,更新计数数组 count2
count2[s2.charAt(i) - 'a']++;
// 当窗口的大小超过字符串 s1 的长度时,左边界向右移动一个位置,缩小窗口
if (i >= n) {
count2[s2.charAt(i - n) - 'a']--;
}
// 将计数数组 count2 与 count1 进行比较,如果相等,则说明 s2 包含 s1 的排列组合
if (Arrays.equals(count1, count2)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
String s1 = "abc";
String s2 = "eidbacooo";
boolean result = checkInclusion(s1, s2);
// 输出结果:Is "eidbacooo" contains permutation of "abc": true
System.out.println("Is \"" + s2 + "\" contains permutation of \"" + s1 + "\": " + result);
}
}
14、字符串转换整数(atoi)
题目描述:请你来实现一个函数 atoi,使其能将字符串转换成整数。
函数 atoi 的算法需要满足以下条件:
- 读取字符串并丢弃无用的前导空格。
- 检查第一个字符(假设该字符为 c)是否为正号 '+' 或负号 '-',或者是否为数字(即 '0' - '9')。
- 解析字符串中的整数,直到不能继续读取为止。
- 解析得到的整数需要满足下列条件:
- 如果整数数值大于 INT_MAX,返回 INT_MAX。
- 如果整数数值小于 INT_MIN,返回 INT_MIN。
示例 1:
输入:s = "42"
输出:42
示例 2:
输入:s = " -42"
输出:-42
解释:第一个非空白字符为 '-', 它是一个负号。
我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
示例 3:
输入:s = "4193 with words"
输出:4193
解释:解析出数字 4193 。
示例 4:
输入:s = "words and 987"
输出:0
解释:第一个非空字符是 'w',但它不是数字或正、负号。
因此无法执行有效的转换。
示例 5:
输入:s = "-91283472332"
输出:-2147483648
解释:解析出数字 -2147483648 。
注意:
- 本题中的空白字符只包括空格字符 ' ' 。
- 除前导空格或数字后的其余字符串外,请勿忽略任何其他字符。
解题思路
最优解法是使用有限状态机来解决字符串转换整数的问题。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
有限状态机的思想是通过定义不同的状态来处理不同的字符。在字符串转换整数的问题中,我们可以将有限状态机定义为以下几个状态:
- 起始空格状态(START)
- 符号状态(SIGN)
- 数字状态(NUMBER)
- 结束状态(END)
具体的最优解法步骤如下:
- 定义有限状态机,并初始化当前状态为起始空格状态(START)。
- 遍历字符串的每个字符,根据当前状态和当前字符进行状态转移,处理不同的情况。
- 如果遍历完成后仍处于符号状态(SIGN)或起始空格状态(START),说明没有有效数字,返回 0。
- 如果得到的结果大于 INT_MAX,返回 INT_MAX;如果得到的结果小于 INT_MIN,返回 INT_MIN。
这种解法只需遍历一次字符串,通过有限状态机来解析整数并进行状态转移,从而得到转换后的整数。
这种解法的空间复杂度是 O(1),因为只使用了常数级别的额外空间。
综上所述,使用有限状态机解决字符串转换整数问题是最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 请你来实现一个函数 atoi,使其能将字符串转换成整数。
* 函数 atoi 的算法需要满足以下条件:
* * 读取字符串并丢弃无用的前导空格。
* * 检查第一个字符(假设该字符为 c)是否为正号 '+' 或负号 '-',或者是否为数字(即 '0' - '9')。
* * 解析字符串中的整数,直到不能继续读取为止。
* * 解析得到的整数需要满足下列条件:
* * 如果整数数值大于 INT_MAX,返回 INT_MAX。
* * 如果整数数值小于 INT_MIN,返回 INT_MIN。
* 示例 1:
* 输入:s = "42"
* 输出:42
* 示例 2:
* 输入:s = " -42"
* 输出:-42
* 解释:第一个非空白字符为 '-', 它是一个负号。
* 我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。
* 示例 3:
* 输入:s = "4193 with words"
* 输出:4193
* 解释:解析出数字 4193 。
* 示例 4:
* 输入:s = "words and 987"
* 输出:0
* 解释:第一个非空字符是 'w',但它不是数字或正、负号。
* 因此无法执行有效的转换。
* 示例 5:
* 输入:s = "-91283472332"
* 输出:-2147483648
* 解释:解析出数字 -2147483648 。
* 注意:
* * 本题中的空白字符只包括空格字符 ' ' 。
* * 除前导空格或数字后的其余字符串外,请勿忽略任何其他字符。
* @date 2021/1/27 23:53
*/
public class StringToInteger {
/**
* 最优解法是使用有限状态机来解决字符串转换整数的问题。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 有限状态机的思想是通过定义不同的状态来处理不同的字符。在字符串转换整数的问题中,我们可以将有限状态机定义为以下几个状态:
* * 起始空格状态(START)
* * 符号状态(SIGN)
* * 数字状态(NUMBER)
* * 结束状态(END)
* 具体的最优解法步骤如下:
* * 定义有限状态机,并初始化当前状态为起始空格状态(START)。
* * 遍历字符串的每个字符,根据当前状态和当前字符进行状态转移,处理不同的情况。
* * 如果遍历完成后仍处于符号状态(SIGN)或起始空格状态(START),说明没有有效数字,返回 0。
* * 如果得到的结果大于 INT_MAX,返回 INT_MAX;如果得到的结果小于 INT_MIN,返回 INT_MIN。
* 这种解法只需遍历一次字符串,通过有限状态机来解析整数并进行状态转移,从而得到转换后的整数。
* 这种解法的空间复杂度是 O(1),因为只使用了常数级别的额外空间。
* 综上所述,使用有限状态机解决字符串转换整数问题是最优解法,具有高效和节省空间的特点。
*/
public static int myAtoi(String str) {
// 定义有限状态机
// 起始空格状态
final int START = 0;
// 符号状态
final int SIGN = 1;
// 数字状态
final int NUMBER = 2;
// 结束状态
final int END = 3;
// 初始化当前状态为起始空格状态
int state = START;
// 初始化符号,默认为正号
int sign = 1;
// 初始化结果
int result = 0;
for (char c : str.toCharArray()) {
if (state == START) {
// 处理起始空格状态
if (c == ' ') {
continue;
} else if (c == '+' || c == '-') {
// 如果是符号字符,则切换到符号状态
state = SIGN;
sign = (c == '+') ? 1 : -1;
} else if (Character.isDigit(c)) {
// 如果是数字字符,则切换到数字状态
state = NUMBER;
result = c - '0';
} else {
// 其他字符,直接返回 0
return 0;
}
} else if (state == SIGN) {
// 处理符号状态
if (Character.isDigit(c)) {
// 如果是数字字符,则切换到数字状态
state = NUMBER;
result = c - '0';
} else {
// 其他字符,直接返回 0
return 0;
}
} else if (state == NUMBER) {
// 处理数字状态
if (Character.isDigit(c)) {
// 如果是数字字符,则继续更新结果
int digit = c - '0';
// 判断是否溢出
if (result > (Integer.MAX_VALUE - digit) / 10) {
return (sign == 1) ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
result = result * 10 + digit;
} else {
// 遇到非数字字符,退出循环
break;
}
}
}
// 返回最终结果
return sign * result;
}
public static void main(String[] args) {
String str1 = "42";
String str2 = " -42";
String str3 = "4193 with words";
String str4 = "words and 987";
String str5 = "-91283472332";
// 输出结果:String to Integer for "42": 42
System.out.println("String to Integer for \"" + str1 + "\": " + myAtoi(str1));
// 输出结果:String to Integer for " -42": -42
System.out.println("String to Integer for \"" + str2 + "\": " + myAtoi(str2));
// 输出结果:String to Integer for "4193 with words": 4193
System.out.println("String to Integer for \"" + str3 + "\": " + myAtoi(str3));
// 输出结果:String to Integer for "words and 987": 0
System.out.println("String to Integer for \"" + str4 + "\": " + myAtoi(str4));
// 输出结果:String to Integer for "-91283472332": -2147483648
System.out.println("String to Integer for \"" + str5 + "\": " + myAtoi(str5));
}
}
15、字符串中的字符频次统计
题目描述:给定一个字符串,统计其中每个字符出现的频次。
示例:
输入: "hello"
输出: {'h': 1, 'e': 1, 'l': 2, 'o': 1}
解题思路
最优解法是使用哈希表来统计字符串中每个字符出现的频次。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 创建一个哈希表 freqMap,用于记录每个字符及其对应的出现频次。
- 遍历字符串的每个字符,将字符作为键,出现的次数作为值,更新哈希表 freqMap。
- 最后,freqMap 中的键值对即为字符串中每个字符出现的频次。
这种解法只需遍历一次字符串,通过哈希表来统计字符的频次,从而得到每个字符出现的频次。
这种解法的空间复杂度是 O(k),其中 k 是字符串中不同字符的个数。在最坏情况下,每个字符都不相同,哈希表的大小为字符串的长度,即空间复杂度为 O(n)。
综上所述,使用哈希表来统计字符串中每个字符出现的频次是最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
import java.util.HashMap;
import java.util.Map;
/**
* @author yanfengzhang
* @description 给定一个字符串,统计其中每个字符出现的频次。
* 示例:
* 输入: "hello"
* 输出: {'h': 1, 'e': 1, 'l': 2, 'o': 1}
* @date 2021/2/2 23:57
*/
public class CharacterFrequency {
/**
* 最优解法是使用哈希表来统计字符串中每个字符出现的频次。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 创建一个哈希表 freqMap,用于记录每个字符及其对应的出现频次。
* * 遍历字符串的每个字符,将字符作为键,出现的次数作为值,更新哈希表 freqMap。
* * 最后,freqMap 中的键值对即为字符串中每个字符出现的频次。
* 这种解法只需遍历一次字符串,通过哈希表来统计字符的频次,从而得到每个字符出现的频次。
* 这种解法的空间复杂度是 O(k),其中 k 是字符串中不同字符的个数。在最坏情况下,每个字符都不相同,哈希表的大小为字符串的长度,即空间复杂度为 O(n)。
* 综上所述,使用哈希表来统计字符串中每个字符出现的频次是最优解法,具有高效和节省空间的特点。
*/
public static Map<Character, Integer> getCharacterFrequency(String str) {
// 创建哈希表 freqMap,用于记录每个字符及其对应的出现频次
Map<Character, Integer> freqMap = new HashMap<>();
// 遍历字符串的每个字符,更新哈希表 freqMap
for (char c : str.toCharArray()) {
freqMap.put(c, freqMap.getOrDefault(c, 0) + 1);
}
// 返回结果,即字符频次统计的哈希表 freqMap
return freqMap;
}
public static void main(String[] args) {
String str = "hello";
Map<Character, Integer> frequencyMap = getCharacterFrequency(str);
// 输出结果:Character Frequency for "hello": {h=1, e=1, l=2, o=1}
System.out.println("Character Frequency for \"" + str + "\": " + frequencyMap);
}
}
16、字符串旋转
题目描述:给定两个字符串 s1 和 s2,判断 s2 是否是由 s1 旋转得到的结果。
旋转字符串是指将字符串的一部分移到末尾。例如,字符串 'water' 经过旋转后可以得到字符串 'aterw'。
示例 1:
输入:s1 = "water", s2 = "aterw"
输出:true
解释:s2 是 s1 旋转得到的结果。
示例 2:
输入:s1 = "abcde", s2 = "deabc"
输出:true
解释:s2 是 s1 旋转得到的结果。
示例 3:
输入:s1 = "abcde", s2 = "abced"
输出:false
解释:s2 不是 s1 旋转得到的结果。
解题思路
最优解法是使用字符串拼接的方法来判断一个字符串是否是另一个字符串旋转得到的结果。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 将字符串 s1 重复拼接一次,得到新的字符串 s1s1。
- 判断字符串 s2 是否是字符串 s1s1 的子串。
这种解法的核心思想是:旋转得到的字符串是由原字符串重复拼接后形成的,因此如果 s2 是由 s1 旋转得到的结果,那么 s2 必然是 s1s1 的子串。
这种解法只需将 s1 重复拼接一次,然后判断 s2 是否是 s1s1 的子串,从而得到字符串是否是旋转得到的结果。
这种解法的空间复杂度是 O(n),其中 n 是字符串的长度,因为需要额外的空间来存储重复拼接后的新字符串 s1s1。
综上所述,使用字符串拼接的方法来判断字符串是否是旋转得到的结果是最优解法,具有高效和简洁的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 给定两个字符串 s1 和 s2,判断 s2 是否是由 s1 旋转得到的结果。
* 旋转字符串是指将字符串的一部分移到末尾。例如,字符串 'water' 经过旋转后可以得到字符串 'aterw'。
* 示例 1:
* 输入:s1 = "water", s2 = "aterw"
* 输出:true
* 解释:s2 是 s1 旋转得到的结果。
* 示例 2:
* 输入:s1 = "abcde", s2 = "deabc"
* 输出:true
* 解释:s2 是 s1 旋转得到的结果。
* 示例 3:
* 输入:s1 = "abcde", s2 = "abced"
* 输出:false
* 解释:s2 不是 s1 旋转得到的结果。
* @date 2021/2/7 23:01
*/
public class StringRotation {
/**
* 最优解法是使用字符串拼接的方法来判断一个字符串是否是另一个字符串旋转得到的结果。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 将字符串 s1 重复拼接一次,得到新的字符串 s1s1。
* * 判断字符串 s2 是否是字符串 s1s1 的子串。
* 这种解法的核心思想是:旋转得到的字符串是由原字符串重复拼接后形成的,因此如果 s2 是由 s1 旋转得到的结果,那么 s2 必然是 s1s1 的子串。
* 这种解法只需将 s1 重复拼接一次,然后判断 s2 是否是 s1s1 的子串,从而得到字符串是否是旋转得到的结果。
* 这种解法的空间复杂度是 O(n),其中 n 是字符串的长度,因为需要额外的空间来存储重复拼接后的新字符串 s1s1。
* 综上所述,使用字符串拼接的方法来判断字符串是否是旋转得到的结果是最优解法,具有高效和简洁的特点。
*/
public static boolean isRotation(String s1, String s2) {
// 将字符串 s1 重复拼接一次,得到新的字符串 s1s1
String s1s1 = s1 + s1;
// 判断字符串 s2 是否是字符串 s1s1 的子串
return s1s1.contains(s2);
}
public static void main(String[] args) {
String s1 = "water";
String s2 = "aterw";
boolean result = isRotation(s1, s2);
// 输出结果: "aterw" is rotation of "water": true
System.out.println("\"" + s2 + "\" is rotation of \"" + s1 + "\": " + result);
}
}
17、字符串的循环移位
题目描述:实现字符串的循环移位操作,将字符串向右循环移动指定的位数。
例如,将字符串 "abcd" 循环右移两位,得到字符串 "cdab"。
解题思路
最优解法是使用字符串切片的方法来实现字符串的循环移位操作。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 对于要循环右移的位数 k,先将 k 对字符串长度取模,以处理 k 大于字符串长度的情况。
- 将字符串 s 分成两部分,前半部分从索引 0 到 n - k - 1,后半部分从索引 n - k 到 n - 1,其中 n 是字符串的长度。
- 将前半部分和后半部分重新拼接得到循环右移后的字符串。
这种解法的核心思想是,将字符串的后部分移到前面,形成循环右移的效果。
这种解法只需进行一次字符串切片和拼接,从而实现字符串的循环移位操作。
这种解法的空间复杂度是 O(n),其中 n 是字符串的长度,因为需要额外的空间来存储切片后的字符串的两部分。
综上所述,使用字符串切片的方法来实现字符串的循环移位操作是最优解法,具有高效和简洁的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 实现字符串的循环移位操作,将字符串向右循环移动指定的位数。
* 例如,将字符串 "abcd" 循环右移两位,得到字符串 "cdab"。
* @date 2021/2/12 23:05
*/
public class StringRotationing {
/**
* 最优解法是使用字符串切片的方法来实现字符串的循环移位操作。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 对于要循环右移的位数 k,先将 k 对字符串长度取模,以处理 k 大于字符串长度的情况。
* * 将字符串 s 分成两部分,前半部分从索引 0 到 n - k - 1,后半部分从索引 n - k 到 n - 1,其中 n 是字符串的长度。
* * 将前半部分和后半部分重新拼接得到循环右移后的字符串。
* 这种解法的核心思想是,将字符串的后部分移到前面,形成循环右移的效果。
* 这种解法只需进行一次字符串切片和拼接,从而实现字符串的循环移位操作。
* 这种解法的空间复杂度是 O(n),其中 n 是字符串的长度,因为需要额外的空间来存储切片后的字符串的两部分。
* 综上所述,使用字符串切片的方法来实现字符串的循环移位操作是最优解法,具有高效和简洁的特点。
*/
public static String rotateString(String s, int k) {
// 对于要循环右移的位数 k,先将 k 对字符串长度取模,以处理 k 大于字符串长度的情况
int n = s.length();
k %= n;
// 将字符串 s 分成两部分,前半部分从索引 0 到 n - k - 1,后半部分从索引 n - k 到 n - 1
String firstPart = s.substring(0, n - k);
String secondPart = s.substring(n - k, n);
// 将前半部分和后半部分重新拼接得到循环右移后的字符串
return secondPart + firstPart;
}
public static void main(String[] args) {
String s = "abcd";
int k = 2;
String result = rotateString(s, k);
// 输出结果:Rotate "abcd" 2 positions to the right: "cdab"
System.out.println("Rotate \"" + s + "\" " + k + " positions to the right: " + result);
}
}
18、最长无重复字符子串
题目描述:给定一个字符串,找到其中最长的连续无重复字符的子串。
例如,对于字符串 "abcabcbb",最长的连续无重复字符的子串是 "abc",其长度为 3。
具体见散列表相关知识及编程练习总结_散列函数的三个要求_张彦峰ZYF的博客-CSDN博客中的第1题。
19、字符串的压缩与解压缩
题目描述:实现字符串的压缩和解压缩功能,例如 Run-Length Encoding (RLE)。
Run-Length Encoding 是一种简单的压缩算法,它将连续出现的字符替换为字符和出现次数的组合。
例如,对于字符串 "aaabbcccaa",使用 Run-Length Encoding 可以将其压缩为 "3a2b3c2a"。
解压缩则是将压缩后的字符串还原为原始字符串的过程。
解题思路
最优解法是使用遍历字符串的方法来实现字符串的压缩和解压缩功能。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 对于压缩功能,遍历字符串并统计连续相同字符的数量,将连续相同字符替换为字符和出现次数的组合。
- 对于解压缩功能,遍历压缩后的字符串,将字符和出现次数的组合还原为连续相同字符。
这种解法的核心思想是遍历字符串并对连续相同字符进行统计,从而实现字符串的压缩和解压缩功能。
这种解法只需要遍历一次字符串,同时使用常数大小的额外空间来存储压缩和解压缩后的字符串,因此空间复杂度是 O(1)。
综上所述,使用遍历字符串的方法来实现字符串的压缩和解压缩功能是最优解法,具有高效和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 实现字符串的压缩和解压缩功能,例如 Run-Length Encoding (RLE)。
* Run-Length Encoding 是一种简单的压缩算法,它将连续出现的字符替换为字符和出现次数的组合。
* 例如,对于字符串 "aaabbcccaa",使用 Run-Length Encoding 可以将其压缩为 "3a2b3c2a"。
* 解压缩则是将压缩后的字符串还原为原始字符串的过程。
* @date 2021/3/2 23:12
*/
public class StringCompressions {
/**
* 实现字符串压缩功能
*/
public static String compress(String str) {
StringBuilder compressed = new StringBuilder();
int count = 1;
// 遍历字符串并统计连续相同字符的数量
for (int i = 0; i < str.length() - 1; i++) {
if (str.charAt(i) == str.charAt(i + 1)) {
count++;
} else {
// 将连续相同字符替换为字符和出现次数的组合
compressed.append(count).append(str.charAt(i));
count = 1;
}
}
// 处理最后一个字符
compressed.append(count).append(str.charAt(str.length() - 1));
return compressed.toString();
}
/**
* 实现字符串解压缩功能
*/
public static String decompress(String str) {
StringBuilder decompressed = new StringBuilder();
int count = 0;
// 遍历压缩后的字符串
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (Character.isDigit(c)) {
// 获取连续出现的次数
count = count * 10 + (c - '0');
} else {
// 将字符和出现次数的组合还原为连续相同字符
for (int j = 0; j < count; j++) {
decompressed.append(c);
}
count = 0;
}
}
return decompressed.toString();
}
public static void main(String[] args) {
String originalStr = "aaabbcccaa";
String compressedStr = compress(originalStr);
// 输出结果:Compressed: 3a2b3c2a
System.out.println("Compressed: " + compressedStr);
String decompressedStr = decompress(compressedStr);
// 输出结果:Decompressed: aaabbcccaa
System.out.println("Decompressed: " + decompressedStr);
}
}
20、字符串的逆序对数
题目描述:计算一个字符串中逆序对的数量。逆序对是指在一个字符串中,如果两个字符的顺序与它们在原字符串中的顺序相反,那么它们构成一个逆序对。
例如,对于字符串 "acbd",它包含两个逆序对:(a, b) 和 (c, b)。
解题思路
最优解法是使用归并排序的方法来计算一个字符串中逆序对的数量。这种解法的时间复杂度是 O(n log n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 字符串分成两个部分,分别计算每个部分中的逆序对数。
- 合并两个部分,并同时计算合并后的部分中的逆序对数。
这种解法的核心思想是利用归并排序的过程来统计逆序对的数量。
在归并排序的过程中,将数组或字符串拆分成更小的部分,然后逐步合并这些部分并排序。在合并的过程中,统计逆序对的数量。
由于归并排序的时间复杂度是 O(n log n),其中 n 是字符串的长度,因此这种解法的时间复杂度也是 O(n log n)。
这种解法只需要使用常数大小的额外空间来存储临时数组和合并后的结果,因此空间复杂度是 O(1)。
综上所述,使用归并排序的方法来计算一个字符串中逆序对的数量是最优解法,具有高效的时间复杂度和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 计算一个字符串中逆序对的数量。
* 逆序对是指在一个字符串中,如果两个字符的顺序与它们在原字符串中的顺序相反,那么它们构成一个逆序对。
* 例如,对于字符串 "acbd",它包含两个逆序对:(a, b) 和 (c, b)。
* @date 2023/3/5 23:17
*/
public class ReversePairsInString {
/**
* 最优解法是使用归并排序的方法来计算一个字符串中逆序对的数量。这种解法的时间复杂度是 O(n log n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 字符串分成两个部分,分别计算每个部分中的逆序对数。
* * 合并两个部分,并同时计算合并后的部分中的逆序对数。
* 这种解法的核心思想是利用归并排序的过程来统计逆序对的数量。
* 在归并排序的过程中,将数组或字符串拆分成更小的部分,然后逐步合并这些部分并排序。在合并的过程中,统计逆序对的数量。
* 由于归并排序的时间复杂度是 O(n log n),其中 n 是字符串的长度,因此这种解法的时间复杂度也是 O(n log n)。
* 这种解法只需要使用常数大小的额外空间来存储临时数组和合并后的结果,因此空间复杂度是 O(1)。
* 综上所述,使用归并排序的方法来计算一个字符串中逆序对的数量是最优解法,具有高效的时间复杂度和节省空间的特点。
*/
public static int reversePairs(String s) {
char[] array = s.toCharArray();
// 创建一个辅助数组,用于归并排序过程中存储临时结果
char[] temp = new char[array.length];
return mergeSortAndCount(array, temp, 0, array.length - 1);
}
/**
* 归并排序过程中计算逆序对的数量
*/
private static int mergeSortAndCount(char[] array, char[] temp, int left, int right) {
if (left >= right) {
return 0;
}
int mid = left + (right - left) / 2;
int count = 0;
// 递归地计算左半部分和右半部分的逆序对数量
count += mergeSortAndCount(array, temp, left, mid);
count += mergeSortAndCount(array, temp, mid + 1, right);
// 合并左半部分和右半部分,并计算合并后的逆序对数量
count += mergeAndCount(array, temp, left, mid, right);
return count;
}
/**
* 计算合并后的逆序对数量
*/
private static int mergeAndCount(char[] array, char[] temp, int left, int mid, int right) {
int i = left;
int j = mid + 1;
int count = 0;
for (int k = left; k <= right; k++) {
temp[k] = array[k];
}
for (int k = left; k <= right; k++) {
if (i > mid) {
array[k] = temp[j++];
} else if (j > right) {
array[k] = temp[i++];
} else if (temp[i] <= temp[j]) {
array[k] = temp[i++];
} else {
// 当前元素左半部分比右半部分大,说明存在逆序对
array[k] = temp[j++];
count += mid - i + 1;
}
}
return count;
}
public static void main(String[] args) {
String s = "acbd";
int count = reversePairs(s);
// 输出结果:Number of reverse pairs in the string: 2
System.out.println("Number of reverse pairs in the string: " + count);
}
}
21、最小覆盖子串
题目描述:给定一个字符串 s 和一个包含指定字符集的字符串 t,在 s 中找到包含 t 中所有字符的最短子串。
例如,对于字符串 s = "ADOBECODEBANC" 和 t = "ABC",最短子串是 "BANC"。
具体可见散列表相关知识及编程练习总结_散列函数的三个要求_张彦峰ZYF的博客-CSDN博客中的第3题。
22、字符串中的单词反转
题目描述:将一个字符串中的每个单词反转。
例如,对于字符串 "Let's take LeetCode contest",反转每个单词后得到 "s'teL ekat edoCteeL tsetnoc"。
解题思路
最优解法是使用双指针的方法来将一个字符串中的每个单词反转。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 定义两个指针 start 和 end,分别表示当前单词的起始位置和结束位置。
- 遍历字符串,当遇到空格或到达字符串末尾时,将 start 到 end 之间的字符进行反转。
- 更新 start 和 end,继续遍历字符串直到结束。
这种解法的核心思想是利用双指针来定位每个单词的起始位置和结束位置,然后对每个单词进行反转。
由于这个解法只需要遍历一次字符串,并且在遍历过程中对每个单词进行反转操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
另外,由于只需要使用常数大小的额外空间来进行指针操作和字符反转,空间复杂度是 O(1)。
综上所述,使用双指针的方法来将一个字符串中的每个单词反转是最优解法,具有高效的时间复杂度和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 将一个字符串中的每个单词反转。
* 例如,对于字符串 "Let's take LeetCode contest",
* 反转每个单词后得到 "s'teL ekat edoCteeL tsetnoc"。
* @date 2021/3/6 23:24
*/
public class WordReverse {
/**
* 最优解法是使用双指针的方法来将一个字符串中的每个单词反转。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 定义两个指针 start 和 end,分别表示当前单词的起始位置和结束位置。
* * 遍历字符串,当遇到空格或到达字符串末尾时,将 start 到 end 之间的字符进行反转。
* * 更新 start 和 end,继续遍历字符串直到结束。
* 这种解法的核心思想是利用双指针来定位每个单词的起始位置和结束位置,然后对每个单词进行反转。
* 由于这个解法只需要遍历一次字符串,并且在遍历过程中对每个单词进行反转操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
* 另外,由于只需要使用常数大小的额外空间来进行指针操作和字符反转,空间复杂度是 O(1)。
* 综上所述,使用双指针的方法来将一个字符串中的每个单词反转是最优解法,具有高效的时间复杂度和节省空间的特点。
*/
public static String reverseWords(String s) {
char[] charArray = s.toCharArray();
// 当前单词的起始位置
int start = 0;
// 当前单词的结束位置
int end = 0;
for (int i = 0; i < charArray.length; i++) {
if (charArray[i] == ' ') {
// 遇到空格,反转当前单词
reverseWord(charArray, start, end);
// 更新下一个单词的起始位置
start = i + 1;
}
// 更新当前单词的结束位置
end = i;
}
// 反转最后一个单词
reverseWord(charArray, start, end);
return new String(charArray);
}
/**
* 反转单词的函数
*/
private static void reverseWord(char[] charArray, int start, int end) {
while (start < end) {
char temp = charArray[start];
charArray[start] = charArray[end];
charArray[end] = temp;
start++;
end--;
}
}
public static void main(String[] args) {
String s = "Let's take LeetCode contest";
String reversed = reverseWords(s);
System.out.println("Reversed Words: " + reversed);
}
}
23、字符串的Z字形变换
题目描述:将一个字符串按照指定的行数进行 Z 字形排列后,逐行读取得到新的字符串。
例如,对于字符串 "PAYPALISHIRING" 和行数 3,按照 Z 字形排列得到:
P A H N
A P L S I I G
Y I R
逐行读取得到新的字符串为 "PAHNAPLSIIGYIR"。
解题思路
最优解法是使用模拟 Z 字形排列的方法来将一个字符串按照指定的行数进行 Z 字形变换。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 创建一个长度为行数的字符串数组,用于保存每一行的字符。
- 定义一个变量 curRow,表示当前行数,初始值为 0,表示从第一行开始。
- 定义一个变量 goingDown,表示当前方向,初始值为 false,表示当前方向是向上。
- 遍历原始字符串的每个字符,按照 Z 字形排列的顺序将字符放入对应的行,并根据当前方向的变化更新 curRow 和 goingDown。
- 最后,将每一行的字符按顺序拼接成新的字符串。
这种解法的核心思想是模拟 Z 字形排列的过程,按顺序将字符放入对应的行,并根据当前方向的变化更新行数 curRow 和方向 goingDown。
由于这个解法只需要遍历一次原始字符串,并且在遍历过程中只进行简单的判断和字符放入操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
另外,除了存储原始字符串和结果字符串外,这个解法只需要使用常数大小的额外空间,因此空间复杂度是 O(1)。
综上所述,使用模拟 Z 字形排列的方法来将一个字符串按照指定的行数进行 Z 字形变换是最优解法,具有高效的时间复杂度和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 将一个字符串按照指定的行数进行 Z 字形排列后,逐行读取得到新的字符串。
* 例如,对于字符串 "PAYPALISHIRING" 和行数 3,按照 Z 字形排列得到:
* P A H N
* A P L S I I G
* Y I R
* 逐行读取得到新的字符串为 "PAHNAPLSIIGYIR"。
* @date 2021/3/9 23:28
*/
public class ZigzagConversion {
/**
* 最优解法是使用模拟 Z 字形排列的方法来将一个字符串按照指定的行数进行 Z 字形变换。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 创建一个长度为行数的字符串数组,用于保存每一行的字符。
* * 定义一个变量 curRow,表示当前行数,初始值为 0,表示从第一行开始。
* * 定义一个变量 goingDown,表示当前方向,初始值为 false,表示当前方向是向上。
* * 遍历原始字符串的每个字符,按照 Z 字形排列的顺序将字符放入对应的行,并根据当前方向的变化更新 curRow 和 goingDown。
* * 最后,将每一行的字符按顺序拼接成新的字符串。
* 这种解法的核心思想是模拟 Z 字形排列的过程,按顺序将字符放入对应的行,并根据当前方向的变化更新行数 curRow 和方向 goingDown。
* 由于这个解法只需要遍历一次原始字符串,并且在遍历过程中只进行简单的判断和字符放入操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
* 另外,除了存储原始字符串和结果字符串外,这个解法只需要使用常数大小的额外空间,因此空间复杂度是 O(1)。
* 综上所述,使用模拟 Z 字形排列的方法来将一个字符串按照指定的行数进行 Z 字形变换是最优解法,具有高效的时间复杂度和节省空间的特点。
*/
public static String convert(String s, int numRows) {
// 特殊情况处理
if (numRows == 1) {
return s;
}
// 创建长度为 numRows 的字符串数组,用于保存每一行的字符
StringBuilder[] rows = new StringBuilder[numRows];
for (int i = 0; i < numRows; i++) {
rows[i] = new StringBuilder();
}
// 当前行数
int curRow = 0;
// 当前方向
boolean goingDown = false;
// 遍历原始字符串的每个字符
for (char c : s.toCharArray()) {
// 将当前字符放入对应的行
rows[curRow].append(c);
// 更新当前行数和方向
if (curRow == 0 || curRow == numRows - 1) {
// 到达第一行或最后一行时改变方向
goingDown = !goingDown;
}
// 根据当前方向更新当前行数
curRow += goingDown ? 1 : -1;
}
// 将每一行的字符按顺序拼接成新的字符串
StringBuilder result = new StringBuilder();
for (StringBuilder row : rows) {
result.append(row);
}
return result.toString();
}
public static void main(String[] args) {
String s = "PAYPALISHIRING";
int numRows = 3;
String converted = convert(s, numRows);
// 输出结果:Zigzag Conversion: PAHNAPLSIIGYIR
System.out.println("Zigzag Conversion: " + converted);
}
}
24、字符串的统计重复字符
题目描述:找到一个字符串中重复出现次数最多的字符及其出现次数。
例如,对于字符串 "abcaabbcdd",重复出现次数最多的字符是 'a' 和 'b',它们分别出现了 3 次。
解题思路
最优解法是使用哈希表来统计字符串中每个字符出现的次数,然后找到出现次数最多的字符及其出现次数。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 创建一个哈希表 charCount,用于记录每个字符出现的次数。
- 遍历字符串,对于每个字符,将其在哈希表中对应的计数值加一。
- 遍历哈希表,找到出现次数最多的字符及其出现次数。
这种解法的核心思想是利用哈希表记录每个字符出现的次数,然后找到出现次数最多的字符及其出现次数。
由于这个解法只需要遍历一次原始字符串,并且在遍历过程中只进行简单的哈希表操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
另外,由于哈希表中存储的键值对数量与字符集大小有关,但在本问题中字符集是有限的,所以哈希表的空间复杂度是 O(1)。
综上所述,使用哈希表来统计字符串中每个字符出现的次数,然后找到出现次数最多的字符及其出现次数是最优解法,具有高效的时间复杂度和固定的空间复杂度。
具体代码展示
package org.zyf.javabasic.letcode.string;
import java.util.HashMap;
import java.util.Map;
/**
* @author yanfengzhang
* @description 找到一个字符串中重复出现次数最多的字符及其出现次数。
* 例如,对于字符串 "abcaabbcdd",重复出现次数最多的字符是 'a' 和 'b',它们分别出现了 3 次。
* @date 2021/3/21 23:31
*/
public class MostRepeatedCharacter {
/**
* 最优解法是使用哈希表来统计字符串中每个字符出现的次数,然后找到出现次数最多的字符及其出现次数。
* 这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 创建一个哈希表 charCount,用于记录每个字符出现的次数。
* * 遍历字符串,对于每个字符,将其在哈希表中对应的计数值加一。
* * 遍历哈希表,找到出现次数最多的字符及其出现次数。
* 这种解法的核心思想是利用哈希表记录每个字符出现的次数,然后找到出现次数最多的字符及其出现次数。
* 由于这个解法只需要遍历一次原始字符串,并且在遍历过程中只进行简单的哈希表操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
* 另外,由于哈希表中存储的键值对数量与字符集大小有关,但在本问题中字符集是有限的,所以哈希表的空间复杂度是 O(1)。
* 综上所述,使用哈希表来统计字符串中每个字符出现的次数,然后找到出现次数最多的字符及其出现次数是最优解法,具有高效的时间复杂度和固定的空间复杂度。
*/
public static void findMostRepeatedCharacter(String s) {
// 创建一个哈希表 charCount,用于记录每个字符出现的次数
Map<Character, Integer> charCount = new HashMap<>();
// 遍历字符串,对于每个字符,将其在哈希表中对应的计数值加一
for (char c : s.toCharArray()) {
charCount.put(c, charCount.getOrDefault(c, 0) + 1);
}
// 初始化重复出现次数最多的字符和出现次数
char mostRepeatedChar = '\0';
int maxCount = 0;
// 遍历哈希表,找到出现次数最多的字符及其出现次数
for (Map.Entry<Character, Integer> entry : charCount.entrySet()) {
char c = entry.getKey();
int count = entry.getValue();
if (count > maxCount) {
mostRepeatedChar = c;
maxCount = count;
}
}
// 输出结果
System.out.println("Most repeated character: " + mostRepeatedChar);
System.out.println("Occurrences: " + maxCount);
}
public static void main(String[] args) {
String s = "abcaabbcdd";
// 输出结果:Most repeated character: a, Occurrences: 3
findMostRepeatedCharacter(s);
}
}
25、字符串中的最长连续数字子串
题目描述:找到一个字符串中最长的连续数字字符的子串。
例如,对于字符串 "ab123cd456ef789",最长的连续数字字符子串是 "789",它的长度是 3。
解题思路
最优解法是使用双指针的方法来找到一个字符串中最长的连续数字字符子串。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 定义两个指针 start 和 end,分别表示当前连续数字字符子串的起始位置和结束位置。
- 定义一个变量 maxLength,用于记录最长的连续数字字符子串的长度。
- 遍历字符串,当遇到数字字符时,将 end 指针向后移动,直到不是数字字符为止,记录当前连续数字字符子串的长度。
- 更新 maxLength 和对应的起始位置 start,以保持记录最长的连续数字字符子串的信息。
- 继续遍历字符串直到结束。
这种解法的核心思想是利用双指针来定位每个连续数字字符子串的起始位置和结束位置,然后计算其长度,并更新最长的连续数字字符子串的信息。
由于这个解法只需要遍历一次字符串,并且在遍历过程中只进行简单的双指针操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
另外,除了存储原始字符串和记录最长子串的信息外,这个解法只需要使用常数大小的额外空间,因此空间复杂度是 O(1)。
综上所述,使用双指针的方法来找到一个字符串中最长的连续数字字符子串是最优解法,具有高效的时间复杂度和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 找到一个字符串中最长的连续数字字符的子串。
* 例如,对于字符串 "ab123cd456ef789",最长的连续数字字符子串是 "789",它的长度是 3。
* @date 2021/3/15 23:35
*/
public class LongestConsecutiveDigitsSubstring {
/**
* 最优解法是使用双指针的方法来找到一个字符串中最长的连续数字字符子串。这种解法的时间复杂度是 O(n),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 定义两个指针 start 和 end,分别表示当前连续数字字符子串的起始位置和结束位置。
* * 定义一个变量 maxLength,用于记录最长的连续数字字符子串的长度。
* * 遍历字符串,当遇到数字字符时,将 end 指针向后移动,直到不是数字字符为止,记录当前连续数字字符子串的长度。
* * 更新 maxLength 和对应的起始位置 start,以保持记录最长的连续数字字符子串的信息。
* * 继续遍历字符串直到结束。
* 这种解法的核心思想是利用双指针来定位每个连续数字字符子串的起始位置和结束位置,然后计算其长度,并更新最长的连续数字字符子串的信息。
* 由于这个解法只需要遍历一次字符串,并且在遍历过程中只进行简单的双指针操作,因此时间复杂度是 O(n),其中 n 是字符串的长度。
* 另外,除了存储原始字符串和记录最长子串的信息外,这个解法只需要使用常数大小的额外空间,因此空间复杂度是 O(1)。
* 综上所述,使用双指针的方法来找到一个字符串中最长的连续数字字符子串是最优解法,具有高效的时间复杂度和节省空间的特点。
*/
public static String findLongestConsecutiveDigitsSubstring(String s) {
// 定义两个指针 start 和 end,分别表示当前连续数字字符子串的起始位置和结束位置
int start = 0;
int end = 0;
// 定义变量 maxLength,记录最长的连续数字字符子串的长度
int maxLength = 0;
// 遍历字符串,用指针 end 定位每个连续数字字符子串的结束位置
while (end < s.length()) {
// 如果当前字符是数字字符,指针 end 向后移动,直到不是数字字符为止
while (end < s.length() && Character.isDigit(s.charAt(end))) {
end++;
}
// 计算当前连续数字字符子串的长度
int currentLength = end - start;
// 更新最长的连续数字字符子串的信息
if (currentLength > maxLength) {
maxLength = currentLength;
}
// 将指针 start 更新为下一个连续数字字符子串的起始位置
start = end + 1;
// 将指针 end 移动到下一个字符的位置
end++;
}
// 根据最长的连续数字字符子串的长度和起始位置,得到结果子串
String result = s.substring(start - 1, start + maxLength - 1);
return result;
}
public static void main(String[] args) {
String s = "ab123cd456ef789";
String longestSubstring = findLongestConsecutiveDigitsSubstring(s);
// 输出结果:Longest consecutive digits substring: 789
System.out.println("Longest consecutive digits substring: " + longestSubstring);
}
}
26、字符串的最长回文前缀
题目描述:找到一个字符串的最长回文前缀。回文前缀是指从字符串的开头开始,能够形成回文字符串的最长子串。
例如,对于字符串 "abacdc",它的最长回文前缀是 "aba"。
解题思路
最优解法是使用中心扩展法来找到一个字符串的最长回文前缀。这种解法的时间复杂度是 O(n^2),其中 n 是字符串的长度。
具体的最优解法步骤如下:
- 定义一个变量 maxPrefix,用于记录最长的回文前缀。
- 遍历字符串的每个字符,以当前字符为中心,向两边扩展检查是否是回文子串。具体扩展方式包括回文子串长度是奇数或偶数的两种情况。
- 更新 maxPrefix 为找到的最长回文前缀。
这种解法的核心思想是从字符串的每个字符开始,以该字符为中心,向两边扩展检查是否形成回文子串。由于回文子串的长度可能是奇数或偶数,因此需要分别考虑两种情况。
由于这个解法需要遍历字符串的每个字符,并在每个字符处检查回文子串的扩展,因此时间复杂度是 O(n^2),其中 n 是字符串的长度。
另外,除了存储原始字符串和记录最长回文前缀外,这个解法只需要使用常数大小的额外空间,因此空间复杂度是 O(1)。
综上所述,使用中心扩展法来找到一个字符串的最长回文前缀是最优解法,具有高效的时间复杂度和节省空间的特点。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 找到一个字符串的最长回文前缀。
* 回文前缀是指从字符串的开头开始,能够形成回文字符串的最长子串。
* 例如,对于字符串 "abacdc",它的最长回文前缀是 "aba"。
* @date 2021/3/18 23:43
*/
public class LongestPalindromePrefix {
/**
* 最优解法是使用中心扩展法来找到一个字符串的最长回文前缀。这种解法的时间复杂度是 O(n^2),其中 n 是字符串的长度。
* 具体的最优解法步骤如下:
* * 定义一个变量 maxPrefix,用于记录最长的回文前缀。
* * 遍历字符串的每个字符,以当前字符为中心,向两边扩展检查是否是回文子串。具体扩展方式包括回文子串长度是奇数或偶数的两种情况。
* * 更新 maxPrefix 为找到的最长回文前缀。
* 这种解法的核心思想是从字符串的每个字符开始,以该字符为中心,向两边扩展检查是否形成回文子串。由于回文子串的长度可能是奇数或偶数,因此需要分别考虑两种情况。
* 由于这个解法需要遍历字符串的每个字符,并在每个字符处检查回文子串的扩展,因此时间复杂度是 O(n^2),其中 n 是字符串的长度。
* 另外,除了存储原始字符串和记录最长回文前缀外,这个解法只需要使用常数大小的额外空间,因此空间复杂度是 O(1)。
* 综上所述,使用中心扩展法来找到一个字符串的最长回文前缀是最优解法,具有高效的时间复杂度和节省空间的特点。
*/
public static String findLongestPalindromePrefix(String s) {
if (s == null || s.length() == 0) {
return "";
}
// 定义一个变量 maxPrefix,用于记录最长的回文前缀
String maxPrefix = "";
// 遍历字符串的每个字符,以当前字符为中心,向两边扩展检查是否是回文子串
for (int i = 0; i < s.length(); i++) {
// 回文子串长度是奇数的情况
String oddLengthPalindrome = expandPalindrome(s, i, i);
// 回文子串长度是偶数的情况
String evenLengthPalindrome = expandPalindrome(s, i, i + 1);
// 更新 maxPrefix 为找到的最长回文前缀
if (oddLengthPalindrome.length() > maxPrefix.length()) {
maxPrefix = oddLengthPalindrome;
}
if (evenLengthPalindrome.length() > maxPrefix.length()) {
maxPrefix = evenLengthPalindrome;
}
}
return maxPrefix;
}
/**
* 从给定的中心位置向两边扩展,找到最长的回文子串
*/
private static String expandPalindrome(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
// 返回找到的回文子串
return s.substring(left + 1, right);
}
public static void main(String[] args) {
String s = "abacdc";
String longestPalindromePrefix = findLongestPalindromePrefix(s);
// 输出结果:Longest palindrome prefix: aba
System.out.println("Longest palindrome prefix: " + longestPalindromePrefix);
}
}
27、字符串相乘
题目描述:实现两个大整数字符串的相乘操作,要求高效且正确处理溢出。
解题思路
最优解法是使用竖式乘法的思想来实现字符串相乘,这种解法的时间复杂度是 O(n*m),其中 n 和 m 分别是两个大整数字符串的长度。
具体的最优解法步骤如下:
- 创建一个数组 result 来保存相乘的结果,数组的长度为两个大整数字符串的长度之和。
- 从两个大整数字符串的最低位(个位)开始遍历,分别与另一个大整数字符串中的每一位相乘,并将结果存储在 result 数组中的对应位置。
- 处理进位,将 result 数组中的每一位都保持在 0-9 的范围内,并考虑进位的情况。
- 最后将 result 数组转换为字符串,注意去除前导零。
这种解法的核心思想是模拟手工乘法的过程,将两个大整数字符串的每一位相乘,然后累加得到最终结果。由于乘法的结果可能是多位数,因此需要考虑进位的情况。
由于这个解法只需要遍历两个大整数字符串一次,并进行一次进位处理,因此时间复杂度是 O(n*m),其中 n 和 m 分别是两个大整数字符串的长度。
另外,由于 result 数组的长度是两个大整数字符串长度之和,所以空间复杂度也是 O(n+m)。
综上所述,使用竖式乘法的思想来实现字符串相乘是最优解法,具有高效的时间复杂度和固定的空间复杂度。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 实现两个大整数字符串的相乘操作,要求高效且正确处理溢出。
* @date 2021/3/23 23:47
*/
public class StringMultiplication {
/**
* 最优解法是使用竖式乘法的思想来实现字符串相乘,这种解法的时间复杂度是 O(n*m),其中 n 和 m 分别是两个大整数字符串的长度。
* 具体的最优解法步骤如下:
* * 创建一个数组 result 来保存相乘的结果,数组的长度为两个大整数字符串的长度之和。
* * 从两个大整数字符串的最低位(个位)开始遍历,分别与另一个大整数字符串中的每一位相乘,并将结果存储在 result 数组中的对应位置。
* * 处理进位,将 result 数组中的每一位都保持在 0-9 的范围内,并考虑进位的情况。
* * 最后将 result 数组转换为字符串,注意去除前导零。
* 这种解法的核心思想是模拟手工乘法的过程,将两个大整数字符串的每一位相乘,然后累加得到最终结果。由于乘法的结果可能是多位数,因此需要考虑进位的情况。
* 由于这个解法只需要遍历两个大整数字符串一次,并进行一次进位处理,因此时间复杂度是 O(n*m),其中 n 和 m 分别是两个大整数字符串的长度。
* 另外,由于 result 数组的长度是两个大整数字符串长度之和,所以空间复杂度也是 O(n+m)。
* 综上所述,使用竖式乘法的思想来实现字符串相乘是最优解法,具有高效的时间复杂度和固定的空间复杂度。
*/
public static String multiply(String num1, String num2) {
int m = num1.length();
int n = num2.length();
int[] result = new int[m + n];
// 从个位开始遍历 num1 和 num2,实现竖式乘法的思想
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
int product = (num1.charAt(i) - '0') * (num2.charAt(j) - '0');
// 乘积的高位
int p1 = i + j;
// 乘积的低位
int p2 = i + j + 1;
// 把乘积累加到 result 中的对应位置
int sum = product + result[p2];
// 处理进位
result[p1] += sum / 10;
result[p2] = sum % 10;
}
}
// 转换 result 数组为字符串
StringBuilder sb = new StringBuilder();
for (int digit : result) {
if (sb.length() > 0 || digit != 0) {
sb.append(digit);
}
}
// 处理结果为 "0" 的特殊情况
if (sb.length() == 0) {
return "0";
}
return sb.toString();
}
public static void main(String[] args) {
String num1 = "123";
String num2 = "456";
String product = multiply(num1, num2);
// 输出结果:123 * 456 = 56088
System.out.println(num1 + " * " + num2 + " = " + product);
}
}
28、Roman to Integer(罗马数字转整数)
具体可见数学思维编程练习总结_面具虚拟机_张彦峰ZYF的博客-CSDN博客中的第8题。
29、数字转换为英文单词
将给定的非负整数转换为对应的英文单词表示,例如 12345 转换为 "Twelve Thousand Three Hundred Forty-Five"。
解题思路
最优解法是使用递归的方式来将给定的非负整数转换为对应的英文单词表示。这种解法的时间复杂度是 O(log10(n)),其中 n 是给定的非负整数。
具体的最优解法步骤如下:
- 定义两个辅助数组 ones 和 tens,分别用于表示 1 到 9 和 10 到 90 的英文单词。
- 实现一个递归函数 convertToWords,用于将给定的非负整数转换为英文单词。
- 在递归函数中,将整数按照每三位进行分组,例如 12345 分组为 12 和 345。
- 处理每个分组,分别转换为对应的英文单词,并加上对应的单位(例如 "Thousand"、"Million" 等)。
- 将所有处理后的分组拼接起来得到最终结果。
这种解法的核心思想是利用递归函数处理整数的每个分组,然后逐层返回处理后的结果,最终得到整个数字转换为英文单词的表示。
由于这个解法是通过递归函数来处理每个分组,并且每次处理后的数字都会减小一半,因此时间复杂度是 O(log10(n)),其中 n 是给定的非负整数。
另外,除了存储辅助数组和递归函数的栈空间外,这个解法不需要使用额外的空间,所以空间复杂度是 O(1)。
综上所述,使用递归的方式来将给定的非负整数转换为对应的英文单词表示是最优解法,具有高效的时间复杂度和固定的空间复杂度。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 将给定的非负整数转换为对应的英文单词表示,例如 12345 转换为 "Twelve Thousand Three Hundred Forty-Five"。
* @date 2021/3/25 23:50
*/
public class IntegerToEnglishWords {
private static final String[] ones = {
"", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"
};
private static final String[] teens = {
"Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"
};
private static final String[] tens = {
"", "Ten", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"
};
private static final String[] thousands = {
"", "Thousand", "Million", "Billion"
};
/**
* 最优解法是使用递归的方式来将给定的非负整数转换为对应的英文单词表示。这种解法的时间复杂度是 O(log10(n)),其中 n 是给定的非负整数。
* 具体的最优解法步骤如下:
* * 定义两个辅助数组 ones 和 tens,分别用于表示 1 到 9 和 10 到 90 的英文单词。
* * 实现一个递归函数 convertToWords,用于将给定的非负整数转换为英文单词。
* * 在递归函数中,将整数按照每三位进行分组,例如 12345 分组为 12 和 345。
* * 处理每个分组,分别转换为对应的英文单词,并加上对应的单位(例如 "Thousand"、"Million" 等)。
* * 将所有处理后的分组拼接起来得到最终结果。
* 这种解法的核心思想是利用递归函数处理整数的每个分组,然后逐层返回处理后的结果,最终得到整个数字转换为英文单词的表示。
* 由于这个解法是通过递归函数来处理每个分组,并且每次处理后的数字都会减小一半,因此时间复杂度是 O(log10(n)),其中 n 是给定的非负整数。
* 另外,除了存储辅助数组和递归函数的栈空间外,这个解法不需要使用额外的空间,所以空间复杂度是 O(1)。
* 综上所述,使用递归的方式来将给定的非负整数转换为对应的英文单词表示是最优解法,具有高效的时间复杂度和固定的空间复杂度。
*/
public static String numberToWords(int num) {
if (num == 0) {
return "Zero";
}
StringBuilder words = new StringBuilder();
// 用于指示当前处理的分组的单位索引
int index = 0;
while (num > 0) {
if (num % 1000 != 0) {
// 处理当前分组的数字
StringBuilder groupWords = new StringBuilder();
convertToWords(groupWords, num % 1000);
words.insert(0, groupWords.append(thousands[index]).append(" "));
}
// 继续处理下一个分组
num /= 1000;
index++;
}
return words.toString().trim();
}
private static void convertToWords(StringBuilder words, int num) {
if (num == 0) {
return;
} else if (num < 10) {
words.append(ones[num]).append(" ");
} else if (num < 20) {
words.append(teens[num - 10]).append(" ");
} else if (num < 100) {
words.append(tens[num / 10]).append(" ");
convertToWords(words, num % 10);
} else {
words.append(ones[num / 100]).append(" Hundred ");
convertToWords(words, num % 100);
}
}
public static void main(String[] args) {
int num1 = 12345;
int num2 = 56789123;
String words1 = numberToWords(num1);
String words2 = numberToWords(num2);
// 输出结果:12345 -> Twelve Thousand Three Hundred Forty Five
System.out.println(num1 + " -> " + words1);
// 输出结果:56789123 -> Fifty Six Million Seven Hundred Eighty Nine Thousand One Hundred Twenty Three
System.out.println(num2 + " -> " + words2);
}
}
30、中文数字表达转实际数字格式
具体见剑指offer所有编程练习总结分析_张彦峰ZYF的博客-CSDN博客中的第82题。
31、千位分隔符
题目描述:给定一个整数,将其转换为字符串形式并添加千位分隔符。
解题思路
最优解法是通过将整数转换为字符串形式,并从字符串末尾开始每隔三位添加一个千位分隔符(逗号)来实现。这种解法的时间复杂度是 O(log10(n)),其中 n 是给定的整数。
具体的最优解法步骤如下:
- 将给定的整数转换为字符串形式。
- 创建一个 StringBuilder 来保存结果字符串。
- 从字符串的末尾开始遍历,每隔三位添加一个逗号,并将字符拼接到 StringBuilder 中。
- 如果整数长度不是 3 的倍数,需要额外处理剩余的位数。
这种解法的核心思想是将整数转换为字符串形式,然后从字符串末尾开始遍历并添加千位分隔符。
由于这个解法是通过字符串遍历来实现的,并且每次处理只需要常数时间,因此时间复杂度是 O(log10(n)),其中 n 是给定的整数。
另外,由于使用了 StringBuilder 来保存结果字符串,所以空间复杂度是 O(log10(n)),同样取决于整数的位数。
综上所述,通过将整数转换为字符串并从末尾开始添加千位分隔符是最优解法,具有高效的时间复杂度和固定的空间复杂度。
具体代码展示
package org.zyf.javabasic.letcode.string;
/**
* @author yanfengzhang
* @description 给定一个整数,将其转换为字符串形式并添加千位分隔符。
* @date 2021/3/26 22:53
*/
public class ThousandSeparator {
/**
* 最优解法是通过将整数转换为字符串形式,并从字符串末尾开始每隔三位添加一个千位分隔符(逗号)来实现。这种解法的时间复杂度是 O(log10(n)),其中 n 是给定的整数。
* 具体的最优解法步骤如下:
* * 将给定的整数转换为字符串形式。
* * 创建一个 StringBuilder 来保存结果字符串。
* * 从字符串的末尾开始遍历,每隔三位添加一个逗号,并将字符拼接到 StringBuilder 中。
* * 如果整数长度不是 3 的倍数,需要额外处理剩余的位数。
* 这种解法的核心思想是将整数转换为字符串形式,然后从字符串末尾开始遍历并添加千位分隔符。
* 由于这个解法是通过字符串遍历来实现的,并且每次处理只需要常数时间,因此时间复杂度是 O(log10(n)),其中 n 是给定的整数。
* 另外,由于使用了 StringBuilder 来保存结果字符串,所以空间复杂度是 O(log10(n)),同样取决于整数的位数。
* 综上所述,通过将整数转换为字符串并从末尾开始添加千位分隔符是最优解法,具有高效的时间复杂度和固定的空间复杂度。
*/
public static String addThousandSeparator(int num) {
if (num == 0) {
return "0";
}
// 将整数转换为字符串形式
String numStr = String.valueOf(num);
StringBuilder result = new StringBuilder();
int count = 0;
// 从字符串的末尾开始遍历
for (int i = numStr.length() - 1; i >= 0; i--) {
// 将字符插入到 result 的开头
result.insert(0, numStr.charAt(i));
count++;
// 每隔三位添加一个千位分隔符
if (count % 3 == 0 && i > 0) {
result.insert(0, ',');
}
}
return result.toString();
}
public static void main(String[] args) {
int num1 = 1234567;
int num2 = 1234567890;
String formattedNum1 = addThousandSeparator(num1);
String formattedNum2 = addThousandSeparator(num2);
// 输出结果:1234567 -> 1,234,567
System.out.println(num1 + " -> " + formattedNum1);
// 输出结果:1234567890 -> 1,234,567,890
System.out.println(num2 + " -> " + formattedNum2);
}
}