反转字符串
例题344:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
class Solution {
public:
void reverseString(vector<char>& s) {
int left=0, right=s.size()-1;
while (left < right)
{
//swap(s[left], s[right]);//自己写交换函数
//reverse(s);直接调换字符串
char t;
t = s[left];
s[left] = s[right];
s[right] = t;
left++;
right--;
}
}
};
反转字符串 ||
例题541:给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
- 如果剩余字符少于 k 个,则将剩余字符全部反转。
- 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。
class Solution {
public:
string reverseStr(string s, int k) {
int i, left, right;
//这段可加可不加,加上运行速度更快
if (k > s.length())
{
left=0;
right=s.length()-1;
while(left<right)
{
swap(s[left],s[right]);
left++;
right--;
}
return s;
}
for (i = 0; i < s.length(); i+=2*k)
{
left = i;
right = i + k-1;
if(right>s.length()-1)
{
right=s.length()-1;//注意right可能超出字符串的长度
}
while (left < right)
{
swap(s[left], s[right]);
left++;
right--;
}
}
return s;
}
};
输入字符串用
getline(cin,s)
替换空格
例题剑指offer 05:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
class Solution {
public:
string replaceSpace(string s) {
/*新开空间做法
int i =0;
string res;
while (i<s.length())
{
if (s[i] == ' ')
{
res.push_back('%');
res.push_back('2');
res.push_back('0');
}
else{
res.push_back(s[i]);
}
i++;
}
return res;
}
*/
//原地从后往前后移做法
int count = 0;
for (char a : s)
{
if (a == ' ')
{
count++;//计算空格数
}
}
int oldsize = s.size();
s.resize(s.size() + 2 * count);//扩充字符串函数resize
int newsize = s.size();
for(int left=oldsize-1,right=newsize-1;left<right;left--,right--)
{
if (s[left] == ' ')
{
s[right] = '0';
s[right - 1] = '2';
s[right - 2] = '%';
right -= 2;
}
else
{
s[right] = s[left];
}
}
return s;
}
};
扩充字符串函数
xx.resize()
字符串和数组的区别:char a[5]="abc";
通常在C语言中是以'\0'
来判断字符数组是否结束
如:for(int i=0;a[i]!='\0';i++)
而C++中,通常提供string类,提供size接口判断字符串是否结束,就不用'\0'
来判断
如:for(int i=0;i<s.size();i++)
string与vector的区别在于string提供了更多的字符串处理接口,如重载了+,而vector却没有
翻转字符串里的单词
例题151:给你一个字符串 s ,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
//重新开空间,空间复杂度为O(n)的做法
class Solution {
public:
string reverseWords(string s) {
if(s.size()==1)
{
return s;
}
string res;
int right = s.size() - 1;
int left = right;
int t;
while(left>0)
{
while (right>0 && s[right] == ' ' )//找到每个单词的最后一位
{
right--;
if(right==0 && s[right]==' ')
{
res.erase(res.size()-1);
return res;
}
}
left = right;
while (left>0 && s[left - 1] != ' ')//找到每个单词的第一位
{
left--;
}
t = left-1;
while (t < right)//将每个单词推入新的容器
{
res.push_back(s[++t]);
}
res.push_back(' ');//每个单词后加一个空格
right = left-1;
}
while(res[res.size()-1]==' ')//删去倒转后结尾的空格
{
res.resize(res.size()-1);//容器重定义大小
}
return res;
}
};
类似于双指针的做法,找到每个单词的前后位置,然后将该单词推入容器,后加一个空格,遍历完后删去结尾的一个空格
容器的头插法xx.insert(xx.begin(),i)
擦除容器的第i位元素xx.erase(xx[i])
也可以利用栈先进后出的特点实现
进阶:使用复杂度为O(1)实现——先将字符串多余的空格删去,再将字符串倒序,最后将单词翻转回来
左旋转字符串
例题剑指offer58-||:字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
//多开了n位大小的空间
class Solution {
public:
string reverseLeftWords(string s, int n) {
int oldsize=s.size();
s.resize(oldsize + n);
int newp = oldsize-1;
int left = -1;
while(left < n)
{
s[++newp] = s[++left];
}
s.erase(s.begin(),s.begin()+n);//begin指向第一个元素,end指向最后一个元素的下一个位置。erase中擦除的范围是[begin,end)
return s;
}
};
注意xx.erase()函数的起始区间是左开右闭,到最后一个位置的前一个元素,时间复杂度为O(n)
进阶:不新开空间,直接在原字符串上操作(先局部再整体翻转)
reverse(s.begin(), s.begin() + n);//reverse不是对象内的函数,可以直接调用,与swap()类似。和xx.erase()声明对象不同
reverse(s.begin() + n, s.end());
reverse(s.begin(), s.end());
return s;
实现strStr()
例题28:给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
class Solution {
public:
int strStr(string haystack, string needle) {
if (needle.size() > haystack.size())
{
return -1;
}
int ns = needle.size();
int minpos;
int i =0,j= 0, left, right;
for(i=0;i<haystack.size();i++)
{
left = i;
right = left + ns - 1;
while (left <= right)
{
if (haystack[left] == needle[j])
{
if (j == ns - 1)
{
return i;
}
left++;
j++;
}
else
{
j=0;
break;
}
}
}
return -1;
}
};
常规做法,每次移动一位找后续needle位是否与needle相同,时间复杂度是O(n*m),n是haystack的大小,m是needle的大小,效率不高
升级做法:KMP,利用前缀表找到不匹配时应该回退的下标,时间复杂度是O(n+m)
next数组:可以是前缀表,也可以是前缀表统一减一(后移一位,初始位置-1)
KMP算法
解决字符串匹配问题,当不匹配时跳到前面匹配后的位置
暴力法:每次后移一位,重头继续比较模式串与字符串,O(n*m)
KMP算法:不匹配时回退到之前匹配过的下标
例如模式串 aabaaf
前缀表:找到之前已经匹配过的内容(每次不匹配时,找到前一个子串的最长相等前后缀是多少,跳到下标为最长前后缀的位置),模式串的前缀表为(0,1,0,1,2,0)
前缀:不包含尾字符的子串,模式串的前缀为(a,aa,aab,aaba,aabaa)
后缀:不包含首字符的子串,模式串的后缀为(f,af,aaf,baaf,abaaf)
最长相等前后缀:为模式串前缀表的最大值,该例中为2
next数组:前缀表-1,初始位为-1,也可以完成KMP的工作,原理相同,该例next为(-1,0,-1,0,2,-1)
next数组不同的实现方法:
①前缀表右移一位:找到不匹配位置时,直接看该位置next数组的值,退回到该值表示的下标
②前缀表减一:不匹配时看前一位,退回到前一位加一的下标处
获取next数组的具体代码:①初始化;②前后缀不同;③前后缀相同;④next
前缀表不减一
void getNext(int* next,const string& s)
{
//初始化i,j,j指向前缀末尾位置,i指向后缀末尾位置
j=0,next[0]=j;
for(i=1;i<s.size();i++)
{
//前后缀不同,j回退到前一位的下标处
while(j>0 && s[i]!=s[j])
{
j=next[j-1];
}
//前后缀相同
if(s[i]==s[j])
{
j++;
}
next[i]=j;//更新
}
前缀表减一
void getNext(int* next,const string& s)
{
int j=-1,next[j]=j;
for(int i=1;i<s.size();i++)
{
while(j>=0 && s[j]!=s[i])
{
j=next[j];
}
if(s[j]==s[i])
{
j++;
}
next[i]=j;
}
}
用next数组匹配的代码:
定义两个下标,i指向文本串的开始位置用来遍历文本串,j指向模式串的开始位置用来遍历模式串
for(int i=0;i<s.size();i++)
接下来就要比较文本串与模式串相同否?就是s[i]与s[j+1],因为j从-1开始。
如果不相同,j就回退
while(j>=0 && s[i]!=s[j]){
j=next[j]
};
如果s[i]与s[j+1]相同,那么i和j同时向后移动
if(s[i]==s[j+1]){
j++;
}
怎么判断模式串完整出现在文本串中,当j走到模式串末尾就表示完全匹配
怎么找到文本串中开始完全匹配的起始位置,返回文本串i的位置减去模式串的长度,就是文本串中出现模式串的第一个位置
if(j==s.size()){
return (i-s.size()+1);
}
因此,使用减一后的next数组匹配的完整代码为
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);
}
}
不减一的next数组匹配的完整代码为
int j = 0; // 因为next数组里记录的起始位置为-1
for (int i = 0; i < s.size(); i++) { // 注意i就从0开始
while(j > 0 && s[i] != s[j]) { // 不匹配
j = next[j-1]; // j 寻找之前匹配的位置
}
if (s[i] == s[j]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t
return (i - t.size() + 1);
}
}
因此,例题28使用KMP算法的代码如下:
public:
void getNext(int* next, const string& s)//传入的next数组的地址
{
int j = 0;
next[j] = j;
for (int i = 1; i < s.size(); i++)
{
while (j > 0 && s[j] != s[i])
{
j = next[j - 1];
}
if (s[j] == s[i])
{
j++;
}
next[i] = j;
}
}
if (needle.size() == 0)
{
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++)
{
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;
}
};
重复的子字符串
例题459:给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。
class Solution {
public:
bool repeatedSubstringPattern(string s) {
if(s.size()==1)
{
return false;
}
bool res=true;
//暴力法:枚举可能的子串长度,循环遍历是否有重复的子串
int i, j;
for (i = 1; i <= s.size() / 2; i++)//枚举可能的子串长度
{
if (s.size() % i == 0)
{
res = true;
for (j = i; j < s.size(); j++)
{
if (s[j] != s[j - i])//比较以后子串位置的元素是否相等
{
res = false;
break;
}
if(j==s.size()-1)
{
return true;
}
}
}
}
return res;
}
};
还可以用KMP算法
int len = s.size();//如果不用len表示长度会在数组定义时出错
if (len == 0) {
return false;
}
//也可以是int next[s.size()];
int* next = new int[len];
getNext(next, s);
if (next[len - 1] != 0 && len % (len - (next[len - 1])) == 0) {
return true;
}
return false;
总结
C语言中使用'\0'
作为字符数组的结束标志,例如
char a[5] = "asd";
for (int i = 0; a[i] != '\0'; i++) {
}
C++中提供一个string类,string类会提供size接口,判断string字符串是否结束,就不用使用'\0'
判断,例如
string a = "asd";
for (int i = 0; i < a.size(); i++) {
}
那么vector< char > 和 string 又有什么区别呢?
其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。
双指针法
在344题翻转字符串中,可以使用双指针反转字符串,双指针在数组、链表、字符串中很常用
在题目(字符串:替换空格)中,也使用双指针用时间复杂度O(n)从后向前替换空格
其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
erase是O(n)的操作
反转系列
看到反转类型的题,可以想想经过多次反转后,是否可以达到目的,例如先局部再整体,或者先整体再局部
KMP算法
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。
前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
前缀表减一与否的两种实现方式
双指针法是字符串处理的常客。
KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易,不断总结和完善,才能把KMP理解清楚。