提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
前言
一、串的结构
二、基本操作
三、串的匹配算法
1.朴素模式匹配算法
2.RabinKarp(滚动哈希)
3.KMP算法
4.字典树(前缀树)
5.后缀数组
6.后缀自动机
四、字符串的经典问题
总结
串即字符串,由零个或多个字符组成的有限序列,属于线性表,以下为有关串的结构、基本操作实现和串的匹配算法。
顺序存储:
typedef struct{
char ch[MAX_SIZE];
int length; //使用ch[0]或length或字符串最后一个位置加上‘\0’标识
}SString;
C++可以直接使用string来表示字符串,需要头文件
输入输出不使用scanf和printf而是使用cin和cout。
但string、cin和cout的耗时长。
常用方法:
string.size()
string.erase()
operator[]
operator=
operator+
operator==,!=,<,>,<=,>=以字典序比较两个字符串
operator>>, <<进行string上的流输入与输出
getline()从I/O流读取数据到字符串
。。。
习题:洛谷 P1012拼数
//基本操作仅实现提取子串、串的比较、求子串的位置
bool SubString(SString &Sub, SString S, int pos, int len){
if(pos + len-1 > S.length)
return false;
int j = 1;
for(int i = pos; i < pos + len; i++)
Sub.ch[j++] = S.ch[i];
Sub.length = len;
return true;
}
bool subsstring(SString &s, SString t, int pos, int len){
if(pos + len - 1 > t.length)
return false;
int i = 1, j = pos;
while(j < pos + len){
s[i++] = t[j++];
}
s.length = len;
return true;
}
int StrCompare(SString S, SString T){
int m = S.length;
int n = T.length;
for(int i = 1; i <= m && i <= n; i++)
if(S.ch[i] != T.ch[i])
return S.ch[i] - T.ch[i];
//若扫描的字符都相等
return S.length - T.length;
}
int Index(SString S, SString T){
int m = S.length,n = T.length;
SString h;
for(int i = 1; i <= m - n - 1; i++){
SubString(h, S, i , n);
if(StrCompare(h, T)== 0)
return i;
}
return 0;
}
时间复杂度:O(M * N)
//串的朴素模式匹配算法,和前面求子串的位置思路一样的,算法思想:
//每次从主串中提取与模式串长度相同的子串,再与模式串比较
//若不一致,提取位置后一位继续比较。
int Index(SString S, SString T){
int k = i = j =1;
while( i <= S.length && j <= T.length){
if(S.ch[i] == T.ch[j]){
i++;
j++;
}else{
k++;
i = k;
j = 1;
}
}
if(j > T.length)
return k;
else
return 0;
}
时间复杂度:O(M + N)
利用:
计算字符串哈希,比较哈希值判断相同
第一求出原字符串前N个字符组成的子串res[0]求出hash值,
对第一次求出的哈希值 , 根据res[n] = res[n - 1] * seed - pow(seed, n) * firstChar + newChar ; 去除第一个字符,加上一个新字符
缺点:可能出现哈希冲突
long hash(string s){
long hashValue = 0;
long seed = 31;
for(int i = 0; i < s.length(); i++)
hashValue = seed * hashValue + s[i];
return hashValue;
}
//KMP算法,减少模式串指针的回溯,
//求模式串的next数组:当比较模式串的第i个元素不匹配时,
//记模式串的第1~i-1个元素组成的串为S,
//next[i] = S的最长相等前后缀长度 + 1, next[1] = 0
void get_Next(SString T, int next[]){
int i = 1, j = 0; //不相等的时候,便于都加1
next[1] = 0;
//比较串的前后缀相同长度,i一直遍历,不回头,j回头,j表示最长前缀后缀的长度
while(i < T.length){
if(j==0 || T.ch[i] == T.ch[j] ){
i++;
j++;
next[i] = j;
}else
j = next[i];
}
}
若ch[i] == ch[j] 或者 j == -1,则next[i + 1] = j + 1;
否则 j继续回溯,直到满足ch[i] == ch[j]或者j == -1
void get_Next(SString T, int next[]){
int i = 0, j = -1; //不相等的时候,便于都加1
next[0] = -1;
//比较串的前后缀相同长度,i一直遍历,不回头,j回头,j表示最长前缀后缀的长度
while(i < T.length){
if(j==-1 || T.ch[i] == T.ch[j] ){
i++;
j++;
next[i] = j;
}else
j = next[i];
}
}
void get_Nextval(SString T, int next[]){
int i = 1, j = 0; //不相等的时候,便于都加1
next[1] = 0;
//比较串的前后缀相同长度,i一直遍历,不回头,j回头,j表示最长前缀后缀的长度
while(i < T.length){
if(j==0 || T.ch[i] == T.ch[j] ){
i++;
j++;
if(T.ch[i] != T.ch[j])
next[i] = j;
else
next[i] = next[j];
}else
j = next[i];
}
}
int Index_KMP(SString S, SString T){
if(S.length == 0 || S.length == 0) return -1;
if(S.length > T.length) return -1;
int i = 0,j = -1;
int next[] = new int[T.length + 1];
get_Next(T, next);
while(i < S.length && j < T.length){
if(j == -1 || S.ch[i] = T.ch[j]){
i++;
j++;
}else
j = next[j];
}
if(j == T.length )
return i - j;
return -1;
}
定义:
串的所有后缀子串的按照字典序排序后,在数组中记录后缀的起始下标。
例如s[k] = m表示所有后缀子串按照字典序排序后,第k个次序的后缀子串为【m, 原串结尾字符下标】
rank数组:给定后缀的下标,返回其字典序,rank[s[k]] = k, 即绑定 后缀下标 —— 字典序
作用:匹配子串
子串一定是某个后缀的前缀,子串匹配时间复杂度O(N * log M), 根据字典序二分查找,比较子串是否为某个后缀数组的前缀
实现代码如下:
#include
using namespace std;
struct Suff{
string str;
int index;
Suff(){}
Suff(string s, int x):str(s), index(x) {}
string toString(){
string s = "Suff{ str=\'" + str + "\', \t index=";
return s + to_string(index) + "}";
}
Suff& operator=(const Suff s){
this->str = s.str;
this->index = s.index;
return (*this);
}
Suff& operator=(const Suff *s){
this->str = s->str;
this->index = s->index;
return (*this);
}
};
/*
直接对所有后缀子串按照字典序排序,字符串比较O(n),
整体为N2log(N)
*/
Suff* getSuffArrays(string src){
int n = src.length();
Suff* SuffArrays = new Suff[n];
for(int i = 0; i < n; i++){
string temp = src.substr(i);
SuffArrays[i] = new Suff(temp, i);
}
sort(SuffArrays, SuffArrays + n, [&](Suff a, Suff b)-> bool{
return a.str < b.str;
});
return SuffArrays;
}
//倍增法构造后缀数组
//S串找t串
bool CompareString(string s, string t){
if(s.length() < t.length())
return false;
Suff* a = getSuffArrays(s);
int l = 0;
int r = s.length() - 1;
while(l <= r){
int mid = l + (r - l) >> 1;
Suff suff = a[mid]; // 中间后缀子串
string midstr = suff.str;
// 后缀子串与模式串比较
int compareRes;
if(midstr.length() >= t.length()){
string tp = midstr.substr(0, t.length());
compareRes = tp.compare(t);
}else
compareRes = midstr.compare(t);
// 二分查找
if(compareRes == 0){
cout << "find t! 起始位置为:" << mid << endl;
return true;
}else if(compareRes > 0){
r = mid + 1;
}else{
l = mid - 1;
}
}
return false;
}
int main(){
string src = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
string s = "GHIJKLMNO";
Suff* a = getSuffArrays(src);
for(int i = 0; i < src.length(); i++)
cout << a[i].toString() << endl;
cout << CompareString(src, s) << endl;
return 0;
}
1.翻转字符串
分析:
开辟数组空间,逆序遍历存储,最后返回 || 对string类型调用reverse函数
2.字符串按单词翻转
分析:
切分单词 + 翻转字符串(第一题)
3.字符串有无重复字符
分析:
解法1:暴力搜索判断O(N)
解法2:若字符范围确定,可使用辅助数组标记是否出现
4.字符串变形词问题: 两字符串是否含有相同的元素(包括元素数目)
分析: 排序+按位比较 || 计数数组标记元素出现次数
5.字符串简单压缩问题
分析:
遍历字符数组,相同相邻元素count++,遇到不同元素作为结束标志,将count加入字符串,对结尾字符结束作特殊处理
6.字符串字符集是否相同
分析:
标记数组 或者 set/map
7.字符串替换问题
若匹配替换的字符串为单个字符组成:遍历计算出现次数,扩容字符串,双指针从原结尾和现结尾向前遍历,重新生成新串
若匹配替换的字符串为K个相同的字符组成:前面的基础上,对匹配字符count++计数,若不一致时,只追加count % k的单个字符
若匹配替换的字符串为多个字符组成:借助对原字符串遍历进入栈,若栈顶的字符匹配完成,则先出栈,再入栈替换,否则直接入栈
8.字符串旋转词问题
分析:
原串复制一份拼接到原串后面,再字符串匹配处理
9.字符串回文串问题
分析:
翻转字符串,判断翻转字符串是否与原串相同
或中间分离双指针
10.最短摘要问题
分析:
同向双指针(滑动窗口)
未完待续