C数据结构-串和数组

一,串及其运算

串(即字符串)是一种特殊的线性表,它的数据元素仅由字符组成。

串是由零个或多个字符组成的有限序列。串中任意连续个字符组成的子序列称为子串。子串的第一个字符在主串中的序号,定义为子串在主串中的位置,该位置索引从0开始。

特别的,空串是任意串的子串,任意串是自身的子串。

串的逻辑结构与线性表极为相似,区别仅在于串的数据对象约束为字符集,但操作有很大差别。

求子串,从第i位置的字符开始抽出j个字符构成一个新的串。j<=strlen(s)+1-i;

二,串的存储结构

存储串的方法也是存储线性表的一般方法。

1,顺序存储

typedef struct{
char ch[maxsize];
int len;
}string;

用一个不会出现在串中的字符作为串的终结符,放在串的尾部,表示串的结束。(C中为\0)

2,链式存储

typedef struct linknode{
char data;
struct linknode *next;
}linkstring;
linkstring *s;

链串由头指针唯一确定。但存储效率极低。

3,索引存储

在索引存储中,除了存放串值外,还要建立一个串名和串值之间对应关系的索引表。

(1)带长度的索引表

typedef struct{
char name[maxsize];  //串长
int length;
char *stadr;   //串值存入的起始地址
}lennode;

记录每个串的名字,长度和起始地址。可以由起始地址加长度来确定串名表示的串。

(2)带末指针的索引表

用一个指向串值存放的末地址的指针enadr来代替长度。

typedef struct{
char name[maxsize];
char *stadr,*enadr;
}enode;

(3)带特征位的索引表

当串值只需要一个指针域的空间就能存放时,可将串值放在stadr域中。这时需要增加一个特征位tag来指出stadr域中是指串还是指针。

typedef struct{
char name[maxsize];
int tag;
union{
  char *stadr;
  char value[4];
}uval;
}tagnode;

在串的索引存储下实现串值空间的动态分配:假设有一个大的向量表示可供分配用的连续的存储空间,用一个指针指示尚未分配存储空间的起始位置。当程序运行过程中,每产生一个新串,就从起始位置进行存储分配,同时在索引表中建立一个相应的结点,在结点中填入名字,分配到的起始地址,串长等,然后修改起始指针。

三,串运算的实现

大多数情况下串采用顺序存储,这是关于串的运算往往是通过元素的移动来实现的。

1,基本运算实现

typedef struct{
char ch[maxsize];
int len;
}string;

(1)串的连接运算(strcat)

for(i=0;i<s->len;i++)
     r->ch[i]=s->ch[i];
for(i=0;i<t->len;i++)
     r->ch[s->len+i]=t->ch[i];
r->ch[s->len+i]='\0';
r->len=s->len+t->len;

(2)求子串运算

将S中第i个字符起,抽取j个字符构成一个子串,结果存放在t中。

if(i+j-1>s->len)
     printf("超界");
