数据结构-串

秋风阁——北溪入江流:https://focus-wind.com/
秋风阁——数据结构-串

串(String)的定义与实现

串的定义

串(即字符串)是一种特殊的线性表,它的数据元素仅有字符组成。
:是由零个或多个字符组成的有限序列。一般记为:
S = " a 0 a 1 ⋯ a n − 1 " ( n ≥ 0 ) S = "a_0a_1 \cdots a_{n - 1}"(n \ge 0) S="a0a1an1"(n0)
其中。 S S S是串名,单引号括起来的字符序列是串的值; a i a_i ai可以是字母、数字或其他字符;串中字符的个数 n n n称为串的长度。 n = 0 n = 0 n=0时的串称为空串(用 ∅ \varnothing 表示),不包含任何字符。在C语言中,串一般使用不可显示的字符“\0”作为串的结束符。

子串:串中任意多个连续的字符组成的子序列称为该串的子串。
主串:包含子串的串称为主串。
子串的第一个字符在主串中的序号,定义为子串在主串中的位置,该位置索引(或序号)从0开始。特别的:空串是任何串的子串,任意串是其自身的子串。

串与线性表的区别

  • 串的逻辑结构与线性表极为相似,区别仅在于串的数据对象约束为字符集。
  • 串的基本操作和线性表有很大差别。
    • 在线性表中,大多以“单个元素”作为操作对象。
    • 在串中,通常以“串的整体”(子串)作为操作对象。

串的基本操作

  • StrCopy(&Str1, Str2):复制操作,将串Str2的值赋给串变量Str1。
  • StrConcat(&Str1, Str2):串连接,将串Str2接在Str1末尾,组成一个新串Str1。
  • StrLength(Str):求串长,返回串Str的元素个数。
  • SubString(&Sub, Str, index, len):求子串,表示从Str中index位置的字符开始抽出len个字符构成一个新的串用Sub串返回。
  • StrCompare(Str1, Str2):比较操作,比较串的编码,从串的一个序号开始比较其编码,若为0比较下一个序号,直到非0或比较完毕。若返回值大于0,表示Str1 > Str2;返回值等于0,表示Str1 = Str2;返回值小于0,表示Str1 < Str2。
  • StrInsert(Str1, Str2, index):插入子串,表示把串Str2插入到Str1的index位置处。
  • StrDelete(Str, index, len):删除子串,表示删除Str串的index位置及以后的的连续共len个字符。
  • StrIndex(Str1, Str2):子串定位,若主串Str1中存在子串Str2,则返回主串中子串的起始位置,否则返回-1。

串的存储结构

定长顺序存储

串的顺序存储结构简称顺序串。顺序串的字符被依次存放在一片连续的单元中。我们可以用一个特定的、不会出现在串中的字符作为串的终结符,放在串的尾部,表示结束。在C语言中用字符“\0”作为串的结束符。

// 顺序串的最大长度
#define MaxSize 255

typedef struct {
	// 数据域
	char ch[MaxSize];
	// 串的长度
	int len;
} SeqString;

堆分配存储表示

堆分配存储仍然使用一组连续的存储单元存放串值的字符序列,但是它们的存储空间实在执行过程中动态分配的。

typedef struct {
	// 数据域,动态分配存储空间ch的地址为串的基地址
	char *ch;
	// 串的长度
	int len;
} HeapString;

C语言中,存在一个称为“堆”的自由存储区,并用molloc()和free()函数来动态存储管理。

链式存储表示

串的链式存储结构简称为链串。

typedef struct Node{
	char data;
	struct Node *next;
} LinkString;

索引存储

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

带长度的索引表
#define MaxSize 255

typedef struct{
	// 串名
	char name[MaxSize];
	// 串长
	int len;
	// 串值基地址
	char *strAdr;
} LinkString;
带末指针的索引表

带末指针的索引表用存放的末地址的指针endAdr来代替长度len

#define MaxSize 255

typedef struct{
	// 串名
	char name[MaxSize];
	// 串值基地址,末指针
	char *strAdr, *endAdr;
} LinkString;

串的基本运算

串的连接运算

/**
 * 串连接,将Str2连接在Str1并赋值给Str1
 * @param Str1 
 * @param Str2 
 */
void StrConcat(SeqString *Str1, SeqString Str2) {
    if (Str1->len + Str2.len > MaxSize) {
        // 判断两个串连接后是否溢出
        printf("上溢");
    } else {
        // 将Str2接在Str1后
        for (int i = 0; i < Str2.len; i++) {
            Str1->ch[Str1->len + i] = Str2.ch[i];
        }
        // 重置Str1长度和结束符
        Str1->len = Str1->len + Str2.len;
        Str1->ch[Str1->len] = '\0';
    }
}

求子串运算

