大师兄的数据结构学习笔记(十五): 串

大师兄的数据结构学习笔记(十四): 词典
大师兄的数据结构学习笔记(十六): 排序

一、关于串

1. 什么是串
  • 字符串(String),是由n个字符组成集合(n\geq0), 记作:S="a_0,a_1 ... a_{n-1}",其中a_i\in\Sigma,0\leq i<n
  • 字符可以由字母、数字或者其他字符组成。
  • \Sigma为字符表(Alphabet),是所有可用字符的集合。
  • n为字符串S所含字符串的总数,成为S的长度,记作|S|=n,|n<\infty|
2.特殊串
2.1 空串
  • 长度为0的串称为空串(null string),通常用Ø 表示。
2.2 子串
  • 字符串中任一连续的片段,称为子串(substring),记作S.substr(i,k) = "a_1,a_{i+1}...a_{i+k-1} = S[i,i+k)
  • 起始位置为0,长度为k的子串称为前缀(prefix),记作prefix(S,k)=S.substr(0,k)=S[0,k)
  • 终止与位置n-1,长度为k的子串称为后缀(suffix),记作suffix(S,k)=S.substr(n-1,k)=S[n-k,n)
2.3 更多定义
  • 空串是任何字符串的子串,也是任何字符串的前缀和后缀。
  • 任何字符串都是自己的子串,也是自己的前缀和后缀,称为平凡子串(trivial substring)平凡前缀(trivial prefix)平凡后缀(trivial suffix)
  • 字符串本身之外的所有非空字符串、前缀和后缀,称为真子串(proper substring)真前缀(proper prefix)真后缀(proper suffix)
3.串的存储结构
3.1 定长顺序存储
  • 即采用静态数组存储串。
char a[5] = "abcde";
  • 此方式存储串时,需要预估串的长度提前申请足够的存储空间, 如出申请的长度则串会被截断。
3.2 动态数组存储
  • 用堆分配存储。
char* a = new char[5];
delete a[];
3.3 块链存储
  • 用链表结构存储。
4. 判等
  • 字符串S[0,n)T[0,m)称作相等,当且仅当二者长度相等(n=m),且对应的字符分别相同(对任何0\leq i <n都有S[i]=T[i])。

二、串的匹配算法

  • 串匹配(string pattern matching)基于同一字符表的任何文本串(T)和模式串(P),判定T中是否存在某一字串与P相同。
  • 若匹配,则返回该字符串在T中的起始位置。
1. 蛮力算法(Brute Force algorithm)
  • 给定一个n个字符组成的串(称为文本),一个m(m\leq n)个字符的串(称为模式)。
  • 从文本中寻找匹配模式的子串,蛮力匹配是最直接的方法,将模式串与文本串逐个比对。


  • 如果不匹配,则将模式串后移一位,并将指针回退,重新比较,直至确定可能的匹配位置。


  • 蛮力算法最多迭代n-m+1轮,且各轮至多进行m次比对,故总共需要不超过(n-m+1)\times m次比对,时间复杂度为\omicron(n\times m)
int BF_match(char* P, char* T)
{
    size_t i = 0; //模式串字符对齐位置
    size_t j;     //当前接收比对字符的位置
    size_t n = strlen(T); //文本串长度
    size_t m = strlen(P); //模式串长度
    for (i = 0; i < n - m + 1; i++) //从文本串第i个字符开始
    {
        for (j = 0; j < m; j++)  //模式串中的字符逐个比对
        {
            if (T[i + j] != P[j]) break; //若不匹配,则模式串整体右移一个字符继续匹配
        }
        if (j >= m) break; //若匹配则跳出循环
    }
    return i;
}
2. KMP算法
  • 由于蛮力算法在每一轮的m次对比中,一旦发现失配,文本串和模式串的字符指针都要回退,并从头开始下一轮尝试,在应对大型数据时效率低。
  • 因此,可以通过记录前一轮迭代中已经成功匹配的信息,通过减少重复匹配提高效率,实现仅仅后移模式串,比较指针不回退
  • 如果指针在某个位置发现不匹配。


  • 并且模式串左右两端有两个子串是完全匹配的,这两个子串称为模式串的公共前后缀
  • 公共前后缀必须是最长的真前缀和真后缀
  • 则直接移动模式串的公共前缀公共后缀所在的位置。
  • 如果匹配时模式串超出文本串,则匹配失败。



2.1 next表
  • 实际对于公共前后缀,可以只关注模式串。


  • 假如第一位不匹配,则将模式串向右移动一位,并从指针第一位开始比较。



    image.png
  • 假如第二位不匹配,由于公共长度小于1,则从指针第一位开始与主串当前位比较。



  • 假如第四位不匹配,公共模式串后移,从指针第二位开始于珠串当前位比较。



    image.png
  • 以此类推,获得全部的比较数据。


  • 将第一位标注位0,则可以获得next表。


  • 这样如果任意位置不匹配,即可对照next表快速获得指针位置。
