数据结构与算法笔记——字符串篇

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

 

目录

前言

一、串的结构

二、基本操作

 

三、串的匹配算法

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;
}

三、串的匹配算法

1.朴素模式匹配算法

时间复杂度: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;
}

2.RabinKarp(滚动哈希)

时间复杂度: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;
} 

3.KMP算法

//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;
}

4.字典树(前缀树)

5.后缀数组

定义:

           串的所有后缀子串的按照字典序排序后,在数组中记录后缀的起始下标。

           例如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;
}

6.后缀自动机

四、字符串的经典问题

1.翻转字符串

分析:

开辟数组空间,逆序遍历存储,最后返回 ||   对string类型调用reverse函数

2.字符串按单词翻转

分析:

切分单词 + 翻转字符串(第一题)

3.字符串有无重复字符

分析:

解法1:暴力搜索判断O(N)

解法2:若字符范围确定,可使用辅助数组标记是否出现

4.字符串变形词问题: 两字符串是否含有相同的元素(包括元素数目)

 分析: 排序+按位比较 ||  计数数组标记元素出现次数

5.字符串简单压缩问题

分析:

遍历字符数组,相同相邻元素count++,遇到不同元素作为结束标志,将count加入字符串,对结尾字符结束作特殊处理

6.字符串字符集是否相同

分析:

标记数组 或者 set/map

7.字符串替换问题

若匹配替换的字符串为单个字符组成:遍历计算出现次数,扩容字符串,双指针从原结尾和现结尾向前遍历,重新生成新串

若匹配替换的字符串为K个相同的字符组成:前面的基础上,对匹配字符count++计数,若不一致时,只追加count % k的单个字符

若匹配替换的字符串为多个字符组成:借助对原字符串遍历进入栈,若栈顶的字符匹配完成,则先出栈,再入栈替换,否则直接入栈

8.字符串旋转词问题

分析:

原串复制一份拼接到原串后面,再字符串匹配处理

9.字符串回文串问题

分析:

翻转字符串,判断翻转字符串是否与原串相同

或中间分离双指针

10.最短摘要问题

分析:

同向双指针(滑动窗口)

 


 

总结

未完待续

你可能感兴趣的:(数据结构与算法,字符串,算法,数据结构)