/**
 * 求子串
 * @param Sub 子串,传入传出参数
 * @param Str 主串
 * @param index 子串在主串的下标号
 * @param len 子串长度
 */
void SubString(SeqString *Sub, SeqString Str, int index, int len) {
    if ((index + len - 1) > Str.len) {
        printf("超界");
    } else {
        for (int i = 0; i < len; i++) {
            Sub->ch[i] = Str.ch[i + index];
        }
        // 重置Str1长度和结束符
        Sub->len = len;
        Sub->ch[Sub->len] = '\0';
    }
}

串的模式匹配(子串定位)

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

设有两个串S和T, S = " s 0 s 1 ⋯ s n − 1 " S = "s_0s_1 \cdots s_{n - 1}" S="s0s1sn1" T = " t 0 t 1 ⋯ t m − 1 " T = "t_0t_1 \cdots t_{m - 1}" T="t0t1tm1",其中 0 < m ≤ n 0 < m \le n 0<mn。子串定位是在主串S中找出和子串T相同的子串。
一般把主串S称为目标;把子串T称为模式;把从目标S中查找模式为T的子串的过程称为模式匹配

朴素的模式匹配(BF算法)

朴素的模式匹配思想:从目标S中的第一个字符开始和模式T中的第一个字符比较,若相等,则继续逐个比较后续字符,否则从目标S的第二个字符开始重新与模式串的第一个字符比较,依次类推,直到模式T中的每个字符依次和目标S中的一个连续字符序列相等为止,则匹配成功,返回模式T中第一个字符在目标S中的位置。否则匹配失败,返回-1。
朴素模式匹配算法是一种不依赖于其他串的暴力匹配算法。

/**
 * 朴素模式匹配算法
 * @param Str1 主串
 * @param Str2 子串
 * @return 
 */
int SimpleIndex(SeqString Str1, SeqString Str2) {
    // i:控制主串下标,j:控制子串下标
    int i = 0, j = 0;
    // 因为i和j都是下标,所以必不小于对应的串长度
    while (i < Str1.len && j < Str2.len) {
        // 比较字符
        if (Str1.ch[i] == Str2.ch[j]) {
            // 相等,比较下一个字符
            i++;
            j++;
        } else {
            // 不想等,此时j表示本次匹配,匹配了j个字符,那i - j则主串下标回到本次子串还未开始匹配的位置
            // 令其加1,从下一个字符重新开始子串匹配,所以i = i - j + 1
            i = i - j + 1;
            // 子串回到其第一个字符下标处重新匹配
            j = 0;
        }
    }
    // 若此时j即子串的下标等于子串的长度,即为子串匹配成功,否则是主串遍历所以字符仍未找到对应子串
    if (j == Str2.len) {
        // 此时i的值对应的是匹配串后的一个位置,i - 子串长度Str2.len即为子串在主串中的索引号
        return i - Str2.len;
    } else {
        return -1;
    }
}
  • 朴素匹配算法的最好平均时间复杂度为: O ( n + m ) O(n + m) O(n+m)
  • 朴素匹配算法的最坏平均时间复杂度为: O ( n m ) O(nm) O(nm)

KMP模式匹配算法

在朴素匹配算法中,每次回溯都会回到比较字符的下一个位置,即回溯 i i i值,这其中的大部分回溯都是不必要的。
KMP算法相比朴素匹配算法的改进之处在于:每当一趟匹配过程中出现字符比较不相等时,不需回溯 i i i值,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,再进行比较。

字符串的前缀、后缀和部分匹配值

要了解子串的结构,首先要弄清:前缀、后缀和部分匹配值等概念。
前缀:除最后一个字符外,字符串的所有头部字符。
后缀:除第一个字符外,字符串的所有尾部字符。
部分匹配值:字符串的前缀和后缀的最长相等前后缀长度。
下面以“ababa”为例进行说明:

模式串的各子串前缀后缀交集部分匹配值
a ∅ \varnothing ∅ \varnothing ∅ \varnothing 0
abab ∅ \varnothing 0
abaa, aba, baa0
ababa, ab, abab, ab, babab2
ababaa, ab, aba, ababa, ba, aba, babaa, aba3

next数组求法

