算法与数据结构——串

定义和实现

定义

串:即字符串,是有零个或多个字符组成的有限序列。一般记为
$$
S = 'a1a2a3...an'
$$
其中,S是串名,单引号括起来的字符序列是串的值;ai可以是字母、数字或其它字符。

串中字符的个数称为串的长度。

n=0时表示为空串。

子串:串中任意个连续的字符组成的子序列。

空串也是该字符串的子串

主串:包含字串的串

字符在主串中的位置:字符在串中的序号(第一次出现)

字串在主串中的位置:字串的第一个字符在主串中的位置

串是一种特殊的线性表,数据元素之间呈线性关系。

串的数据对象限定为字符集(如中文字符、英文字符、数字字符和标点字符等)

串的基本操作,如增删改查等通常以字串为操作对象

串的存储结构

定长顺序存储表示

// 静态数组实现(定长顺序存储)
#define MAXLEN 255
typedef struct {
    // 每个分量存储一个字符
    char ch[MAXLEN];
    // 串的实际大小
    int length;
}SString;

堆分配存储表示

堆分配存储表示仍然以一组地址连续的存储单元存放串值的字符序列,但它们的存储空间是在程序执行过程中动态分配的得到的。

typedef struct {
    char *ch;
    int length;
}HString;

需要使用malloc()和free()函数来完成动态存储管理。

HString s;
s.ch = (char *) malloc(MAXLEN * sizeof(char));
s.length = 0;

free(s.ch);

块链存储表示

typedef struct StringNode {
    char ch;
    struct StringNode *next;
}StringNode, *String;

​ 如果每一个结点只存放一个字符,一个字符占用的空间为1B,而每个指针需要占用4B。这种情况会造成存储密度低的问题。

​ 所以,采用下面的方式,每一个结点存放多个地府。

typedef struct StringNode {
    char ch[4];
    struct StringNode *next;
}StringNode, *String;

​ 需要注意的是,如果最后一个结点占不满时通常用#补上。

串的基本操作

// 赋值操作。把串T赋值为chars
StrAssign(&T, chars);
// 赋值操作。由串S复制得到串T
StrCopy(&T, S);
// 判空操作。若S为空串,则返回True,否则返回False
StrEmpty(S);
// 求串长。返回串S的元素个数
StrLength(&S);
// 清空操作。将S清为空串
ClearString(&S);
// 销毁串。将串S销毁(回收存储空间)
DestroyString(&S);
// 串联结。用T返回由S1和S2连接而成的新串
Concat(&T, S1, S2);
// 求子串。用Sub返回串S的第pos个字符起长度为len的子串
SubString(&Sub, S, pos, len);
// 定位操作。若主串S中存在与T值相同的子串,则返回它在主串S中第一次出现的位置,否则返回0
Index(S, T);
// 比较操作。若S>T,则返回>0;若S=T,则返回值=0;若S < T,则返回值<0
StrCompare(S, T);

求子串

bool SubString(SString &Sub, SString S, int pos, int len) {
    // 判断字串范围越界
    if (pos + len -1 > S.length)
        return false;
    for(int i=pos;i <pos+len; i++) {
        Sub.ch[i-pos+1] = S.ch[i];
    }
    Sub.length = len;
    return true;
}

比较串大小

// 比较操作
int StrCompare(SString S, SString T) {
    for(int i = 1; i <= S.length && i <= T.length; i++) {
        if (S.ch[i] != T.ch[i]) {
            return S.ch[i] - T.ch[i];
        }
    }
    // 扫描过的所有字符都相同,则长度长的串更大
    return S.length - T.length;
}

定位操作

int Index(SString S, SString T) {
    int i = 1;
    int n = StrLength(S);
    int m = StrLength(T);
    // 用于暂存子串
    SString sub;
    while(i < n-m+1) {
        SubString(sub, S, i, m);
        if(StrCompare(sub, T) != 0)
            ++i;
        else
            return i;
    }
    return 0;
}

串的模式匹配

简单的模式匹配算法

子串的定位操作通常称为串的模式匹配

int Index(SString S, SString T) {
    int i = 1, j = 1;
    while (i <= S.length && j <= T.length) {
        if(S.ch[i] == T.ch[i]) {
            // 继续比较后面的字符
            i++;
            j++;
        }
        else {
            // 指针后退重新开始匹配
            i = i - j + 2;
            j = 1;
        }
    }
    if (j > T.length)
        return i - T.length;
    else
        return 0;
}

较好的情况:每个子串的第一个字符就与模式串不匹配

若模式串长度为m,模式串长度为n

匹配成功的最好时间复杂度:O(m)

匹配失败的最好时间复杂度:O(n - m +1) = O(n - m) = O(n)

最坏的时间复杂度: O(nm)

KMP算法

朴素模式匹配算法存在一个缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i经常回溯,导致时间开销增加。

改进思路

主串指针不回溯,只有模式串指针回溯。

KMP是三位大牛:D.E.KnuthJ.H.MorrisV.R.Pratt同时发现的。

posted @ 2022-06-15 18:30  Gazikel  阅读(138)  评论(0编辑  收藏  举报