第四章 串、数组和广义表
本章对串、数组和广义表这几类特殊的线性表进行讨论,也可看做为线性表的扩充。串的特殊性体现在数据元素是一个字符,即是内容受限的线性表,数组与广义表的特殊性为线性表的数据元素自身又是一个数据结构。
对于应试,本章内容较为容易掌握,相对于其他章节所占比分较少,往往以选择或填空题出现,对算法题目主要出现在对BF算法和KMP算法理解上。
【考点】①串的重点考点为串的模式匹配算法;
②数组的主要考点为数组下标与存储地址计算和特殊矩阵的压缩存储方法;
③广义表的定义、性质及其GetHead和GetTail的操作。
【本章大纲】
【目录】
一、串
1.1 串的相关概念
【定义】由零个或多个字符组成的有限序列, 一般记为 s= "a1 a2 … an" (n≥O)
【串名】s就是串的名字。
【串值】由双引号括起来的字符序列就是串的值
【串长】串中字符的数目n即为串长
【空串】零个字符的串,其长度为零。注意空格串与空串的区别。
【子串】串中任意个连续的字符组成的子序列称为该串的子串。
【主串】包含子串的串相应地称为主串。
1.2 串的模式匹配算法
串的模式匹配也称为子串定位运算或串匹配,其目的在于确定主串中所含子串第一次出现的位置(定位)。在串匹配中,一般将主串称为目标串,子串称之为模式串。
同时,模式匹配不一定是从主串的第一个位置开始, 可以指定主串中查找的起始位置 pos。该算法主要分为两种:①BF算法(又称穷尽算法或暴力算法); ②KMP算法(拥有速度快的特点)。
1.2.1 BF算法(⭐⭐)
【算法思想】BF算法作为暴力算法,其特点为未比较失败,当出现不匹配时,主串返回到模式后移一位字符进行比较。具体过程如下:
①分别利用计数指针i 和j指示主串S和模式T中当前正待比较的字符位置,初值为pos, j初值为1。
②如果两个串均未比较到串尾, 即i和j均分别小于等于S和T的长度时,则循环执行以下 操作:
● S[i].ch和T[i].ch比较,若相等,则i和j分别指示串中下个位置,继续比较后续字符;
●若不等,指针后退重新开始匹配, 从主串的下一个字符 (i=i-j+2) 起再重新和模式的第一个字符 (j=1) 比较。
③如果j> T.length, 说明模式T中的每个字符依次和主串S中的一个连续的字符序列相等, 则匹配成功,返回和模式T中第一个字符相等的字符在主串S中的序号(i-T.length); 否则称匹配不成功,返回0。
【算法分析】 如下例:主串为“000000000002”,子串为“002”,i=1开始比较,每趟都比较3次后发现不相等,指针都要回溯。
因此,若n为主串长度,m为模式串长度, 最坏情况是主串前面n-m个位置都部分匹配到子串的最后一位,即这n-m位各比较了m次,最后m位也各比较了1次,总次数为:(n-m)*m+m=(n-m+1)*m,若m<<n,则算法复杂度O(n*m)。
【算法描述】
int Index(Sstring S,Sstring T,int pos){
i=pos; j=1; //主串从第pos位置开始
while (i<=S.length && j <=T.length){
if (S[i]==T[j]) { //相同时继续后移比较
++i;
++j; }
else{ //不相等,指针回溯比较,回宿到主串的下一个字符
i=i-j+2;
j=1; }
}
if ( j>T.length)
return i-T.length; //返回字串位置
else
return 0;
}
1.2.2 KMP算法(难)
【算法思想】
在BF算法中每次失败都是从模式后移一位再从头开始比较,这时就产生一个问题,某趟已匹配相等的字符序列是模式的某个前缀,这种重复比较相当于模式串在不断地进行自我比较,这种自我比较是非常低效的。
KMP算法就是为解决此问题应运而生,其改进在千:每当一趟匹配过程中出现字符比较不等时,不需回溯i指针,而是利用已经得到的‘部分匹配’的结果将模式向右‘滑动’尽可能远的一段距离后,继续进行比较。即主串i指针无须回溯,并从该位置开始继续比较,而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。
如下图的例子中,在BF算法思想中,在第三趟的匹配中,当i=7、j=5字符比较不等时,又从i=4、j=1重新开始比较。然后,经仔细观察可发现,i=4和j=1, i=5和j=1,以及i=6和j=1这3次比较都是不必进行的。因为从第三趟部分匹配的结果就可得出,主串中第4个、第5个和第6个字符必然是“b"、 “c”和“a”(即模式串中第2个、第3个和第4个字符)。
又因模式中的第一个字符是“a”, 因此它无需再和这3个字符进行比较,而仅需将模式向右滑动3个字符的位置继续进行i=7、j=2时的字符比较即可。
同理,在第一趟匹配中出现字符不等时,仪需将模式向右移动两个字符的位置继续进行i=3、j=1时的字符比较。由此,在整个匹配的过程中,i指针没有回溯,如图4.5所示。
【前缀后缀】①前缀:除去最后一个字符以外的子串
②后缀:除去第一个字符以外的尾部子串
③部分匹配值:前缀和后缀的最长相等前后缀长度。
eg:在字符串S={abab}中,前缀为{a}、{ab}、{abc},后缀为{b}、{ab}、{bab},其最长相等前后缀长度为2(即部分匹配值为2)。
【next数组】根据上面的理论,不禁让人想到在比较不相同时,主串中第 i个字符应与模式中哪个字符再比较?此时就引出了next[j]函数来指导正确的右滑位置,计算当模式串中第j个字符与主串中相应字符“失配”时,在模式中需重新和主串中该字符进行比较的字符的位置 k 的值。next数组确定方法:①通过最长前后缀值确定;
②将前一位元素与其next值所对应的字符进行比较
相等:前一位元素的next值+1,即为此字符的next值;
不相等:继续向前寻找与next值对应的元素进行比较,直到某一位置的next值所对应的元素与此前一位字符相同,则此字符的next值为这个位置的next+1;若前移至第一位都没有相同的,则将其next=1。<注意>初始状态下,默认next[1]=0,next[2]=1。
(关于KMP算法不太好理解,具体计算过程例题可参考练习题中例子)
【算法优化】
前面定义的next 函数在某些情况下尚有缺陷;例如模式"aaaab" 在和主串"aaabaaaab"匹配 时,当i=4、j=4 时s.ch[4]≠t.ch[4],由next[j]的指示还需进行i=4、j=3, i=4、j=2,i=4、j=l这3次比较。实际上,因为模式中第1~3个字符和第4个字符都相等,因此不需要再和主串中第4个字符相比较,而可以将模式连续向右滑动4个字符的位置直接进行i=5、j=l时的字符比较。
由上述定义可得,当next[j] = k,而pj=pk,则 主串中si和j不等时,不需再和pk进行比较,而直接和pnext[k]进行比较。因此,产生了对next数组值进行修正的nextval[ ]数组,其主要计算方法是将next值与其所对应字符进行比较:
①若相等,nextval[ ]值=原next对应的值
②若不相等,nextval[ ]值=前一个相等元素的next所对应的值,即为next[ j ]=next[.. next[ j ]..]
【算法分析】
设主串s的长度为n,模式串t长度为m,在KMP算法中求next数组的时间复杂度为O(m),在后面的匹配中因主串s的下标不减即不回溯,比较次数可记为n,所以KMP算法总的时间复杂度为O(n+m)。
【算法描述】(统考不要求KMP算法代码,但是通过代码更好理解KMP算法的思想)
/*next数组求法*/
void get_next(SString T, int &next[])
{
i= 1; next[1]=0;j=0; //初始化
while(i<T[0]){
if(j==0||T[i]==T[j]){
++i; ++j;
next[i]=j; //若pi=pj,则next[j+1]=next[j]+1
}
else
j=next[j]; //若pi≠pj,继续循环
}
}
/*KMP算法*/
int Index_KMP (SString S,SString T, int pos)
{
i= pos,j =1;
while (i<S[0]&&j<T[0]) {
if (j==0||S[i]==T[j]){ //第一个位置匹配失败
i++;j++;
}
else
j=next[j]; //i不变,j后退
}
if (j>T[0]) return i-T[0]; //匹配成功
else return 0; //返回不匹配标志
}
/*nextval数组求法*/
void get_nextval(SString T, int &nextval[])
{
i=1;nextval[1]=0;j=0;
while(i<T[0]){
if(j==0||T[i]==T[j]){
++i; ++j;
if(T[i]!=T[j])
nextval[i]=j;
else
nextval[i]=nextval[j];
}
else j=nextval[j];
}
}
二、数组
2.1 一维数组
一维数组可以看成是一个线性表,如图所示,假设数组起始地址为a(A[0]),则求第i个位置的地址为LOC(i) = LOC(i-1) = a+i*L(L指每个存储单元的大小)
2.2 二维数组
二维数组分为以行序优先进行存储和以列序优先进行存储,①行序优先表示:主要存储过程如下图所示
设数组开始存放位置Loc(0,0) =a ,每个数组元素所占存储单元为L,二维数组的行下标范围为[0,n],列下标为[0,m],则求某一数组元素的开始存储位置为Loc(i, j) = a+ [ i*(m +1)+j ]*L
②以列序优先存储:主要存储过程如下图所示
设数组开始存放位置Loc(0,0) =a ,每个数组元素所占存储单元为L,二维数组的行下标范围为[0,n],列下标为[0,m],则求某一数组元素的开始存储位置为Loc(I,j) = a+[ j*(n+1)+I ]*L。
2.3 三维数组
设数组开始存放位置Loc(0,0,0) =a ,每个维度存储元素个数为m1, m2, m3,则求某一数组元素的开始存储位置为Loc( i1, i2, i3 ) = a+i1*m2*m3+i2* m3+i3
2.4 矩阵的压缩存储
压缩存储的主要目的是若多个数据元素的值都相同,则只分配一个元素值的存储空间,且零元素不占存储空间,依次来减少空间的浪费。
2.4.1 对称矩阵
【特点】在n´n的矩阵a中,满足如下aij=aji (1 £ i, j £ n),被称为对称矩阵。
【存储】只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。
【规律】求位置aji的位置k,满足以下规律:
2.4.2 三角矩阵
【特点】对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c。
【存储】重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素空间.
【规律】①上三角矩阵(行优先存储)中求位置aji的位置k,满足以下规律:
②下三角矩阵(行优先存储)中求位置aji的位置k,满足以下规律:
2.4.3 对角矩阵(带状矩阵)
【特点】在n´n的方阵中,非零元素集中在主对角线及其两侧共L(奇数)条对角线的带状区域内 — L对角矩阵。
【存储方式】以对角线的顺序存储。
【规律】求位置aji的位置k,满足以下规律:k=(i1+2)*n+j1=(i-j+2)*n+j
2.4.4 稀疏矩阵
【特点】一般情况下,将存储非零元素的个数较少的矩阵称为稀疏矩阵。
【存储方式】只记录每一非零元素(i,j,aij ),以此来节省空间,同时也会丧失随机存取功能。
①对于顺序存储常使用三元组表方式。三元组的表示方式(行标,列标,值);
②对于链式存储常使用十字链表(正交链表法)
三、广义表
3.1 广义表的相关概念
【定义】广义表:n ( ³ 0 )个表元素组成的有限序列, 记作LS = (a0, a1, a2, …, an-1)。
【表名】LS是表名。
【表元素】ai是表元素,它可以是表 (称为子表),可以是数据元素(称为原子)。
【表长】n为表的长度。
【空表】n = 0 的广义表为空表。
【深度】广义表中括号嵌套的最大层数。
【注意】习惯上用大写字母表示广义表的名称,用小写字母表示原子。
3.2 广义表与线性表的区别
①线性表的元素都是结构上不可分的单元素;
②广义表的元素可以是单元素,也可以是有结构的表;
③广义表不一定是线性表。
3.3 广义表的基本运算
3.3.1 求表头、表尾方法
①求表头GetHead(L):非空广义表的第一个元素,可以是一个原子,也可以是一个子表;
②求表尾GetTail(L):非空广义表除去表头元素以外其它元素所构成的表。表尾一定是一个表。
3.3.2 例题
A=()---A是一个空表,其长度为0;
B=(e)---B只有一个原子,其长度为1,表头为e,表尾为空表();
C=(a,(b,c,d))---长度为2,表头是原子a,表尾是子表((b,c,d));
D=(A,B,C)---其长度为3,表头是A,表尾是子表(B,C);
E=(a,E)---递归表,长度为2,表头是原子a,表尾是子表(E)。
3.4 广义表的特点
①有次序性:一个直接前驱和一个直接后继;
②有长度: 表中元素个数;
③有深度:表中括号的重数;
④可递归:自己可以作为自己的子表;
⑤可共享:可以为其他广义表所共享。