next数组本质上就是模式串 i i i前面的子串部分匹配值(有点教材里面是部分匹配值 + 1)。
next数组的开始两个为固定值next[0] = 0; next[1] = 1; 或者 next[0] = -1; next[1] = 0
下面以等于部分匹配值为例给出next函数定义(next[0] = -1; next[1] = 0):
y ( x ) = { − 1 j = 0 m a x { k ∣ 0 < k < j ,且使得 t 0 t 1 ⋯ t k − 1 = t j − k ⋯ t j − 1 } 当此集合不空时 0 其他情况 y(x)=\left\{ \begin{aligned} -1 && j = 0 \\ max\{k |0 < k < j,且使得t_0t_1 \cdots t_{k - 1} = t_{j - k} \cdots t_{j - 1} \} && 当此集合不空时 \\ 0 && 其他情况 \end{aligned} \right . y(x)= 1max{k∣0<k<j,且使得t0t1tk1=tjktj1}0j=0当此集合不空时其他情况
对于next[0] = 0; next[1] = 1;的情况,只需将对应值加1即可。

下面以具体示例来更好演示求模式串的next值(这里以两种方法穿插求解next值,一种是通过求解前缀后缀后求其交集来求解next(根据next值本质来求),一种是通过前缀后缀的性质来求解。)。
计算 T = " a b a a b c a c " T = "abaabcac" T="abaabcac"的next值:
0:固定给next[0]赋值为-1;

j01234567
模式abaabcac
next[j]-1???????

1:下标为1对应求解的子串为a,a的前缀和后缀皆为空,对应的部分匹配值为0,所有next[1] = 0

j01234567
模式abaabcac
next[j]-10??????

2:下标为2对应求解的子串为ab,去掉头字符后为b,因为头字符为a,所以对应匹配的后缀必以a开头,所以b不是其后缀,所有next[2] = 0

j01234567
模式abaabcac
next[j]-100?????

3:下标为3对应求解的子串为aba,aba的前缀为{a, ab},后缀为{a, ba},交集为{a},对应的部分匹配值为1,所有next[3] = 1

j01234567
模式abaabcac
next[j]-1001????

4:下标为4对应求解的子串为abaa,去掉头字符后为baa,因为头字符为a,所以匹配后缀必以a开头,baa不可能是其匹配后缀。使用aa从头字符开始对比两个字符为ab,aa != ab,所以aa不是匹配后缀;使用a和头字符对比,对比成功,长度为1,所有next[4] = 1

j01234567
模式abaabcac
next[j]-10011???

5:下标为5对应求解的子串为abaab,去掉头字符后为baab,因为头字符为a,所以匹配后缀必以a开头,baab不可能是其匹配后缀。使用aab从头字符开始对比三个字符为aba,aab != aba,所以aab不是匹配后缀;使用ab和头字符对比两个字符为ab,对比成功,长度为2,所有next[5] = 2

j01234567
模式abaabcac
next[j]-100112??

6:下标为6对应求解的子串为abaabc,abaabc的前缀为{a, ab, aba, abaa, abaab},后缀为{c, bc, abc, aabc, baabc},交集为{},对应的部分匹配值为,所有next[6] = 0

j01234567
模式abaabcac
next[j]-1001120?

7:下标为6对应求解的子串为abaabca,abaabc的前缀为{a, ab, aba, abaa, abaab, abaabca},后缀为{a, ca, bca, abca, aabca, baabca},交集为{a},对应的部分匹配值为1,所有next[7] = 1

j01234567
模式abaabcac
next[j]-10011201

next数组代码求解

/**
 * 求取next数组
 * @param Str 模式串
 * @param next next数组
 */
void GetNext(SeqString Str, int next[]) {
    int i = 0, j = -1;
    next[0] = -1;
    while (i < Str.len) {
        if (j == -1 || Str.ch[i] == Str.ch[j]) {
            ++i;
            ++j;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
}

KMP算法的代码

KMP算法的代码和朴素模式匹配算法代码基本类似,主要区别在于回溯的移动的区别。

/**
 * kmp模式匹配算法
 * @param Str1 
 * @param Str2 
 * @return 
 */
int KmpIndex(SeqString Str1, SeqString Str2) {
    // 求解next数组
    int next[Str2.len];
    GetNext(Str2, next);
    // i:控制主串下标,j:控制子串下标
    int i = 0, j = 0;
    // 因为i和j都是下标,所以必不小于对应的串长度
    while (i < Str1.len && j < Str2.len) {
        // 比较字符
        if ((-1 == j) || (Str1.ch[i] == Str2.ch[j])) {
            // 相等,比较下一个字符
            i++;
            j++;
        } else {
            // 不想等,根据next数组将模式串向右移动
            j = next[j];
        }
    }
    // 若此时j即子串的下标等于子串的长度,即为子串匹配成功,否则是主串遍历所以字符仍未找到对应子串
    if (j == Str2.len) {
        // 此时i的值对应的是匹配串后的一个位置,i - 子串长度Str2.len即为子串在主串中的索引号
        return i - Str2.len;
    } else {
        return -1;
    }
}

KMP算法的时间复杂度为 O ( m + n ) O(m + n) O(m+n)。KMP算法只是在主串和子串有很多“部分匹配”是才明显由于朴素算法。
KMP算法的主要优点是:主串不回溯。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北溪入江流

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值