int* buildNext(char* P)
{
    size_t m = strlen(P), j = 0; //主指针
    int* N = new int[m]; //next表
    int t = N[0] = -1; //模式串指针
    while (j < m - 1)
    {
        if (0 > t || P[j] == P[t]) //如果匹配
        {
            j++;
            t++;
            N[j] = (P[j] != P[t]?t:N[t]);
        }
        else //失配
        {
            t = N[t];
        }
    }
    return N;
}

int KMP_match(char* P, char* T)
{
    int* next = buildNext(P);
    int n = (int)strlen(T), i = 0; //文本串指针
    int m = (int)strlen(P), j = 0;//模式串指针
    while (j < m && i < n)
    {
        if (0 > j || T[i] == P[j]) //若匹配
        {
            i++;
            j++;
        }
        else
        {
            j = next[j]; //右移
        }
    }
    delete[] next; //释放next表
    return i - j;
}
3. BM算法
  • 与KMP算法类似,BM算法也需要充分利用以往的比对信息,使P向右移动尽可能远的距离。


  • BM算法的模式串P与文本串T的对准位置依旧自左向右推移,而在每一个对准位置却是自右向左地逐一比对个字符。
  • 在比对过程中一旦失配,则将P右移一段距离并再次与T对准,然后重新一轮自右向左的扫描比对。
int BM_match(char* P, char* T)
{
    int* bc = buildBC(P); //bc表
    int* gs = buildGS(P); //gs表
    size_t i = 0; //模式串相对于文本串的起始位置
    while (strlen(T) >= i + strlen(P)) 
    {
        int j = strlen(P) - 1; // 模式串最后一个字符
        while (P[j] == T[i + j]) //从右向左比对
        {
            if (0 > --j)break;
        }
        if (0 > j) {
            break;
        }
        else {
            i += __max(gs[j], j - bc[T[i + j]]); //位移量根据BC表和GS表选择最大者
        }
    }
    delete[] gs;
    delete[] bc;
    return i;
}
3.1 坏字符策略
  • 在a和b中,若P当前在文本串T中的对齐位置位i,且在这一轮自右向左将P与substr(T,i,m)的比对过程中,在P[j]处首次发现失配T[i+j] = X \neq Y = P[j],则X称为坏字符(bad character)
  • 此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置
  • 此外,如果"坏字符"不包含在模式串之中,则最右出现位置为 -1,坏字符针对的是文本串。
  • 若P与T的某哥字符串匹配,则必然在T[i+j]=X处匹配。
  • 反之,若T[i+j]=X对准的字符不是X,则必然失配。
  • 所以如c中,只需要找出P中的每一个字符X,分别与T[i+j]=X对准,并执行一轮自右向左的扫描比对。
  • 如果没有坏字符,则整体移到坏字符后方


  • 如果包含坏字符,则将bc表中的最后一个坏字符对齐。


1) bc表

  • 若P中包含多个X,则仅尝试P中最靠右的字符X。
  • 若P中最靠右的字符X为P[k]=X,则P的右移量为j-k。
  • 任一给定模式串P, k值取决于字符串T[i+j] = X,可将其看作字符表到整数的一个函数:bc(c)=\left\{\begin{aligned}k&&(若P[k]=c ,且对所有的i>k都有P[i]\neq c)\\-1&&(若P[]中不含字符c)\end{aligned}\right.
  • 故如当前对其位置为i, 一旦出现坏字符Y,则重新对齐与i += j - bc[T[i+j]
  • 预先将函数bc()整理为一份查询表,称为BC表
int* buildBC(char* P)
{
    int* bc = new int[256]; //BC表
    for (size_t j = 0; j < 256; j++)
    {
        bc[j] = -1; //初始化
    }
    for (size_t m = strlen(P), j = 0; j < m; j++)
    {
        bc[P[j]] = j; //将字符P[j]的BC项更新为j
    }
    return bc;
}
3.2 好后缀策略
  • 将每轮比对中的连续的若干次成功匹配,对应于模式串P的一个后缀,称为好后缀(good suffix)
  • 当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为 -1,好后缀针对的是模式串。
  • 如(a)(b),设自右向左的扫描适配位置为:T[i+j] = X \neq Y = P[j]
  • T中匹配的子串为:W=substr(T,i+j+1,m-j-1) = T[i+j+1,m+i)
  • 好后缀为:U=suffix(P,m-j-1)=P[j+1,m)
  • 好后缀长度为m-j-1,故当j<m-2时,U必非空且U=W
  • 设某整数k,是P右移j-k单元,并使P[k]与T[i+j]对其之后,P能与T的某一子串匹配,即P=substr(T,i+j-k,m)=T[i+j-k,m+i+j-k)
  • 若记:V(k) = substr(P,k+1,m-j-1)=P[k+1,m-j+k)V(k)=W=U.
  • 如果发现好后缀,则将目标串与好后缀对齐。



  • 如果无法找到匹配好的后缀,则找一个匹配的最长的前缀与目标串对齐。


3.2.1 gs表
  • 与bs表类似,gs表通过预处理将模式串P事先转换为一张表记录对应的位移量。
  • 其中gs[j] = j-k

1) 蛮力算法

  • 对于每个好后缀P(J,m),按照自后向前的次序(j-1>0)与P的每个字符串P(k,m_k-j)逐一对其,如果匹配,对应的位移量即为gs[j]的取值。
  • 此方法可能需要O(m^3)的时间,效率很低。

2) ss表

  • ms[j]表示在P[0,j]的所有后缀中,与P的某一后缀匹配的最长者。
  • ss[i]是表示在P[0, i]的所有后缀中,与P的某一后缀匹配的最长长度(也就是|ms[j]|),即最长匹配后缀(maximum matched suffix)的长度。


