gitstats/_posts/2020-06-15-datastruct-strin...

13 KiB
Raw Blame History

layout title date author header-style tags
post 大话数据结构:串 2020-06-15 10:01:08 Viletyy text
数据结构

string是由零个或多个字符组成的有限序列又名叫字符串。

一般记为s="a1a2...an"(n>=0)其中s是串的名称用双引号有些地方也用作单引号括起来的字符序列是串的值注意引号不属于串的内容。ai(1<=1<=n)可以是字母、数字或其它字符i就是该字符在串中的位置。串中字符数目n称为串的长度定义中谈到“有限”是指长度n是一个有限的数值。零个字符的串称为空串null string),它的长度为零,可以直接用两双引号““””表示,也可以用字母𝛷来表示。所谓的序列,说明串的相邻字符之间具有前驱和后继的关系。

空格串,是只包含空格的串。注意它与空串的区别,空格串是有内容有长度的,而且可以不止一个空格。

子串与主串,串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。子串在主串中的位置就是子串的第一个字符在主串中的序号。

串的比较

给定两个串s="a1a2...an", t="b1b2...bn",当满足以下条件之一时s<t

  1. n<m, 且ai=bi(i=1, 2, ..., n)
  2. 存在某个k <= min(m, n), 使得ai=bi(i=1, 2, ..., k-1), ak<bk。

串的抽象数据类型

ADT (string)
Data 
    串中元素仅由一个字符组成,相邻元素具有前驱和后继关系。
Operation
    StrAssign(T, *chars): 生成一个其值等于字符串常量chars的串T
    StrCopy(T, S): S存在,由串S复制得串T.
    ClearString(S): S存在,将串清空。
    StringEmpty(S): 若串S为空,返回true,否则返回false
    StrLength(S): 返回串S的元素个数,即串的长度。
    StrCompare(S, T): S>T,返回值>0,S=T,返回0,若S<T,返回值<0.
    Concat(T,S1,S2): T返回由S1和S2联接而成的新串
    SubString(Sub,S,pos,len): S存在1<=pos<=StrLength(S)0<=len<=StrLength(S)-pos+1,Sub返回串S的第pos个字符起长度为len的子串
    Index(S,T,pos): S和T存在T是非空串1<=pos<=StrLength(S)。若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0.
    Replace(S,T,V): ST和V存在T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串
    StrInsert(S,pos,T): S和T存在1<=pos<=StrLength(S)+1。在串S的第pos个字符之前插入串T
    StrDelete(S,pos,len): S存在1<=pos<=StrLength(S)-len+1。从串S中删除第pos个字符起长度为len的子串

endADT

操作Index

/*T为非空串。若主串S中第pos个字符之后存在与T相等的子串 则返回第一个这样的子串在S中的位置否则返回0*/
int Index(String S, String T, int pos)
{
    int n, m, i;
    String sub;
    if (pos > 0 )
    {
        n = StrLength(S);   /*得到主串S的长度*/
        m = StrLength(T);   /*得到子串T的长度*/
        i = pos;
        while (i <= n-m+1)
        {
            SubString(sub, S, i, m); /*取主串第i个位置长度与T相等子串给sub*/
            if (StrCompare(sub, T) != 0) /*如果两串不相等*/
                ++i;
            else
                return i; /*如果两串相等则返回i值*/
        }
    }
    return 0; /*若无子串与T相等返回0*/
}

串的顺序存储结构

串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。

既然是定长数组就存在一个与定义的最大串长度一般可以将实际的串长度值保存在数组0下标位置有的书中也会定义存储在数组的最后一个下标位置。但也有些编程语言不想这么干觉得存个数字占个空间麻烦。它规定在串值后面加一个不计入串长度的结束标记字符比如"\0"来表示串值的终结,这个时候,你要向知道此时的串长度,就需要遍历计算一下才知道了,其实这还是需要占用一个空间,何必呢。

刚才讲的串的顺序存储方式其实是有问题的因为字符串的操作比如两串的连接Concat新串的插入StrInsert以及字符串的替换Replace都有可能使得串序列的长度超过了数组的长度MaxSize

串的链式存储结构

对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每个元素数据是一个字符,如果用简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。因此,一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未被占满时,可以用“#”或者其它非串值字符补全。

朴素的模式匹配算法

找一个单词在一篇文章中的定位问题。这种子串的定位操作通常称作串的模式匹配。

