反转字符串
例题344(简单)反转字符串
注意要点:
- 只需要遍历一半,每次交换首尾即可。
下面贴出代码:
CPP版本
class Solution {
public:
void reverseString(vector<char>& s) {
for (int i = 0; i < s.size() / 2; i++)
{
int tmp = s[i];
s[i] = s[s.size() - 1 - i];
s[s.size() - 1 - i] = tmp;
}
}
};
C版本
void reverseString(char* s, int sSize){
for (int i = 0; i < sSize / 2; i++)
{
int tmp = s[i];
s[i] = s[sSize - 1 - i];
s[sSize - 1 - i] = tmp;
}
}
拓展题541(简单)反转字符串II
注意要点:
- 直接通过for循环,循环变量i+=2k即可完成每次递增2k,然后判断翻转即可。
下面贴出代码:
CPP版本
class Solution {
public:
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += 2 * k)
{
if (i + k <= s.size())
{
reverse(s.begin() + i, s.begin() + i + k);
}
else {reverse(s.begin() + i, s.end());}
}
return s;
}
};
C版本
char * reverseStr(char * s, int k){
int len = strlen(s);
for (int i = 0; i < len; i += 2 * k)
{
int right = fmin(i + k - 1, len - 1);
int left = i;
while (left < right)
{
char tmp = s[right];
s[right] = s[left];
s[left] = tmp;
left++;
right--;
}
}
return s;
}
替换空格
例题 剑指Offer05(简单)替换空格
注意要点:
- 从前往后的暴力解法需要移动数组位置,所以时间复杂度是O(n^2);
- 双指针法,先统计出所需数组的大小,然后从后往前填充,复杂度降为O(n)。
- C++可以直接 resize() 数组的大小。
下面贴出代码:
CPP版本
class Solution {
public:
string replaceSpace(string s) {
//统计空格数量
int count = 0;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == ' ') {count++;}
}
//扩充为所需的数组大小
int oldSize = s.size();
s.resize(s.size() + 2 * count);
int newSize = s.size();
for (int i = newSize - 1, j = oldSize - 1; i >= 0, j >= 0; i--, j--)
{
if (s[j] == ' ')
{
s[i--] = '0';
s[i--] = '2';
s[i] = '%';
}
else {s[i] = s[j];}
}
return s;
}
};
C版本
char* replaceSpace(char* s){
int size = 0;
for (int i = 0; i < strlen(s); i++)
{
if (s[i] != ' ') {size++;}
else {size += 3;}
}
char* ans = (char* )malloc(sizeof(char) * (size + 1));
int ptr_s = strlen(s) - 1, ptr_ans = size;
ans[ptr_ans] = '\0';
ptr_ans--;
while (ptr_s >= 0)
{
if (s[ptr_s] != ' ')
{
ans[ptr_ans] = s[ptr_s];
ptr_s--;
ptr_ans--;
}
else
{
ans[ptr_ans] = '0';
ans[ptr_ans-1] = '2';
ans[ptr_ans-2] = '%';
ptr_ans -= 3;
ptr_s--;
}
}
return ans;
}
翻转字符串中的单词
例题151(中等)反转字符串中的单词
注意要点:
- 最重要的思路:首先先删掉多余的空格,反转整个字符串,然后寻找到每一个单词,再把单词翻转;
- 删除多余的空格,使用快慢指针就可以,类似之前的移除元素;
- 反转可以直接库函数reverse,也可以自己写,我是写了一个左闭右开的翻转函数;
- 如果使用void修改原字符串,需要注意传参就需要传入原先字符串的首地址。
下面贴出代码:
CPP版本
class Solution {
public:
void myreverse(string& s, int start, int end)
{
int mid = start + (end - start) / 2;
for (int i = start; i < mid; i++)
{
swap(s[i], s[end - 1 - (i - start)]);
}
}
void removeExtraSpaces(string& s)
{
int slow = 0;
for (int i = 0; i < s.size(); i++)
{
if (s[i] != ' ')
{
if (slow) {s[slow++] = ' ';}
while (i < s.size() && s[i] != ' ') {s[slow++] = s[i++];}
}
}
s.resize(slow);
}
string reverseWords(string s) {
removeExtraSpaces(s);
myreverse(s, 0, s.size());
int start = 0;
for (int i = 0; i <= s.size(); i++)
{
if (i == s.size() || s[i] == ' ')
{
myreverse(s, start, i);
start = i + 1;
}
}
return s;
}
};
C版本
char * reverseWords(char * s){
int slow = 0;
for (int i = 0; i < strlen(s); i++)
{
if (s[i] != ' ')
{
if (slow) {s[slow++] = ' ';}
while (i < strlen(s) && s[i] != ' ') {s[slow++] = s[i++];}
}
}
s[slow] = '\0';
int left = 0, right = strlen(s) - 1; // 当前头部仍可能有空格时的字符串长度
while (left < right)
{
char tmp = s[right];
s[right] = s[left];
s[left] = tmp;
left++;
right--;
}
int len_tmp = strlen(s);
int len = strlen(s);
// 接下来翻转单词
int word_left = 0;
for (int i = 0; i < len + 1; i++)
{
if (s[i] == ' ' || s[i] == '\0')
{
int word_right = i - 1;
while (word_left < word_right)
{
char tmp = s[word_right];
s[word_right] = s[word_left];
s[word_left] = tmp;
word_left++;
word_right--;
}
word_left = i + 1;
}
}
return s;
}
左旋转字符串
例题 剑指Offer58-II(简单)左旋转字符串
注意要点:
- 类似上一题,都是直接翻转再加局部翻转。
下面贴出代码:
CPP版本
class Solution {
public:
string reverseLeftWords(string s, int n) {
reverse(s.begin(), s.end());
reverse(s.begin(), s.end() - n);
reverse(s.end() - n, s.end());
return s;
}
};
C版本
void reverse(char* s, int start, int end)
{
int left = start, right = end - 1;
while (left < right)
{
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left++;
right--;
}
}
char* reverseLeftWords(char* s, int n){
reverse(s, 0, strlen(s));
reverse(s, 0, strlen(s) - n);
reverse(s, strlen(s) - n, strlen(s));
return s;
}
KMP算法
这不是代码随想录的分类方法,他的分类逻辑是根据题目讲解某种算法。但是这种算法我之前一刷完成后,只有在字符串这个章节中出现,但是却又非常重要(不会的话有些题直接很难做),所以单独开一个专题记录一下笔记。
KMP算法可以用作字符串匹配,当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了!所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。
前缀表
next数组就是一个前缀表(prefix table)。
前缀表的定义:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
前缀表的作用:用来回退,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
也就是说,前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
最长公共前后缀
文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
前缀表要求的就是相同前后缀的长度。
使用前缀表的原因(重中之重,帮助理解)
当前的搜索字符串的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。
所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
前缀表与next数组
next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
其实这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。实际写代码的情况下,全部右移写起来简单很多,严格按照定义,写代码的时候就会有一些小细节容易忘记。
构造next数组
这里我就直接把带注释的代码拉过来了:
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
使用next数组来做匹配
还是直接把代码拉过来,其中s是待匹配字符串,而t是模版字符串:
int j = -1; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
while(j >= 0 && s[i] != t[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
return (i - t.size() + 1);
}
}
例题28(简单)找出字符串中第一个匹配项的下标
注意要点:
- 就是直接使用KMP算法进行匹配就可以了。
下面贴出代码:
CPP版本
class Solution {
public:
void getNext(int* next, const string& s)
{
int j = -1;
next[0] = j;
for (int i = 1; i < s.size(); i++)
{
while (j >= 0 && s[i] != s[j + 1]) {j = next[j];}
if (s[i] == s[j + 1]) {j++;}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (!needle.size()) {return 0;}
int next[needle.size()];
getNext(next, needle);
int j = -1;
for (int i = 0; i < haystack.size(); i++)
{
while (j >= 0 && haystack[i] != needle[j + 1]) {j = next[j];}
if (haystack[i] == needle[j + 1]) {j++;}
if (j == needle.size() - 1) {return i - (needle.size() - 1);}
}
return -1;
}
};
C版本
void getNext(int* next, const char* s)
{
int j = -1;
next[0] = j;
for (int i = 1; i < strlen(s); i++)
{
// 前后缀不同
while (j >= 0 && s[i] != s[j + 1]) {j = next[j];}
if (s[i] == s[j + 1]) {j++;}
next[i] = j;
}
}
int strStr(char * haystack, char * needle){
if (!strlen(needle)) {return 0;}
int next[strlen(needle)];
getNext(next, needle);
int j = -1;
for (int i = 0; i < strlen(haystack); i++)
{
while (j >= 0 && haystack[i] != needle[j + 1]) {j = next[j];}
if (haystack[i] == needle[j + 1]) {j++;}
if (j == strlen(needle) - 1)
{
return i - strlen(needle) + 1;
}
}
return -1;
}
例题459(简单)重复的子字符串
注意要点:
- 不使用KMP,可以直接复制两个字符串数组,然后掐头去尾,在其中判断是否有原字符串即可;
- 使用KMP,重复子字符串就是原字符串-最长相等前后缀。
下面贴出代码:
CPP版本
class Solution {
public:
void getNext (int* next, const string& s)
{
next[0] = -1;
int j = -1;
for(int i = 1;i < s.size(); i++)
{
while(j >= 0 && s[i] != s[j + 1]) {j = next[j];}
if(s[i] == s[j + 1]) {j++;}
next[i] = j;
}
}
bool repeatedSubstringPattern(string s) {
if (!s.size()) {return 0;}
int next[s.size()];
getNext(next, s);
int len = s.size();
if (next[len - 1] != -1 && !(len % (len - (next[len - 1] + 1)))) {return 1;}
return 0;
}
};
C版本
void getNext(int* next, const char* s)
{
int j = -1;
next[0] = j;
for (int i = 1; i < strlen(s); i++)
{
while (j >= 0 && s[i] != s[j + 1]) {j = next[j];}
if (s[i] == s[j + 1]) {j++;}
next[i] = j;
}
}
bool repeatedSubstringPattern(char * s){
int* next = (int* )malloc(sizeof(int) * strlen(s));
getNext(next, s);
int len = strlen(s);
if (next[len - 1] != -1 && !(len % (len - (next[len - 1] + 1)))) {return 1;}
return 0;
}
总结
- 字符串的题目大多涉及翻转,使用C++就可以直接用库函数reverse()完成操作,C只能自己苦哈哈的去for循环交换;
- 翻转单词等的操作,都可以采用整体+局部的两次翻转实现;
- 如果题目有设计删除操作,可以考虑快慢指针;
- KMP算法,只能死记硬背,而且之后没有涉及,就在字符串这边出现了一下。