else
{
    for(k=0;k<j;k++)
          t->ch[k]=s->ch[i+k-1];
    t->len=j;
    t->ch[t->len]='\0'; 

(3)子串定位

子串定位又称为串的模式匹配,是串处理中最重要的运算之一。

从目标S中查找模式为T的子串的过程称为模式匹配。

<1>朴素的模式匹配思想是:从目标S中的第一个字符开始和模式T中的第一个字符比较(用i和j分别表示S串和T串中正在比较的字符位置),若相等,则继续比较,否则从目标S的第二个字符开始重新与模式串的第一个字符比较。又称布鲁特-福斯算法。

在一趟失败的匹配中必定存在一个j使得S(i)!=T(j),T(j-1)!=S(i-1),,,,,,T(1)!=S(i-j+1),即T(1)和S的第i-j+1个字符对应。因此,新的一趟匹配开始时,i应该回溯到i-j+2的位置。

int Index(string *s,string *t){
int i=0,j=0;
while(i<s->len&&j<t->len)
{
   if(s->ch[i]==t->ch[j]
   {
      i++;
      j++;
   }
   else
   {
      i=i-j+1;   //此时回溯位置尚有疑问
      j=0;
   }
}
if(j==t->len)
    return i-t->len;
else
    return -1;
}//Index

匹配成功时j==t->len,i的值相对应于t->len的后一个位置,故返回值是i-t->len,而不是i-t->len+1;

上述算法可做改进,当某一趟匹配已失败,i值回溯时,可加入另一个判断,若i>s->len-t->len,则串中剩余子串的长度已小于模式T的长度,此时匹配不可能成功,故可直接返回-1.

也可以在i值回溯前加入判断i>s->len-t->len+j-1.    (此处尚有疑问)

采用链式存储结构写出模式匹配算法:

//只要用一个指针first,记住每一趟匹配开始时目标串起始比较结点的地址。若某一趟匹配成功,则返回first的值;若整个匹配失败,返回空指针。
linkstring *iNDEX(linkstring *s,linkstring *t){
linkstring *first,*sptr,*tptr;   //s,t是不带头结点的链串
first=s;     //first指向s起始比较地址
sptr=first;   tptr=t;
while(sptr&&tptr)
{
   if(sptr->data==tptr->data)    //继续比较后续结点的字符
   {
       sptr=sptr->next;
       tptr=tptr->next;
   }
   else      //本趟匹配失败,回溯
   {
       first=first->next;
       sptr=first;
       tptr=t;
   }
}
if(tptr==NULL)           //匹配成功
     return first;
else                   //匹配失败
     return NULL;
}

<2>改进的模式匹配算法(KMP算法)

改进之处:每当一趟匹配过程中出现字符比较不相等时,不需回溯i值,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较。

设计一个无回溯的匹配算法,关键在于匹配过程中,一旦Si与Tj比较不相等,存在有下列条件时

substr(s,i-j+1,j-1)=substr(t,1,j-1)    ;      si!=tj;

注:substr(s,i,j)求子串:从S的第i个字符开始抽取j个字符组成新串。

要立即确定右移的位数和继续比较的字符,也就是说,能确定T中哪一个字符应该和S中的Si进行比较。将这个字符记为Tk,k<j,且对于不同的j值,k值也不同。(之前回溯到i-j+1,现在不必回溯,还是和Si比较)

这个K值仅仅依赖于模式串T本身的前j个字符的组成,而与目标S无关。

一般使用next[j]表示与j对应 的k值,即若next[j]>0,表示一旦匹配过程中出现不相等Si!=Tj,可用T中的以next[j]为下标的字符与Si进行比较;若next[j]=0,则表示T中任何字符都不与Si进行比较,将T1与Si+1进行比较。

可见,对模式T而言,只要能确定next[j]的值,就可以加速匹配过程。

对next[j]性质进行分析的步骤如下:

(1)next[j]是一个整数,并且满足0<next[j]<j;

(2)匹配过程中,一旦Si!=Tj,根据next[j]的意义,用Tk(k=next[j]!=0)与Si继续比较。也可以看做将T右移j-next[j]位。

为了保证下一步比较是有效的,这时应有t1=si-k+1,t2=si-k+2,.........tk-1=si-1.

由于在si!=tj之前已有t1=si-j+1,t2=si-j+2,........,tj-1=si-1,因此上述条件等价于t1=tj-k+1,t2=tj-k+2,,,,,,,,,tk-1=tj-1,即表示k的取值范围应使得t1t2.....tj-1的首尾k-1个字符组成的子串相等,也就是说SubStr(T,1,k-1)=SubStr(T,j-k+1,k-1).

(3)为使T的右移不丢失任何匹配成功的可能,对于同时存在多个满足(2)的K值应取其中最大的值,这样移动的位数j-k最小。

(4)如果在t1t2......tj-1的首尾不存在相同的子串,即子串的长度为0,则K=1,表示一旦有tj!=si,则用t1与sj进行比较。

特殊情况,当J=1时,不能右移,可将t1与si+1进行比较,next[j]=0;若tj与si比较不相等,则可用(K=NEXT[J])tk与si比较;如果已知tj=tk,则相比较必有tk!=si;此时可根据next[j]意义再取k'=next[k]!=0,用tk'与si继续比较。所以当tj=tk时,next[j]=next[k].

下面给出next函数的定义:

next[j]=0,j=i;      max(k|0<k<j且使得t1t2.......tk-1=tj-k+1.......tj-1成立   当此集合不为空时        1 ,其他情况

模式串的next值
int index(string *s,string *t)
{
 int i=0,j=0;
while(i<s->len&&j<t->len)
{
   if((j==-1)||(s->ch[i]==t->ch[i]))
   {
         i++;     j++;
   }
   else
   { 
        j=next[j];       //模式串向右移动
    }
  }
if(j==t->len)
    return i-t->len;     //匹配成功
else
    return -1;          //匹配失败
} 

下面给出求next数组的算法:

void getnext(string *t,int *next){
int i=0,j=-1;
next[0]=-1;
while(i<t->len)
{
   if((j==-1)||t->ch[i]==t->ch[j])
   {
       ++i;
       ++j;
       next[i]=j;
    }
    else
        j=next[j];
  }
} 

快速模式匹配算法仅在模式与主串之间存在许多“部分匹配”的情况下,才显得更有效果。快速匹配的优点是指示目标串的指针不需要回溯,在整个匹配过程中,对目标串仅需从头到尾扫描一遍,这对处理从外设输入的庞大文件很有效,可以边读入边匹配,无需回头重读。

四,数组的定义和运算

三维数组可以看做其元素用二维数组定义的特殊线性表。以此类推,n维数组是由n-1维数组定义的,每个元素受到n个下标约束。

数组是由值与下标构成的有序对,结构中的数据元素都与其下标有关。

数组性质:数据元素数目确定;数据元素有相同类型;下标有上下界的约束,并且下标有序;

五,数组的顺序存储结构

一旦建立了数组,则结构中的数据元素个数和元素之间的关系就不再发生变动。

二维数组的顺序存储:以行为主序的优先存储和以列为主序的优先存储。以下标顺序为主序的优先存储和以逆下标顺序为主序的优先存储。

多维数组中,以下标顺序为主序表示先排最右的下标,从右向左直到排到最左的下标;而以逆下标顺序为主序,则表示从最左开始向右排列。

六,矩阵的压缩存储

用二维数组来描述矩阵。

压缩存储,是指对多个相同的元素只分配一个空间,对零元素不分配空间。

1,特殊矩阵

(1)对角矩阵

所有非零元都集中在以主对角线为中心的带状区域中,即除了主对角线上和主对角线相邻近的上下方以外,其余元素均为0.

最简单的情况为只在主对角线上含有非零元。

A[0]=A[0][0],A[1]=A[1][1],即A[k]与aii对应关系是k=i.

(2)三角矩阵

上三角矩阵是指矩阵的下三角(不包含对角线)中的元素均为0,上三角矩阵与之相反。

在三角矩阵中,值相同的元素可共享一个存储空间,若重复元素为0则可不分配存储空间;其余的元素共有n*(n+1)/2个。

在下三角矩阵中,对于aij元素,前面已经存放了i行,元素的总数为i*(i+1)/2,aii处在第i+1行的第j+1个元素,则其前面已存放的元素数目为i*(i+1)/2+j,也就是说,aij应是数组A的第k+1个元素,k=i*(i+1)/2+j.

A[k]与aij的对应关系为   k=i*(i+1)/2+j,i>=j;     k=n*(n+1)/2,i<j

(3)对称矩阵

在n阶方阵中,若A中元素满足aij=aji,则A是对阵矩阵。

元素关于对角线对称,因此在存储时只需存储上三角或下三角中的元素,使得对称的元素共享一个存储空间。

假设存储下三角中的元素,k=i*(i+1)/2+j,i>=j;

当i>j时,在上三角矩阵,k=j*(j+1)/2+i,i<j;

统一起来就是:k=i*(i+1)/2+j,   i=max(i,j),     j=min(i,j).

2,稀疏矩阵

含有非零元素及较多的零元素,但非零元素的分布没有任何规律。

下面仅讨论用三元组表来表示两种稀疏矩阵的压缩存储方法。

若将表示稀疏矩阵的非零元素的三元组按行优先(或列优先)的顺序排列(跳过零元素),则得到一个其结点都是三元组的线性表。我们将该线性表的顺序存储结构称为三元组表。因此,三元组表是稀疏矩阵的一种顺序存储结构。

要唯一的存储一个稀疏矩阵,还必须存储该矩阵的行数和列数。还要将非零元素的个数与三元组表存储在一起。

typedef strucct{
   int i,j;   //行号,列号
   datatype v;    //元素值
}node;    
typedef struct{ 
  int m,n,t;           //行数,列数,非零元素个数
  node data[maxsize];      //三元组表
}spmatrix;           //稀疏矩阵类型

通过行号,列号以及元素值来存储矩阵。

压缩存储中实现矩阵的运算:

将矩阵转置,将A转置成B,就是将A的三元组表中的a->data置换成b->data,再重新排列三元组 的顺序使之成为按行优先排列。

由于A的列是B的行,因此,按a->data的列序转置,所得到的转置矩阵B的三元组表必定是按行优先存放的。为了找到A的每一列中所有的非零元素,需要对三元组表从第一行起整个扫描一遍。

spmatrix *transmat(spmatrix *a){
int i,j,bno=0;
apmatrix *b;
b=(spmatrix *)malloc(sizeof(apmatrix));
b->m=a->n;
b->n=a->m;
b->t=0;
if(a->t==0)     return b;
for(i=0;i<a->n;i++)    //遍历列
{
    for(j=0;j<a->t;j++)
    {
        if(a->data[j].j==i)
        {
            b->data[bno].i=a->data[j].j;           
            b->data[bno].j=a->data[j].i;           
            b->data[bno].v=a->data[j].v;
            bno++;
        }
     }
}
b->t=bno;
return b;
} 

猜你喜欢

转载自blog.csdn.net/shen_zhu/article/details/79616022