从主串S="goodgoogle"中找到T="google"这个子串的位置。步骤如下:

  1. 主串S第一位开始S与T前三个字母都匹配成功但S第四个字母是d而T的是g。第一位匹配失败。
  2. 主串S第二位开始主串S首字母是o要匹配的T首字母是g匹配失败
  3. 主串S第三位开始主串S首字母是o要匹配的T首字母是g匹配失败
  4. 主串S第四位开始主串S首字母是d要匹配的T首字母是g匹配失败
  5. 主串S第五位开始S与T6个字母全匹配匹配成功。

简单的说就是对主串的每一个字符做为子串开头与要匹配的字符串进行匹配。对主串做大循环对每个字符开头做T的长度小循环知道匹配成功或全部遍历完成为止。

用基本的数组来实现该算法

/*返回子串T在主串S中第pos个字符之后的位置。若不存在则函数返回值为0*/
/*T非空1<=pos<=StrLength(S)*/
int Index(String S, String T, int pos) 
{
    int i = pos; /*i用于主串S中当前位置下标若pos不为1则从pos位置开始匹配*/
    int j = 1; /*j用于子串T中当前位置下标值*/
    while (i <= S[0] && j <= T[0])
    {
        if (S[i] == T[j]) /*两字母相等则继续*/
        {
            ++i;
            ++j;
        }
        else     /*指针后退重新开始匹配*/
        {
            i = i-j+2; /*i退回到上次匹配首位的下一位*/
            j = 1; /*j退回到子串T的首位*/
        }
    }
    if (j > T[0])
        return i-T[0];
    else 
        return 0;
}

KMP模式匹配算法

在很多年前我们的科学家们觉得像这种有多个0和1重复字符的字符串却要挨个遍历的算法是非常糟糕的事情。于是有三位前辈D.E.Knuth、J.H.Morris和V.R.Pratt其中Knuth和Pratt共同研究Morris独立研究发表一个模式匹配算法可以大大避免重复遍历的情况我们把它称之为克努特-莫里斯-普拉特算法简称KMP算法。

next数组值推导

image-20210628192152671

具体如何推导出一个串的next数组值呢。

T="abcdex"

  1. 当j=1时next[1]=0
  2. 当j=2时j由1到j-1就只有字符“a",属于其它情况next[2]=1;
  3. 当j=3时j由1到j-1串是“ab”显然“a"与“b"不相等属其它情况next[3]=1;
  4. 以后同理所以最终此T串的next[j]为011111

T="abcabx"

  1. 当j=1时next[1]=0;
  2. 当j=2时同上例说明next[2]=2;
  3. 当j=3时同上next[3]=1;
  4. 当j=4时同上next[4]=1;
  5. 当j=5时此时j由1到j-1的串是"abca"前缀字符“a"与后缀字符“a“相等因此可推算出k的值为2p1..pk-1'='pj-k+1...pj-1',得到p1=p4)因此next[5]=2;
  6. 当j=6时j由1到j-1的串是”abcab“由于前缀字符”ab“与后缀”ab“相等所以next[6]=3。

我们可以根据经验得到如果钱后缀一个字符相等k值是2两个字符k值是3

T="ababaaaba"

  1. 当j=1时next[1]=0;
  2. 当j=2时同上next[2] = 1;
  3. 当j=3时同上next[3]=1;
  4. 当j=4时j由1到j-1到串是“aba"前缀字符“a”与后缀字符“a”相等next[4]=2;
  5. 当j=5时j由1到j-1的串是“abab”由于前缀字符“ab”与后缀“ab”相等所以next[5]=3;
  6. 当j=6时j由1到j-1的串是“ababa”由于前缀字符“aba”与后缀“aba”相等所以next[6]=4;
  7. 当j=7时j由1到j-1的串是“ababaa”由于前缀字符“ab”与后缀“aa”并不相等只有“a”相等所以next[7]=2;
  8. 当j=8时j由1到j-1的串是“ababaaa”只有“a”相等所以next[8]=2;
  9. 当j=9时j由1到j-1到串是“ababaaab”由于前缀字符“ab”与后缀“ab”相等所以next[9]=3。

T="aaaaaaaab"

  1. 当j=1时next[1]=0;
  2. 当j=2时同上next[2]=1;
  3. 当j=3时j由1到j-1的串是“aa”前缀字符“a”与后缀字符“a”相等next[3]=2;
  4. 当j=4时j由1到j-1的串是“aaa”由于前缀字符“aa”与后缀“aa”相等所以next[4] =3;
  5. ......
  6. 当j=9时j由1到j-1的串是“aaaaaaaa”由于前缀字符“aaaaaaa”与后缀“aaaaaaa”相等所以next[9]=8。