情形一:如果ss[j]对应的最长匹配后缀延伸到了P的最左侧,则有ss[j] == j + 1

  • 此时,模式串中所有的秩为i的字符,如果有i < m - j - 1,则MS[j]都是在该处匹配失败的一个候选对齐位置,对应了上面好后缀的第二种情形,此时它们的移动距离距离都是m - j - 1,即m - j -1必然是gs[i]的一个候选。(不适用于i >= m - j - 1的情形,因为首位两个子串完全匹配,在该位置对齐后的下一次匹配必然会失败。)

情形二: ss[j]P[0, j]的真后缀,此时有ss[j] < j + 1

  • 此时ms[j]只能作为在位置m - ss[j] - 1处比对失败的候选对齐位置。
int* buildSS(char* P)
{
   int m = strlen(P); //P的长度
   int* ss = new int[m]; //ss表
   for (int lo = m - 1, hi = m - 1, j = lo - 1; j >= 0; j--)
   {
       if ((lo > j) && (ss[m - hi + j - 1 <= j - lo])) //情形一
       {
           ss[j] = ss[m - hi + j - 1]; 
       }
       else
       {
           hi = j;
           lo = __min(lo, hi);
           while ((0 <= lo) && (P[lo] == P[m - hi + lo - 1]))
           {
               lo--;
           }
           ss[j] = hi - lo;
       }
   }
   return ss;
}
int* buildGS(char* P)
{
    int* ss = buildSS(P); //ss表
    size_t m = strlen(P);
    int* gs = new int[m]; //gs表
    for (size_t j = 0; j < m; j++)
    {
        gs[j] = m; //初始化
    }

    for (size_t i = 0,j = m - 1; j < UINT_MAX; j--) //逆向逐一扫描字符串P[j]
    {
        if (j + 1 == s[j])
        {
            while (i < m - j - 1)
            {
                gs[i++] = m - j - 1;
            }
        }
    }

    for (size_t j = 0; j < m - 1; j++)
    {
        gs[m - ss[j] - 1] = m - j - 1;
    }
    delete[] ss;
    return gs;
}
4. Karp-Rabin算法
  • Karp-Rabin算法对模式串进行哈希运算,并将哈希值与文本中子串的哈希值进行比对。
  • 如果哈希值比对匹配,再将哈希匹配的部分逐位比对(因为要考虑哈希碰撞)。
  • 为此,需要找到一个哈希函数 ,将字符串转换为数字。
  • 复杂度为O(mn),但在实际使用过程中的复杂度通常被认为是O(n+m)
const size_t d = 256;

void KR_match(char* P,char* T,int q)
{
    size_t m = strlen(P);
    size_t n = strlen(T);
    size_t j;
    
    int p = 0; //P的哈希值
    int t = 0; //T的哈希值
    int h = 1;

    for (size_t i = 0; i < m - 1; i++)
    {
        h = (h * d) % q;
    }

    for (size_t i = 0; i <m; i++) //计算哈希值
    {
        p = (d * p + P[i]) % q;
        t = (d * t + T[i]) % q;
    }

    for (size_t i = 0; i <= n - m; i++) //逐一比对哈希值
    {
        if (p == t) //如果哈希值相等
        {
            for (j = 0; j < m; j++)
            {
                if (T[i + j] != P[j])break;
            }

            if (j == m)
            {
                cout << i << endl;
            }
        }

        if (i < n - m)
        {
            t = (d * (t - T[i] * h) + T[i + m]) % q;
            if (t < 0)
            {
                t = t + q;
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容