【Day 8-9】 -【代码随想录训练营20期】打卡
字符串的操作是十分灵活的,和数组还不一样,包括了很多字符串操作的函数和语法。
字符串的基本操作语法
1. 字符串的创建与初始化:
String str1 = "Hello, World!"; // 使用双引号创建字符串
String str2 = new String("Hello"); // 使用构造函数创建字符串对象
2. 字符串连接:
String result = str1 + " " + str2; // 字符串连接使用加号操作符
3. 获取字符串长度:
int length = str1.length(); // 获取字符串的长度
4. 字符串比较:
boolean isEqual = str1.equals(str2); // 比较两个字符串是否相等
boolean isEqualIgnoreCase = str1.equalsIgnoreCase(str2); // 忽略大小写比较
int compareResult = str1.compareTo(str2); // 按字典顺序比较字符串
5. 字符串查找与替换:
int index = str1.indexOf("o"); // 查找字符或子串的位置
int lastIndex = str1.lastIndexOf("o"); // 从后往前查找字符或子串的位置
String replaced = str1.replace("o", "e"); // 替换字符或子串
6. 字符串切割:
String[] parts = str1.split(", "); // 切割字符串得到字符串数组
7. 字符串截取:
String substring = str1.substring(7); // 截取从索引7到结尾的子串
String substring2 = str1.substring(7, 12); // 截取索引7到索引11的子串
8. 字符串转换:
int num = Integer.parseInt("123"); // 字符串转换为整数
double decimal = Double.parseDouble("3.14"); // 字符串转换为双精度浮点数
String strFromNum = String.valueOf(42); // 基本类型转换为字符串
9. 去除首尾空格:
String trimmed = str1.trim(); // 去除首尾空格
10. 大小写转换:
String upperCase = str1.toUpperCase(); // 转换为大写
String lowerCase = str1.toLowerCase(); // 转换为小写
KMP算法
KMP算法有啥用?
在应用字符串匹配时,避免每次都冲头开始匹配,造成资源的浪费。
在字符串匹配中,文本串和模式串是什么东西?
文本串是需要查找的主串,模式串就是查看有没有的子串。
前缀表是什么东西?
它是只针对模式串的,记录了在模式串的当前位置匹配失败时,应该回退到哪个位置。
什么是前缀,什么是后缀呢?
注意,这属于前缀表的范围,因此这里讨论的只有模式串。
前缀指得是包含第一个字符,但不包含最后一个字符的连续子串。
后缀则相反,包含最后但不包含第一个。
最长公共前后缀是什么?有什么用?
其实是最长相等前后缀,指的是前缀与后缀相等,且最长的那一部分子串。
最长公共前后缀就是前缀表上填的数。
如何计算前缀表呢?
下标i对应的前缀表数值,指的就是该下标到0的那个子串的最长相等前后缀。
i = 0 , 前缀和后缀分别为 null null,因此最长相等前后缀就是0。
i = 1,a a 1
i = 2,aa ba 0
i = 3,aab aba 1
.....
next数组又是什么东西?
其实就是前缀表,一般就将它所有元素都减去1得到的,具体问题还有不同的操作。其目的是更方便具体的实现。
KMP算法的时间复杂度是多少呢?
O(n+m),其中n为文本串长度,m为模式串长度。时间复杂度其实是由匹配+next数组生成两者相加得到的。
那么如何构造next数组呢?
这个属于代码实现范畴了,主要有三步:初始化、分别处理前后缀不同的情况、相同的情况。
1. 初始化:定义 i 和 j ,分别表示后缀末尾和最大相等前缀末尾。而next[i]就表示之前最长相等前后缀长度,也就是 j 。
int j = -1;
next[0] = j;
2. 处理前后缀不同以及相同的情况
for (int i = 1; i < s.length(); i++){ //这里的i从1开始
while(j >= 0 && s.charAt(i) != s.charAt(j+1)){ //前后缀不相同
j=next[j]; //向前回退
}
if(s.charAt(i) == s.charAt(j+1)){ //找到相同的前后缀
j++;
}
next[i] = j; //将j(前缀的长度)赋给next[i]
}
回退的目的就是为了找到上一个最大相等前缀的末尾,避免重复运算,如果不行,就一直回退到最开始的位置。
如何使用Next数组来做匹配呢?
public int strStr(String haystack, String needle) {
//模式串大于主串直接返回0
if(needle.length()==0){
return 0;
}
//获取next数组
int[] next = new int[needle.length()];
getNext(next, needle);
//初始化j
int j = -1;
for(int i = 0; i < haystack.length(); i++){
//当不匹配时,寻找之前匹配的位置
while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
j = next[j];
}
//匹配时,j和i同时向后移动
if(haystack.charAt(i) == needle.charAt(j+1)){
j++;//i的增加在for里边
}
//文本串里出现了模式串
if(j == needle.length()-1){
return (i-needle.length()+1);
}
}
return -1;
}
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:s = ["h","e","l","l","o"] 输出:["o","l","l","e","h"]
思路:用一个临时字符作为中转,然后交换左右指针。
class Solution {
public void reverseString(char[] s) {
int leftIndex = 0;
int rightIndex = s.length - 1;
char temp;
while(leftIndex < rightIndex){
temp = s[leftIndex];
s[leftIndex] = s[rightIndex];
s[rightIndex] = temp;
++leftIndex;
--rightIndex;
}
}
}
541. 反转字符串 II
给定一个字符串
s
和一个整数k
,从字符串开头算起,每计数至2k
个字符,就反转这2k
字符中的前k
个字符。
- 如果剩余字符少于
k
个,则将剩余字符全部反转。- 如果剩余字符小于
2k
但大于或等于k
个,则反转前k
个字符,其余字符保持原样。
示例 1:
输入:s = "abcdefg", k = 2 输出:"bacdfeg"
思路:分情况讨论并模拟
class Solution {
public String reverseStr(String s, int k) {
char[] arr = s.toCharArray();
int leftIndex, rightIndex;
int findIndex = k;
char temp;
while(findIndex <= arr.length){
leftIndex = findIndex - k;
rightIndex = findIndex - 1;
while(leftIndex < rightIndex){
temp = arr[leftIndex];
arr[leftIndex] = arr[rightIndex];
arr[rightIndex] = temp;
leftIndex++;
rightIndex--;
}
findIndex = findIndex + (2 * k);
}
if(findIndex > arr.length){
leftIndex = findIndex - k;
rightIndex = arr.length - 1;
while(leftIndex < rightIndex){
temp = arr[leftIndex];
arr[leftIndex] = arr[rightIndex];
arr[rightIndex] = temp;
leftIndex++;
rightIndex--;
}
}
String str = new String(arr);
return str;
}
}
剑指 Offer 05. 替换空格
请实现一个函数,把字符串
s
中的每个空格替换成"%20"。
思路:这种就是无脑往前推,那就应该用StringBuffer
class Solution{
public static String replaceSpace(String s) {
if (s == null) {
return null;
}
//选用 StringBuilder 单线程使用,比较快,选不选都行
StringBuilder sb = new StringBuilder();
//使用 sb 逐个复制 s ,碰到空格则替换,否则直接复制
for (int i = 0; i < s.length(); i++) {
//s.charAt(i) 为 char 类型,为了比较需要将其转为和 " " 相同的字符串类型
//if (" ".equals(String.valueOf(s.charAt(i)))){}
if (s.charAt(i) == ' ') {
sb.append("%20");
} else {
sb.append(s.charAt(i));
}
}
return sb.toString();
}
}
151. 反转字符串中的单词
给你一个字符串
s
,请你反转字符串中 单词 的顺序。单词 是由非空格字符组成的字符串。
s
中使用至少一个空格将字符串中的 单词 分隔开。返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串
s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
思路:这是一道综合题,可以拆分为消除字符串的连续多余空格、字符串反转、单词反转这三个操作。
class Solution {
/**
* 不使用Java内置方法实现
* <p>
* 1.去除首尾以及中间多余空格
* 2.反转整个字符串
* 3.反转各个单词
*/
public String reverseWords(String s) {
// System.out.println("ReverseWords.reverseWords2() called with: s = [" + s + "]");
// 1.去除首尾以及中间多余空格
StringBuilder sb = removeSpace(s);
// 2.反转整个字符串
reverseString(sb, 0, sb.length() - 1);
// 3.反转各个单词
reverseEachWord(sb);
return sb.toString();
}
private StringBuilder removeSpace(String s) {
// System.out.println("ReverseWords.removeSpace() called with: s = [" + s + "]");
int start = 0;
int end = s.length() - 1;
while (s.charAt(start) == ' ') start++;
while (s.charAt(end) == ' ') end--;
StringBuilder sb = new StringBuilder();
while (start <= end) {
char c = s.charAt(start);
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
start++;
}
// System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]");
return sb;
}
/**
* 反转字符串指定区间[start, end]的字符
*/
public void reverseString(StringBuilder sb, int start, int end) {
// System.out.println("ReverseWords.reverseString() called with: sb = [" + sb + "], start = [" + start + "], end = [" + end + "]");
while (start < end) {
char temp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, temp);
start++;
end--;
}
// System.out.println("ReverseWords.reverseString returned: sb = [" + sb + "]");
}
private void reverseEachWord(StringBuilder sb) {
int start = 0;
int end = 1;
int n = sb.length();
while (start < n) {
while (end < n && sb.charAt(end) != ' ') {
end++;
}
reverseString(sb, start, end - 1);
start = end + 1;
end = start + 1;
}
}
}
//解法二:创建新字符数组填充。时间复杂度O(n)
class Solution {
public String reverseWords(String s) {
//源字符数组
char[] initialArr = s.toCharArray();
//新字符数组
char[] newArr = new char[initialArr.length+1];//下面循环添加"单词 ",最终末尾的空格不会返回
int newArrPos = 0;
//i来进行整体对源字符数组从后往前遍历
int i = initialArr.length-1;
while(i>=0){
while(i>=0 && initialArr[i] == ' '){i--;} //跳过空格
//此时i位置是边界或!=空格,先记录当前索引,之后的while用来确定单词的首字母的位置
int right = i;
while(i>=0 && initialArr[i] != ' '){i--;}
//指定区间单词取出(由于i为首字母的前一位,所以这里+1,),取出的每组末尾都带有一个空格
for (int j = i+1; j <= right; j++) {
newArr[newArrPos++] = initialArr[j];
if(j == right){
newArr[newArrPos++] = ' ';//空格
}
}
}
//若是原始字符串没有单词,直接返回空字符串;若是有单词,返回0-末尾空格索引前范围的字符数组(转成String返回)
if(newArrPos == 0){
return "";
}else{
return new String(newArr,0,newArrPos-1);
}
}
}
剑指 Offer 58 - II. 左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
思路:这题简单,使用新数组储存也行,使用StringBuilder储存也行。
class Solution {
public String reverseLeftWords(String s, int n) {
//先转为char数组,然后申请一个新数组实现左旋
//不使用额外空间
char [] arr1 = s.toCharArray();
char [] arr2 = new char [s.length()];
int k = 0;
for(int i = n; i < arr1.length; i++){
arr2[k] = arr1[i];
k++;
}
for(int i = 0; i < n; i++){
arr2[k] = arr1[i];
k++;
}
String result = new String(arr2);
return result;
}
}
28. 找出字符串中第一个匹配项的下标
给你两个字符串
haystack
和needle
,请你在haystack
字符串中找出needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果needle
不是haystack
的一部分,则返回-1
。
思路:可以用双指针,也可以用KMP算法。双指针要注意firstIndex的返回位置,是在匹配字符串的第二个字符位置上。但这里存在一个问题,就是每次都返回到第二个字符位置从头匹配,那万一needle有前后重复的字符段,岂不是造成了时间的浪费?
双指针法
class Solution {
public int strStr(String haystack, String needle) {
//用双指针
int firstIndex = 0;
int secondIndex = 0;
//needle > haystack直接-1
if(needle.length() > haystack.length()){
return -1;
}
while(firstIndex < haystack.length()){
//如果相等,second指针进一
if(haystack.charAt(firstIndex) == needle.charAt(secondIndex)){
++secondIndex;
++firstIndex;
//如果second指针进到底,就返回对应first指针起始的值
if(secondIndex >= needle.length()){
return firstIndex - secondIndex;
}
}else{
//如果不相等,此时有两种情况
//1. second指针未移动
//2. second指针已移动,即前边有字符匹配,则将first指针返回到匹配的第二个字符的位置,重置second指针
if(secondIndex == 0){
++firstIndex;
}else{
firstIndex = firstIndex - secondIndex + 1;
secondIndex = 0;
}
}
}
return -1;
}
}
KMP算法
class Solution {
public void getNext(int[] next, String s){
int j = -1;
next[0] = j;
for (int i = 1; i < s.length(); i++){
while(j >= 0 && s.charAt(i) != s.charAt(j+1)){
j=next[j];
}
if(s.charAt(i) == s.charAt(j+1)){
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 = -1;
for(int i = 0; i < haystack.length(); i++){
while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){
j = next[j];
}
if(haystack.charAt(i) == needle.charAt(j+1)){
j++;
}
if(j == needle.length()-1){
return (i-needle.length()+1);
}
}
return -1;
}
}
459. 重复的子字符串
给定一个非空的字符串
s
,检查是否可以通过由它的一个子串重复多次构成。
思路:可以使用KMP算法,next数组中前面几个-1的元素长度如果能被数组长度整除,那么就是重复的子串。也可以使用数学规律,一个重复的字符串,其元素无论怎么滑动,必然会包含原来的重复子串。
KMP算法
class Solution {
public boolean repeatedSubstringPattern(String s) {
if (s.equals("")) return false;
int len = s.length();
// 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了
s = " " + s;
char[] chars = s.toCharArray();
int[] next = new int[len + 1];
// 构造 next 数组过程,j从0开始(空格),i从2开始
for (int i = 2, j = 0; i <= len; i++) {
// 匹配不成功,j回到前一位置 next 数组所对应的值
while (j > 0 && chars[i] != chars[j + 1]) j = next[j];
// 匹配成功,j往后移
if (chars[i] == chars[j + 1]) j++;
// 更新 next 数组的值
next[i] = j;
}
// 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值
if (next[len] > 0 && len % (len - next[len]) == 0) {
return true;
}
return false;
}
}
数学规律
lass Solution {
public boolean repeatedSubstringPattern(String s) {
String str = s + s;
return str.substring(1, str.length() - 1).contains(s);
}
}
substring是java的一个字符串方法,其目的是截取相应位置的字符串。