基础知识
子串:要求字符之间是连续的
子序列:不要求连续
例题
双指针,left指向最左边,right指向最右边,交换left和right的位置,直到left>=right.
c++的reverse()就是字符串翻转函数。
class Solution {
public:
void reverseString(vector<char>& s) {
int left=0;
int right=s.size()-1;
while(left<right ){
swap(s[left],s[right]);
left++;
right--;
}
}
};
如果剩余字符串长度小于k,翻转整个字符
如果剩余字符串长度大于等于k,翻转前k个字符。
i每次加2k
class Solution {
public:
void my_reverse(string& s,int l,int r){
int left=l;
int right=r;
while(left<right){
swap(s[left++],s[right--]);
}
}
string reverseStr(string s, int k) {
for(int i=0;i<s.length();i+=2*k){
if(s.length()-i<k){//剩余字符小于k
my_reverse(s,i,s.length()-1);
}
else{//剩余字符大于或等于k
my_reverse(s,i,i+k-1);
}
}
return s;
}
};
直接法:遇到空格,就多申请2个字符的空间,然后整体后移两位,然后替换为%20.
class Solution {
public:
string replaceSpace(string s) {
for(int i=0;i<s.length();i++){
if(s[i]==' '){
s.resize(s.length()+2);
for(int j=s.length()-1;j>=i+3;j--){//整体后移两位
s[j]=s[j-2];
}
s[i]='%';
s[i+1]='2';
s[i+2]='0';
i=i+2;//由于前面的移到了后面
}
}
return s;
}
};
解2 双指针,和解1相比不需要整体挪动
先统计s中空格的个数,然后多申请2*count大小的空间
令right指向新s的末尾,left指向原来s的末尾,令s[right]=s[left]
,直到left等于0
如果s[left]==' ',s[r]='0'; s[r-1]='2';s[r-2]='%'
,r后移3位,l后移1位。
否则r--,l--.
class Solution {
public:
string replaceSpace(string s) {
int count=0;
for(int i=0;i<s.length();i++){
if(s[i]==' '){
count++;
}
}
s.resize(s.length()+2*count);
int l=s.length()-2*count-1;
int r=s.length()-1;
while(l>=0){
if(s[l]!=' '){
s[r--]=s[l--];
}
else{
s[r--]='0';
s[r--]='2';
s[r--]='%';
l--;
}
}
return s;
}
};
解1 先实现单词的分割,以空格为单位,放在result中,注意多余空格的处理
然后从后往前遍历result,每个单词后面(除最后一个单词)加一个空格放在t中
最后返回t
class Solution {
public:
vector<string>my_split(string s, char a) {
vector<string> result;
string t = "";
for (int i = 0; i < s.length(); i++) {
if (s[i] != ' ') {
t += s[i];
}
else {
if (t != "") {//找到一个单词
result.push_back(t);
t = "";
}
}
}
if (t != "") {
result.push_back(t);
}
return result;
}
string reverseWords(string s) {
vector < string> re = my_split(s, ' ');
string t = "";
for (int i = re.size() - 1; i >= 0; i--) {
if (i == 0) {
t = t + re[i];
}
else {
t = t + re[i] + " ";
}
}
return t;
}
};
解2 不申请额外的空间,在原有字符串的修改
- 去除多余空格
- 翻转字符串(单词也是反的)
- 翻转单词
补充:
substr(start,length)//从start开始截取length长度的字符串
resize(size)//重新分配空间,溢出的直接丢弃
erase(start,end)左闭右开区间删除字符串
class Solution {
public:
void myRerverse(string& s,int start,int end) {//左闭右闭区间翻转
int l = start;
int r = end ;
while (l < r) {
swap(s[l++], s[r--]);
}
}
string reverseWords(string s) {
//1 2 去除多余空格并翻转
int l = 0;
for (int r=0; r < s.length();r++) {
if (r == 0 && s[r] == ' ') {//去除前面的空格
continue;
}
if (r>0 && s[r] == s[r - 1] && s[r] == ' ') {//去除中间的空格
continue;
}
else {
s[l++] = s[r];
}
}
if (s[l - 1] == ' ') {//去除末尾多余的空格
// s = s.substr(0, l-1);//与resize作用一样
s.resize(l - 1);//多余的空格丢弃
myRerverse(s,0,s.length()-1);
}
else {
/* s = s.substr(0, l);*/
s.resize(l);
myRerverse(s, 0, s.length() - 1);
}
//3 翻转单词
int start,end;
for ( start = 0 , end = 0; end < s.length();) {
if (s[end] == ' ') {
myRerverse(s, start, end - 1);
start = end + 1;
end = start;
}
else if (end == s.length() - 1) {//翻转最后一个单词
myRerverse(s, start, end);
break;
}
else {
end++;
}
}
return s;
}
};
解1 多申请n个空间,将旋转的字符放在后面,然后去掉前n个字符
class Solution {
public:
string reverseLeftWords(string s, int n) {
int len0=s.length();
s.resize(len0+n);
int l=0;
int r=len0;
for(int i=0;i<n;i++){
s[r++]=s[l++];
}
s=s.substr(n,s.length());
return s;
}
};
解2 不申请额外的空间
先整体翻转,在局部翻转
class Solution {
public:
void myReverse(string& s,int l,int r){
while(l<r){
swap(s[l++],s[r--]);
}
}
string reverseLeftWords(string s, int n) {
int len=s.length();
myReverse(s,0,len-1);
myReverse(s,0,len-n%len-1);//考虑n大于len的情况
myReverse(s,len-n%len,len-1);
return s;
}
};
解1 暴力搜索,效率太慢
class Solution {
public:
int strStr(string haystack, string needle) {
if (needle == "") {
return 0;
}
int start = -1;//记录第一次出现的位置
for (int i = 0; i < haystack.length();i++) {
bool flag = true;
if (haystack[i] == needle[0] && haystack.length() - i >= needle.length()) {//要保证当前i到结束的长度大于needl的长度,避免"aa","aaa"的情况。
start = i;
int t = i;
for (int j = 0; j < needle.length(); j++) {
if (haystack[t++] != needle[j]) {
flag = false;
break;
}
}
if (!flag) {//不相等
start = -1;
}
else {
break;
}
}
}
return start;
}
};
解2 KMP
KMP主要应用在字符串匹配上。
KMP的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。前缀表记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。(前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。)
例如:要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。
next数组就是前缀表
next数组的初始化
i表示后缀的结尾,j表示前缀的结尾,i从1开始,j从0开始
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;//next[i]表示下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀,由于下标从0开始,也就是回退 重新匹配的位置
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了(遇到冲突看前一位对应的next值)
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
匹配成功返回第一次出现的开始位置即
i-needle.length()+1
class Solution {
public:
void getNext(int* next,const string &s){
next[0]=0;
int j=0;
for(int i=1;i<s.length();i++){//注意i从1开始
while(j>0 && s[i]!=s[j]){
j=next[j-1];
}
if(s[i]==s[j]){
j++;
}
next[i]=j;
}
}
int strStr(string haystack, string needle) {
if(needle==""){
return 0;
}
int next[needle.length()];
getNext(next,needle);
int j=0;
for(int i=0;i<haystack.length();i++){//当j==needle.length()匹配成功
while(j>0 && haystack[i]!=needle[j]){
j=next[j-1];
}
if(haystack[i]==needle[j]){
j++;
}
if(j==needle.length()){//此时j指向needle末尾的后一位,i指向haystack与needle最后一个字符匹配的位置
return (i-needle.length()+1);
}
}
return -1;
}
};
对next数组做一些调整,匹配失败位置对应的next数组的值就为回退的位置
class Solution {
public:
void getNext(vector<int>& next, string& s) {
next.resize(s.size());
int i = 0;
int j = -1;
next[0] = -1;
while (i < s.size()-1) {
if (j == -1 || s[i] == s[j]) {
++i;
++j;
next[i]=j;
}
else {
j = next[j];
}
}
}
int strStr(string haystack, string needle) {
int i = 0;
int j = 0;
vector<int>next;
getNext(next, needle);
int n1 = haystack.size();
int n2 = needle.size();
while ((i<n1)&&(j < n2) ) {//vector.size()返回值是unsigned int,当int与unsigned int比较是,会先将int转为unsigned int
if (j == -1 || haystack[i] == needle[j]) {
++i;
++j;
}
else {
j = next[j];
}
}
if (j == needle.size()) {
return i - needle.size();
}
else {
return -1;
}
}
};
class Solution {
public:
void getNext(vector<int>& nextval, string& s) {
nextval.resize(s.size());
int i = 0;
int j = -1;
nextval[i] = -1;
while (i < s.size()-1) {//因为循环里有++i操作,为避免出界
if (j == -1 || s[i] == s[j]) {
++i;
++j;
if (s[i] != s[j]) {
nextval[i] = j;
}else{
nextval[i]=nextval[j];
}
}
else {
j = nextval[j];
}
}
}
int strStr(string haystack, string needle) {
int i = 0;
int j = 0;
vector<int>nextval;
getNext(nextval, needle);
int n1 = haystack.size();
int n2 = needle.size();
while ((i<n1)&&(j < n2) ) {
if (j == -1 || haystack[i] == needle[j]) {
++i;
++j;
}
else {
j = nextval[j];
}
}
if (j == needle.size()) {
return i - needle.size();
}
else {
return -1;
}
}
};
next数组进一步改进
考虑一种特殊情况:aaaaab
对应next数组:-1 0 1 2 3 4
如果j==4发生不匹配,则 j 回退到3,然后又回退到2,1,0,-1,进行了很多没必要的跳转,其中只要调整位置的字符与当前位置一样,必然很需要继续调整,何不一步到位呢
class Solution {
public:
void getNext(vector<int>& nextval, string& s) {
nextval.resize(s.size());
int i = 0;
int j = -1;
nextval[i] = -1;
while (i < s.size()-1) {
if (j == -1 || s[i] == s[j]) {
++i;
++j;
if (s[i] != s[j]) {
nextval[i] = j;
}else{//改进之处
nextval[i]=nextval[j];
}
}
else {
j = nextval[j];
}
}
}
int strStr(string haystack, string needle) {
int i = 0;
int j = 0;
vector<int>nextval;
getNext(nextval, needle);
int n1 = haystack.size();
int n2 = needle.size();
while ((i<n1)&&(j < n2) ) {//vector.size()的返回值是unsigned int,当int与unsigned int比较是会先将int转为unsigned int
if (j == -1 || haystack[i] == needle[j]) {
++i;
++j;
}
else {
j = nextval[j];
}
}
if (j == needle.size()) {
return i - needle.size();
}
else {
return -1;
}
}
};
查找s的最长相等前后缀,如果最长相等前后缀等于0,则该字符串一定不是由子串构成;若不为0,字符串的长度减去-最长相等前后缀的长度=字符串一个周期的长度。如果len%一个周期的长度==0,说明字符串由重复的子字符串合成。
class Solution {
public:
void getNext(int* next,string& s){
int j=0;
next[0]=0;
for(int i=1;i<s.length();i++){
while(j>0 && s[j]!=s[i]){
j=next[j-1];
}
if(s[i]==s[j]){
j++;
}
next[i]=j;
}
}
bool repeatedSubstringPattern(string s) {
if(s==""){
return false;
}
int len=s.length();
int next[len];
getNext(next,s);
if(next[len-1]!=0 && len%(len-next[len-1])==0){
return true;
}
return false;
}
};
总结
通过这几个例题,掌握了字符串的翻转、空格的替换、通过翻转实现句子的倒置,重点学习了kmp。
- 与字符串有关的操作基本上也会用上双指针。
- kmp解决字符串匹配问题。