文章目录
一、重构字符串
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
提示:
1 <= s.length <= 105
s[i]
都是 ASCII 码表中的可打印字符
题解
解法1:单路交换
- 交换时注意,不能用变量,必须对数组直接操作
class Solution {
public void reverseString(char[] s) {
int n = s.length;
for(int i = 0; i < n / 2; i ++) { // 交换时注意,不能用变量,必须对数组直接操作
char t = s[i];
s[i] = s[n - 1 -i];
s[n - 1 - i] = t;
}
}
}
解法2:双指针法
class Solution {
public:
void reverseString(vector<char>& s) {
int n = s.size();
for(int i = 0, j = n - 1; i < n / 2; i++, j--) swap(s[i], s[j]);
}
};
541. 反转字符串II
给定一个字符串 s
和一个整数 k
,从字符串开头算起,每计数至 2k
个字符,就反转这 2k
字符中的前 k
个字符。
- 如果剩余字符少于
k
个,则将剩余字符全部反转。 - 如果剩余字符小于
2k
但大于或等于k
个,则反转前k
个字符,其余字符保持原样。
示例 1:
输入:s = "abcdefg", k = 2
输出:"bacdfeg"
示例 2:
输入:s = "abcd", k = 2
输出:"bacd"
提示:
1 <= s.length <= 104
s
仅由小写英文组成1 <= k <= 104
题解
- 在遍历字符串的过程中,只要让
i += (2 * k)
,i
每次移动2 * k
就可以了,- 然后判断是否需要有反转的区间
- 要找的也就是每
2 * k
区间的起点
class Solution {
public:
string reverseStr(string s, int k) {
int n = s.size();
// 1. 每隔2k个字符,进行处理
for(int i = 0; i < n; i += (2 * k)) {
// 2. 剩余字符大于等于k个,反转前k个字符:n-1-i+1>=k
if(n - i >= k) reverse(s, i, i + k - 1);//左闭右闭
// 3. 剩余字符小于k个,剩下字符全部反转
else reverse(s, i, s.size() -1);
}
return s;
}
private:
void reverse(string& s, int start, int end) {
for(int i = start, j = end; i < j; i++, j--) swap(s[i], s[j]);
}
};
剑指Offer 05.替换空格
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
限制:
0 <= s 的长度 <= 10000
题解
题解1:不使用额外空间
- 如果直接操作字符数组,不能直接用
%20
替换空格
,- 因为
%20
是三个字符,无法用三个字符替换一个字符 - 因此,需要扩容数组,这时候需要统计空格字符数量,然后扩容
resize
- 因为
- 为了能够不开新数组,使用双指针,从后往前处理,不污染原数组
- 双指针,每个指针分工不同
- 左指针,用来处理原数组元素
- 右指针,用来处理新数组元素
- 双指针,每个指针分工不同
class Solution {
public:
string replaceSpace(string s) {
// 1.统计空格字符数量
int cnt = 0;
int oldlen = s.size();
for(int i = 0; i < oldlen; i ++) {
if(s[i] == ' ') cnt++;
}
// 2.扩容
s.resize(oldlen + cnt * 2);
int newlen = s.size();
// 3.双指针处理,替换
int left = oldlen, right = newlen;
while(left < right && left >= 0) {//其实,一定在第一个空字符时左右指针追上,即,左指针不可能越界
if(s[left] != ' ') {
s[right--] = s[left--];
} else {
s[right--] = '0';s[right--] = '2';s[right--] = '%';
left--;
}
}
return s;
}
};
题解2:使用额外空间
C++
class Solution {
public:
string replaceSpace(string s) {
string s2 = "";
for(int i = 0; i < s.size(); i++) {
if(s[i] == ' ') s2 += "%20";
else s2 += s[i];
}
return s2;
}
};
java
class Solution {
public String replaceSpace(String s) {
StringBuilder s2 = new StringBuilder();
char[] ct = s.toCharArray();
for(char c : ct) {
if(c == ' ') s2.append("%20");
else s2.append(c);
}
return s2.toString();
}
}
151.翻转字符串里的单词
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
**注意:**输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例 1:
输入:s = "the sky is blue"
输出:"blue is sky the"
示例 2:
输入:s = " hello world "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = "a good example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
提示:
1 <= s.length <= 104
s
包含英文大小写字母、数字和空格' '
s
中 至少存在一个 单词
**进阶:**如果字符串在你使用的编程语言中是一种可变数据类型,请尝试使用 O(1)
额外空间复杂度的 原地 解法。
题解
方式1:直接用库函数,
- 将字符串分割得到字符数组,然后进行拼接
class Solution {
public String reverseWords(String s) {
s = s.trim();
StringBuilder s2 = new StringBuilder();
String[] st = s.split(" ");
for(int i = st.length - 1; i >= 0; i--) {
if(st[i] != "") { // 去除重复的空格字符,字符串中""空格
if(i != 0) s2.append(st[i] + " ");
else s2.append(st[i]);
}
}
return s2.toString();
}
}
方式2:不使用库函数
- 1.移除多余空格
- 可以使用双指针法,类似于移除元素的操作-慢指针填补移除后的元素
- 因为操作的是单个字符,要循环添加单个字符
- 对中间的空格,还要手动添加,达到移除多余和首位空格的效果
- 2.将整个字符串反转
- 3.将每个单词再次反转,即可
class Solution {
public:
string reverseWords(string s) {
// 1.移除多余空格
reverseEXSpace(s);
// 2.反转字符串
reverse(s, 0, s.size() -1);
// 3.每个单词再次进行反转-找到每个单词,进行反转
int start = 0; // 寻找单词左指针
for(int i = 0; i <= s.size(); i++) { // 最后一个单词的边界,即单词终止边界
if(i == s.size() || s[i] == ' ') { // 找到一个单词结束的右指针,越界或空格
reverse(s, start, i - 1);
start = i + 1; // 更新左指针,跳过空格
}
}
return s;
}
private:
void reverseEXSpace(string& s) {
// 1.定义左右指针,左指针指向满足条件元素,右指针扩展寻找
int left = 0;
for(int r = 0; r < s.size(); r++) {
if(s[r] != ' ') { // 只处理不为空格,相当于删除空格
// 手动添加一个空格,不能在单词后加,最后一个单词不好找
if(left != 0) s[left++] = ' ';
while(r < s.size() && s[r] != ' ') { // 补上单词
s[left++] = s[r++];
}
}
}
// 2. 重新定义字符串大小
s.resize(left);
}
void reverse(string& s, int start, int end) {
for(int i = start, j = end; i < j; i++, j--) swap(s[i], s[j]);
}
};
剑指Offer58-II.左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = "abcdefg", k = 2
输出: "cdefgab"
示例 2:
输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"
限制:
1 <= k < s.length <= 10000
题解
题解1:使用库函数,先切,再拼
class Solution {
public String reverseLeftWords(String s, int n) {
// 1.先将子串进行拆分
String Fs = s.substring(0, n); // 前k个子串(左闭右开)
String Ls = s.substring(n); // 后面的子串
// 2.再拼接
s = Ls + Fs;
return s;
}
}
题解2:原空间操作
局部反转+整体反转
- 反转区间为前n的子串
- 反转区间为n到末尾的子串
- 反转整个字符串
class Solution {
public:
string reverseLeftWords(string s, int n) {
// 1.反转前n个子串
reverse(s.begin(), s.begin() + n); // 库函数,前闭后开
// 2.反转n到末尾的子串
reverse(s.begin() + n, s.end());
// 3.反转整个字符串
reverse(s.begin(), s.end());
return s;
}
};
整体反转+局部反转
class Solution {
public:
string reverseLeftWords(string s, int n) {
int len = s.size();
// 1.反转整个字符串
reverse(s.begin(), s.end());
// 2.反转原n到末尾的子串
reverse(s.begin(), s.begin() + len - n);
// 3.反转原前n个子串
reverse(s.begin() + len - n, s.end()); // 库函数,前闭后开
return s;
}
};
二、字符串匹配
实现 strStr()
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
提示:
1 <= haystack.length, needle.length <= 104
haystack
和needle
仅由小写英文字符组成
题解
解法1:暴力解法
class Solution {
public int strStr(String haystack, String needle) {
char[] hl = haystack.toCharArray();
char[] nl = needle.toCharArray();
for(int i = 0; i <= hl.length - nl.length; i++) {
if(hl[i] == nl[0]) {
boolean flag = true;
for(int j = i + 1, k = 1; k < nl.length; j++, k++) {
if(hl[j] != nl[k]) {
flag = false;
break;
}
}
if(flag) return i;
}
}
return -1;
}
}
**注意:**C++的string
的size()
默认是unsigned int
即无符号整型,相减为负号时,会不准确
- 即边界条件
(int)haystack.size() - (int)needle.size()
要转为有符号整型 - 当
haystack
比needle
长度小时,条件直接不满足,直接返回-1
class Solution {
public:
int strStr(string haystack, string needle) {
// 1.遍历haystack中的每个起始字符
for(int i = 0; i <= (int)haystack.size() - (int)needle.size(); i++) {
// 2.当起始字符匹配上后,开始每轮循环判定后面字符是否匹配
if(haystack[i] == needle[0]) {
// 3.使用标志位,判断是否完全匹配
bool flag = true;
for(int j = i + 1, k = 1; k < needle.size(); j++, k++) {
if(haystack[j] != needle[k]) {
flag = false;
break;
}
}
if(flag) return i;
}
}
return -1;
}
};
题解2:KMP算法
1.求next
最大公共前后缀长度数组实现
- 初始化:
- 下标从0开始,循环不变量是找前一个数
- 定义两个指针
i
和j
,j
指向前缀末尾位置,0开始,也是无效的位置,所以回退时,从前一个位置开始计算,- 也是最长相等的前后缀长度大小,因为要用它判断是否与后缀相等
i
指向后缀末尾位置,从1开始next[i]
表示i(包括i)之前
最长相等的前后缀长度(其实就是j
)
- 处理前后缀相同的情况
s[i]
与s[j + 1]
相同,那么就同时向后移动i 和j
- 说明找到了相同的前后缀,下标从0开始,长度正好是下标++j的结果
- 同时还要将
j(前缀的长度)赋给next[i]
,因为next[i]
要记录相同前后缀的长度
- 处理前后缀不相同的情况
- 如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。
- next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。
- 那么 s[i] 与 s[j+1] 不相同,就要找 j + 1前一个元素在next数组里的值(就是next[j])
2.使用next
进行匹配
- 因为记录了最长前后缀相同的位置,
- 只要不匹配,作为后缀的值,回退到相同前缀的位置,表明,前缀的字符都是已经匹配好的,无需再从起始位置匹配,可以节省很多性能开销
- 如何匹配,就一直寻找,直到找到模式串最后一个位置,说明找到位置了,返回匹配的起始位置即可
注意:先处理不相等,跳回后,再处理相等的位置
class Solution {
public:
int strStr(string haystack, string needle) {
if (needle.size() == 0) return 0;
int next[needle.size()];
getNext(next, needle);
for(int j = 0, i = 0; i < haystack.size(); i++) {
// 1.同样,不相等,回退到最长前后缀位置
while(j > 0 && haystack[i] != needle[j]) j = next[j -1];
if(haystack[i] == needle[j]) j++;
if(j == needle.size()) return (i - needle.size() + 1);
}
return -1;
}
private:
void getNext(int* next, const string& s) {
// 1.初始化
int j = 0; // 前缀末尾位置
int i = 1; // 后缀末尾位置,也是next下标
next[0] = 0;
for(; i < s.size(); i++) {
// 2.先处理不相等情况,找到j要回退的位置
while(j > 0 && s[i] != s[j]) j = next[j - 1];
// 3.再处理相等时,j记录当前索引的最长相等前后缀长度
if(s[i] == s[j]) j++;
// 处理相同j后,给next数组赋值
next[i] = j;
}
}
};
459.重复的子字符串
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
示例 1:
输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。
示例 2:
输入: s = "aba"
输出: false
示例 3:
输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)
提示:
1 <= s.length <= 104
s
由小写英文字母组成
题解
题解1:暴力枚举
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nbBLuDyW-1674825442324)(assets/image-20221223000913130.png)]
class Solution {
public:
bool repeatedSubstringPattern(string s) {
bool flag = false;
int len = s.size();
// 枚举每个子串的长度,一定不超过总长度的一半
for(int n = 1; n <= len / 2; n++) {
// 1.与总长度成倍数关系
if(len % n != 0) continue;
bool flag = true;
// 2.枚举子串后面的字符,判断能否被子串表示
for(int i = n; i < len; i++) {
// 3.一旦有一个不满足,一票否决
if(s[i] != s[i - n]) {
flag = false;
break;
}
}
if(flag) return true;
}
return false;
}
};
解法2:字符串匹配
- 当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wV7F85wW-1674825442327)(assets/20220728104518.png)]
- 也就是由前后相同的子串组成。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z0l8FTUZ-1674825442328)(assets/image-20221223140833701.png)]
- 那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前后的子串做后串,就一定还能组成一个s,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aE9muj2y-1674825442329)(assets/20220728104931.png)]
-
所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。
-
当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,要刨除 s + s 的首字符和尾字符,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s
- 我们可以从位置 1 开始查询,并希望查询结果不为位置 n,这与移除字符串的第一个和最后一个字符是等价的
class Solution {
public:
bool repeatedSubstringPattern(string s) {
// find(s,1)等价于java的indexOf(s,1),从1开始,出现s的第一次位置
return (s + s).find(s, 1) != s.size();
}
};
题解3:KMP算法
- 在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串,这里拿字符串s:abababab 来举例,ab就是最小重复单位,如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vzo6i1Z7-1674825442329)(assets/20220728205249.png)]
-
数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环
len % (len - (next[len - 1])) == 0
class Solution {
public:
bool repeatedSubstringPattern(string s) {
// 1.获取最长前后缀数组
int len = s.size();
int next[len];
getNext(next, s);
// 2.获取最小重复元素单元,判断能否被s整数
// 注意,如何最后一个字符没有相等前后缀,s一定没有重复子串
return next[len - 1] == 0 ? false : len % (len - next[len - 1]) == 0;
}
private:
// 求取最长前后缀数组next
void getNext(int* next, string& s) {
// 1.初始化
int j = 0, i = 1; // 前后缀末尾,从0开始
next[0] = 0;
for(; i < s.size(); i++) {
// 2.处理不等的情况,进行回退
while(j > 0 && s[j] != s[i]) j = next[j -1];
// 3. 处理相等情况
if(s[j] == s[i]) j++; //j统计最长前后缀长度
next[i] = j;
}
}
};
字符串编码
1392. 最长快乐前缀
「快乐前缀」 是在原字符串中既是 非空 前缀也是后缀(不包括原字符串自身)的字符串。
给你一个字符串 s
,请你返回它的 最长快乐前缀。如果不存在满足题意的前缀,则返回一个空字符串 ""
。
示例 1:
输入:s = "level"
输出:"l"
解释:不包括 s 自己,一共有 4 个前缀("l", "le", "lev", "leve")和 4 个后缀("l", "el", "vel", "evel")。最长的既是前缀也是后缀的字符串是 "l" 。
示例 2:
输入:s = "ababab"
输出:"abab"
解释:"abab" 是最长的既是前缀也是后缀的字符串。题目允许前后缀在原字符串中重叠。
提示:
1 <= s.length <= 105
s
只含有小写英文字母
题解1:字符串哈希编码
// 字符串哈希编码-将字符串通过哈希运算映射成整数:b=131, p = 1e9+7
// 前缀:prefix: 原编码值*b + 新的字符(新字符下标从0开始计算,前缀下标从1开始,且不包含自身)
// 例如:123: 1 = 0+1, 12 = 1*10+2, 123 = 12*10+3
// 后缀:suffix: 原编码值 + 新的字符*b^(i-1)(i从1开始,b^(i-1)在每次循序中计算得出)
// 例如:123: 3 = 0+3*10^0, 23 = 3+2*10^1 , 123 = 23+1*10^2
// 本题可以不用开额外数组,在每次求的前缀和后缀时,顺便进行判断是否相等,更新满足条件的最长前缀
- 注意:
- 注意数值边界,会超过
int
边界,因此要开long
型
- 注意数值边界,会超过
class Solution {
public String longestPrefix(String s) {
int n = s.length();
long prefix = 0, suffix = 0; // 注意数值边界,会超过int边界,因此要开long型
long b = 131, p = (int)(1e9 + 7);
long powBM = 1;
int happyLen = 0;
for(int i = 1; i < n; i ++) { // 前后缀不包含s自身,因此不能包含n,注意s下标(i -1)
// 计算长度i的前缀
prefix = (prefix * b + (s.charAt(i - 1) - 'a' + 1)) % p; // 字符编码从1开始
// 计算长度i的后缀,从后往前数
suffix = (suffix + (s.charAt(n - i) - 'a' + 1) * powBM) % p;
// 计算更新b^(i - 1)
powBM = powBM * b % p;
// 判断前缀与后缀是否相等
if(prefix == suffix) happyLen = i; // 找到了,就更新最长前缀
}
return s.substring(0, happyLen);
}
}