KMP模式匹配算法实现

/*通过计算返回子串T的next数组*/
void get_next(String T, int *next)
{
  int i,j;
  i = 1;
  j = 0;
  next[1]=0;
  while (i<T[0])  /*此处T[0]表示串T的长度*/
  {
    if (j == 0 || T[i] == T[j]) /*T[i]表示后缀的单个字符T[j]表示前缀的单个字符*/
    {
      ++i;
      ++j;
      next[i] = j;
    }
    else 
      	j = next[j]; /*若字符不相同则j值回溯*/
  }
}
/*返回子串T在主串S中第pos个字符之后的位置。若不存在则函数返回值为0。*/
/*T非空1<=pos<=StrLength(S)*/
int Index_KMP(String S, String T, int pos)
{
  int i = pos; /*i用于主串S当前位置下标值若pos不为1则从pos位置开始匹配*/
  int j = 1; /*j用于子串T中当前位置下标值*/
  int next[255]; /*定义一next数组*/
  get_next(T, next); /*对串T作分析得到next数组*/
  while (i <= S[0] && j <=T[0]) /*若i小于S的长度且j小于T的长度时循环继续*/
  {
    if (j==0 || S[i] == S[j]) /*两字母相等则继续与朴素算法增加了j=0判断*/
    {
      ++i;
      ++j;
    }
    else   /*指针后退重新开始匹配*/
    {
      j = next[j]; /*j退回合适的位置i值不变*/
    }
  }
  if (j > T[0]) 
  	return i-T[0];
  else
    return 0;
}

KMP模式匹配算法改进

/*求模式串T的next函数修正值并存入数组nextval*/
void get_nextval(String T, int *nextval)
{
  int i, j;
  i=1;
  j=0;
  nextval[1]=0;
  while (i<T[0]) /*此处T[0]表示串T的长度*/
  {
    if (j == 0 || T[i] == T[j]) /*T[i]表示后缀的单个字符T[j]表示前缀的单个字符*/
    {
      ++i;
      ++j;
      if (T[i]!=T[j]) /*若当前字符与前缀字符不同则当前的j为nextval在i位置的值*/
        nextval[i] = j;
      else
        nextval[i] = nextval[j]; /*如果与前缀字符相同则将前缀字符的nextval值赋值给nextval在i位置的值*/
    }
    else
      j = nextval[j]; /*若字符不相同则j值回溯*/
  }
}

nextval数组值推导

T="ababaaaba"

先算出next数组的值分别为001234223然后再分别判断

  1. 当j=1时nextval[1]=0;
  2. 当j=2时因第二位字符“b”的next值是1而第一位就是“a”它们不相等所以nextval[2] = next[2] = 1,维持原值
  3. 当j=3时因第三位字符"a"的next值为1所以与第一位的“a”比较得知它们相等所以nextval[3] = nextval[1] = 0;
  4. 当j=4时第四位的字符"b"next值为2所以与第二位的“b”相比较得到的结果时相等的因此nextval[4] = nextval[2] = 1;
  5. 当j=5时next值为3第五个字符“a”与第三个字符“a”相等因此nextval[5] = nextval[3] = 0;
  6. 当j=6时next值为4第六个字符“a”与第四个字符“b”不相等因此nextval[6] = 4
  7. 当j=7时next值为2第七个字符“a”与第二个字符“b”不相等因此nextval[7] = 2
  8. 当j=8时next值为2第八个字符“b”与第二个字符“b”相等因此nextval[8] = nextval[2] = 1;
  9. 当j=9时next值为3第九个字符“a”与第三个字符“a”相等因此nextval[9] = nextval[3] = 1。

T="aaaaaaaab"

先算出next数组的值分别为012345678然后再分别判断

  1. 当j=1时nextval[1] = 0;
  2. 当j=2时next值为1第二个字符与第一个字符相等所以nextval[2] = nextval[1] = 0;
  3. 同样的道理其后都为0.......
  4. 当j=9时next值为8第九个字符"b"与第八个字符"a"不相等所以nextval[9] = 8。

总结改进过的KMP算法它是在计算出next值的同时如果a位字符与它next值指向的b位字符相等则该a位的nextbal就指向b位的nextval值如果不等则该a位的nextval值就是它自己a位的next的值。

参考资料:

  • 大话数据结构