【C语言数据结构7】--串的实现

400 阅读5分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

一、什么是串

串就是我们常说的字符串,它同样是一个线性表。可能有人认为串就是元素为字符的线性表,但这种说法是不准确的。对于普通的线性表,它们关注的往往是单个元素,每个单独的元素都有独立的含义。比如我们用线性表存储班级成绩,那么元素类型的定义应该如下:

typedef struct{
    char num[10];	//学号
    char name[10];	//姓名
    float scores;	//分数
}

假设表中存储了下面几个个元素:

学号姓名分数
1809111001zack97.5
1809111002rudy94
1809111003alice96
1809111004atom99

现在我们拿出学号为0104两个人的数据,我们只会说是两个人的成绩,或者排名前面的两个人。而不是用一个整体来称呼它们(通常情况是这样的)。

对于串来说,则有些不一样。比如下面这个字符串:

Do not go gentle into that good night!

我们取出一部分数据:

gentle

我们可以把它称为单词,我们再取出一部分:

good night!

我们可以说它是一个句子。正是因为串各的某个部分有整体意义,在串中我们需要实现对字串模式的操作。后面会详细介绍。

二、串的表示

这里我们使用顺序存储结构来表示一个串,结构和顺序表类似:

#define MAXSIZE 20
typedef struct{
    char ch[MAXSIZE+1];
    int length;
}SString;

这里我们创建了一个长度为MAXSIZE+1的char数组。其中下标为0的元素我们不存储数据,这是为了让逻辑位置和物理位置对应。其它和顺序表则是一样的。

除了上面的表示,还可以采用和C语言本身字符串一样的表示。我们不存储长度信息,而是通过\0这个字符来表示结尾。不过这种方式获取字符串长度的算法时间复杂度是O(n)。

三、串的实现

(1)串的赋值

串的赋值非常简单,就是一个简单的循环操作:

void StringAssign(SString *S, char *str){
    int i = 0;
    //如果当前字符不是\0
    while (s[i] != '\0'){
        //将字符数组的内容赋值给串
        S->ch[i+1] = s[i];
        ++S->length;
        ++i;
    }
}

因为数组的下标是从0开始的,因此将s[i]赋值给S->ch[i+1]。

(2)串的复制

复制操作和赋值操作类似,同样是一个简单的循环,只不过将赋值内容改成了一个串:

int StringCopy(SString *S1, SString S2){
    //如果串为空,则返回0
    if (!S2.length){
        return 0;
    }
    //循环遍历S2,将S2内容依次赋值给S1
    for(int i = 1; i <= S2.length; i++){
        S1->ch[i] = S2.ch[i];
    }
    //修改被赋值串S1的长度
    S1->length = S2.length;
    return 1;
}

我们不需要在意S1原本的内容,只需要将内容依次覆盖,然后修改S1的长度即可。这样逻辑上我们已经完成了串的赋值。而物理上S1的尾部可能有其它字符,不过我们不需要在意。

(3)求长度

我们直接返回串的length成员即是长度:

int StringLength(SString S){
    return S.length;
}

(4)串比较

串的比较就是各个字符ASCII数值的比较:

int StringCompare(SString S1, SString S2){
    //遍历S1,依次比较S1和S2的每个字符
    for(int i = 1; i < S1.length; i++){
        //如果不是同一个字符
        if(S1.ch[i] != S2.ch[i]){
            //返回它们的差值
            return S1.ch[i] - S2.ch[i];
        }
    }
    //返回长度的差值
    return S1.length - S2.length;
}

在循环中,我们判断了字符是否一样。如果不一样则返回S1当前字符和S2当前字符的差。我们判断字符串是通过第一个不匹配的字符来判断的。比如下面几对:

abc    >    abd
acd    >    add

如果每个对应字符都匹配成功,则比较串的长度。这里返回的是长度的差值,如果两个串一样,那函数会返回0。如果S1“大于”S2,那函数会返回大于0的数,否则返回小于0的数。

(5)截取字串

子串就是串中任意个连续的字符组成的串,比如我们有一个串:

Do not go gentle into that good night!

下面几个都是它的子串:

Do
 not
t go gentle
good

子串必须存在与原串,而且必须连续。

截取子串的操作很简单,这里只是单纯通过下标来截取:

int SubString(SString S1, SString *S2, int pos1, int pos2){
    //如果下标不合理,则返回0
    if(pos1 < 1 || pos2 > S1.length || pos2 <= pos1){
        return 0;
    }
    //将S1被截取的内容依次赋值给S2
    for(int i = pos1, j = 1; i <= pos2; i++, j++){
        S2->ch[j] = S1.ch[i];
    }
    //修改S2的长度
    S2->length = pos2-pos1;
    return 1;
}

下面我们来单独看两个操作,定位字串和模式匹配。

四、定位字串和模式匹配

定位子串的操作就是找到子串第一次出现在原串中的位置,比如我们有下面几个子串:

Do not go gentle into that good night!
Do
not
nt

其中Do的位置为1,not的位置为4,而nt在串中出现了两次,我们用第一次出现的位置表示,即13。下面我们就来看看怎么查找字串。

(1)定位子串

定位子串的操作就是不断对比串的过程,我们最开始将原串的指针i指向串首,取i到i+len的串与子串比较(其中len是子串的长度)。如图:

在这里插入图片描述

其中红框部分就是取出来与子串比较的部分。如果与子串相等,我们就返回i作为子串在原串中的位置。如果失败,则i++,直到i+len大于原串的长度。

代码实现如下:

int IndexSubString(SString S1, SString S2){
    //用于存储原串截取的部分
    SString temp;
    InitString(&temp);
    //如果子串长度大于
    if(S1.length < S2.length){
        return 0;
    }
    //循环比较原串和子串
    for(int i = 1; i+S2.length <= S1.length; i++){
        //截取原串内容
        SubString(S1, &temp, i, i+S2.length);
        //将截取内容与子串比较
        int result = StringCompare(temp, S2);
        //如果截取内容与子串相等,则返回i的值(子串的位置)
        if(result == 0){
            return i;
        }
    }
    return 0;
}

通常定位子串的前提是子串一定能在原串中找到。而上面是考虑了子串不存在的情况。而我们无法确定子串是否能在原串中找到时做的定位操作叫模式匹配。不过模式匹配还包括了一些特殊规则的匹配、因此模式匹配的含义要更丰富。

(2)模式匹配

上面的算法我们特意截取出一个临时串用于比较,这一步其实是没必要的,这里为了让大家看代码更轻松才这样安排。不借助辅助串的代码如下:

int IndexSubString(SString S, SString T) {
    //指向被比较子串的首位置
    int k = 1;
    //分别指向原串中被比较的位置和模式串中被比较的位置
    int i = k, j = 1;
    //循环比较
    while (k <= S.length && j <= T.length){
        if(S.ch[i] == T.ch[j]){
        	//当前字符匹配成功则继续匹配
            i++;
            j++;
        }else{
            //当前字符匹配失败则将k指向下一个子串,i与k同步
            k++;
            i=k;
            j=1;
        }
    }
    //防止原串字符不够
    if(j > T.length)
        return k;
    else
        return 0;
}

k、i、j三个指针指向的位置如图所示:

在这里插入图片描述

而在循环过后我们还判断了j是否大于S2.length,这里大家可以模拟匹配下面串的过程:

abaccdo
cdoo

当我们匹配到末尾时,循环可以正常退出。但是j指针指向模式串的第一个o,这时我们算法应该返回匹配失败的信号。这就是最后的if语句的作用了。

准确来说,上面两个算法都是模式匹配算法。在串中还有一个重要的KMP算法,由于篇幅限制,KMP算法将单独写一篇文章。