串
文章目录
4.1 串类型的定义
串(string) (或字符串)是由零个或多个字符组成的有限序列,一般记为:
- s 是串的名,用单引号括起来的字符序列是串的值;
- ai(1 ≤ i ≤ n)可以是字母、数字或 其他字符;
- 串中字符的数目 n称为串的长度。
- 零个字符的串称为空串(null string), 它的长度为零。
- 串中任意个连续的字符组成的子序列称为该串的子串。包含子串的串相应地称为主串。
- 通常称字符在序列中的序号为该字符在串中的位置。子串在主串中的位置则以子串的第一个字符在主串中的位置来表示。
- 两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的。
- 空格常常是串的字符集合中的一个元素。由一个或多个空格组成的串''称为空格串(blank string, 请注意:此处不是空串)。 它的长度为串中空格字符的个数。
串赋值 StrAssign、串复制 Strcopy、串比较 StrCompare、求串长 StrLength、串联接 Concat以及求子串 SubString等六种操作构成串类型的最小操作子集。
👉 例如:Index(S, T, pos)
可利用串比较、求串长和求子串等操作实现 - StrCompare( SubString(S, i, StrLength(T) ), T)
算法的基本思想为:在主串S中取从第i(i的初值为pos)个字符起、长度和串T相等的子串和串T比较,
若相等,则求得函数值为i,否则i值增1直至串S中不存在和串T相等的子串为止
//T为非空串。若主串s中第pos个字符之后存在与相等的子串,则返回第一个这样的子串在s中的位置,否则返回0 int Index(String S, String T, int pos) { if (pos > 0) { n = StrLenth(S); m = StrLenth(S); i = pos; while (i <= n - m + 1) { SubString(sub, S, i, m); if (StrCompare(sub, T) != 0) { ++i; } else { return i; } } } return 0; }
4.2 串的表示和实现
4.2.1 定长顺序存储表示(静态数组实现)
-
用一组地址连续的存储单元存储串值的字符序列。
-
在串的定长顺序存储结构中,按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。
-
串的实际长度可在这预定义长度的范围内随意,超过预定义长度的串值则被舍去,称之为"截断 "
对串长有两种表示方法:
一 、以下标为 0 的数组分量存放串的实际长度;
// ----- 串的定长顺序存储表示(非C语言)---- # define MAXSTRLEN 255 // 用户可在 255 以内定义最大串长 typedef unsigned char SString[MAXSTRLEN + 1]; //0号单元存放串的长度
二 、在串值后面加一个不计入串长的结束标记字符,如在C语言中以"\0"表示串值的终结。此时的串长为隐含值。
⭐️串联接
4.2.2 堆分配存储表示(动态数组实现)
-
仍以一组地址连续的存储单元存放串值字符序列,但它们的存储空间是在程序执行过程中动态分配而得。
-
在C语言中,存在一个称之为"堆"的自由 存储区,并由 C语言的动态分配函数malloc()和 free()来管理。利用函数 malloc()为每 个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址 的指针,作为串的基址,同时约定串长也作为存储结构的一部分。
typedef struct {
char* ch; //若是非空串,则按串长分配存储区,否则 ch为NULL
int length; //串长度
}HString;
StrCopy(&T, S) 的实现算法:若串T已存在,则先释放串T所占空间,当串S不空时, 首先为串T分配大小和串S长度相等的存储空间,然后将串S的值复制到串T中;
Strlnsert(&s, pos, T) 的实现算法是:为串S重新分配大小等于串S和串T长度之和的存储空间,然后进行串值复制
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<malloc.h> #include<stdlib.h> typedef struct HString { char* ch; // 若是非空串,则按串长分配存储区,否则ch为NULL int length; // 串长度 }HString; //初始化(产生空串)字符串S void InitString(HString* S) { S->ch = NULL; S->length = 0; } //将S清为空串 void ClearString(HString* S) { if (S->ch) { free(S->ch); S->ch = NULL; } S->length = 0; } //生成一个其值等于串常量chars的串S void StrAssign(HString* S, char* chars) { int i, j; if (S->ch)//若S的指针不为空则释放S指针域所指向的空间 free(S->ch); // 释放S原有空间 i = strlen(chars); // 求chars的长度i if (!i)//若字符串为空(chars的长度为0)则S也要设置为空 { ClearString(S); } else { // chars的长度不为0 S->ch = (char*)malloc(i * sizeof(char)); // 分配大小跟chars大小相同的串空间 if (!S->ch) // 分配串空间失败 exit(-1); for (j = 0; j < i; j++) // 复制串 S->ch[j] = chars[j]; S->length = i; } } //复制串S到T void StrCopy(HString* T, HString* S) { if (T->ch) free(T->ch); // 释放T原有空间 T->ch = (char*)malloc(S->length * (sizeof(char)));//申请内存 if (T->ch == NULL) { printf("内存分配失败!"); exit(-1); } for (int i = 0; i < S->length; i++)//逐一复制 { T->ch[i] = S->ch[i]; } T->length = S->length;//复制串长 } //显示字符串 void StrShow(HString* S) { int i; for (i = 0; i < S->length; i++) printf("%c", S->ch[i]); printf("\n"); } void Strlnsert(HString* S, int pos, HString* T) { //1≤pos≤strLength(S)+1 。在串S的第pos个字符之前插入串T. //eg: 【S = "abcdefg" , T = "123", pos = 3】 => S = "ab123cdefg" if (pos<1 || pos>S->length + 1) { exit(-1); //pos不合法 } if (T->length) // T非空,则重新分配空间,插入 { if (!(S->ch = (char*)realloc(S->ch, (S->length + T->length) * sizeof(char)))) { exit(-1); } int i; //为插入T而腾出位置 for (i = S->length - 1; i >= pos - 1; --i) { S->ch[i + T->length] = S->ch[i]; } // 插入T int j; for (j = 0; j < T->length; j++) { S->ch[pos + j - 1] = T->ch[j]; } S->length += T->length; } } int main() { HString T, S; InitString(&T); // 初始化串T InitString(&S); // 初始化串S char chars[] = "abjsk123!"; StrAssign(&S, chars); //将常量赋值给串S StrShow(&S); StrCopy(&T, &S);//将串S复制给串T StrShow(&T); Strlnsert(&S, 3, &T); //将串S的第3个位置之前插入串T StrShow(&S); return 0; }
4.2.3 串的块链存储表示
-
每个结点可以存放一个字符,也可以存放多个字符。
-
当结点大小 大于 1 时,由于串长不一定是结点大小的整倍数,则链表中的最后一个结点不一定全被串值占满,此时通常补上"#“或其他的非串值字符(通常”#"不属于串的字符集,是一个特殊的符号)。
-
存储密度 = 串值所占的存储位 实际分配的存储位 存储密度 = \frac{串值所占的存储位}{实际分配的存储位} 存储密度=实际分配的存储位串值所占的存储位
// ----- 串的块链存储表示----
# define CHUNKSIZE 80
typedef struct Chunk { // 结点结构
char c[CHUNKSIZE];
Chunk* next;
}Chunk;
typedef struct { // 串的链表结构
Chunk* head, * tail;
int curlen;
}LString;
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OVERFLOW -2
typedef int Status;
/* 存储结构 */
#define blank '#'
#define CHUNKSIZE 4 //块大小
typedef struct Chunk
{
char ch[CHUNKSIZE];
struct Chunk* next;
} Chunk;
typedef struct
{
Chunk* head, * rear; //串的头和尾指针
int curlen; //串的当前长度
} LString;
void InitString(LString* S)
{
S->head = NULL;
S->rear = NULL;
S->curlen = 0;
}
Status StrAssign(LString* S, char* chars)
{
int len, blockNum, i, j;
Chunk* p, * q = NULL;
len = strlen(chars);
//长度为0或包含#时结束
if (!len || strchr(chars, blank))
return ERROR;
S->curlen = len;
//计算结点数
blockNum = len / CHUNKSIZE; //商
if (len % CHUNKSIZE) //余数
++blockNum;
//循环生成新节点
for (i = 0; i < blockNum; ++i)
{
p = (Chunk*)malloc(sizeof(Chunk));
if (!p)
exit(OVERFLOW);
if (S->head == NULL) //如果是第一个节点
S->head = q = p; //将新节点 p 赋值给 q,将 q(即新节点 p)赋值给 S->head
else
{
if (q != NULL) {
q->next = p;
q = p;
}
}
for (j = 0; j < CHUNKSIZE && *chars; ++j) //每次新增一个块链即赋值,chars指针随之++,当chars指向空字符时结束
{
*(q->ch + j) = *chars; //将chars指向的字符复制到当前Chunk的ch数组的第j个位置。
++chars; // 将chars指针移动到下一个字符。
}
if (!*chars) //当*chars指向空字符(最后一个链块时)
{
S->rear = p; // 将S->rear标记链表的尾部。
S->rear->next = NULL;
for (; j < CHUNKSIZE; ++j)
*(q->ch + j) = blank; //将剩余的部分填充为blank。
}
}
return OK;
}
Status StrCopy(LString* T, LString S) {
if (!S.head) {
return ERROR; // 源字符串为空
}
Chunk* h = S.head; // 源字符串的当前块
Chunk* p = NULL, * q = NULL; // 目标字符串的当前块和前一个块
while (h != NULL) {
p = (Chunk*)malloc(sizeof(Chunk)); // 为新块分配内存
if (p == NULL) {
// 如果内存分配失败,清理已分配的内存并返回OVERFLOW
exit(OVERFLOW); // 或者可以选择其他错误处理方式
}
// 复制块内容
//memcpy(p->ch, h->ch, CHUNKSIZE);
int i;
for (i = 0; i < CHUNKSIZE; i++)
{
*(p->ch + i) = *(h->ch + i);
}
p->next = NULL;
// 构建目标字符串的链表
if (T->head == NULL) {
T->head = p; // 第一个块
}
else {
if (q != NULL) {
q->next = p; // 连接前一个块和当前块
}
}
q = p; // 更新前一个块为当前块
h = h->next; // 移动到源字符串的下一个块
}
T->rear = p; // 最后一个块成为尾部
return OK;
}
Status SubString(LString* Sub, LString S, int pos, int len)
{
Chunk* p;
char* q;
if (pos > S.curlen || pos < 0 || pos + len - 1 > S.curlen)
return ERROR;
q = (char*)malloc((len + 1) * sizeof(char));
int i = 0, j = 0, n;
p = S.head;
while (j < pos) //逐个位置索引到pos
{
if (j == pos - 1)
break;
++j;
++i;
if (i == CHUNKSIZE) //如果索引i达到CHUNKSIZE,则移动到下一个块
{
p = p->next; //移动到下一个Chunk并重置索引i
i = 0;
}
}
j = 0;
while (j < len) //逐个位置赋值
{
*(q + j) = *(p->ch + i); //将S中的字符复制到q中
i++;
if (i == CHUNKSIZE)
{
p = p->next;
i = 0;
}
j++;
}
*(q + j) = '\0'; //在q的末尾添加字符串结束符
InitString(Sub); //初始化子串
StrAssign(Sub, q); //将q中赋值
Sub->curlen = len; //设置Sub的当前长度为len
return OK;
}
void StrPrint(LString T)
{
Chunk* p;
p = T.head;
int i;
while (p)
{
for (i = 0; i < CHUNKSIZE; ++i)
if (*(p->ch + i) != blank)
printf("%c", *(p->ch + i));
p = p->next;
}
printf("\n");
}
void ClearString(LString* S) {
Chunk* p = S->head; // 保存指向第一个块的指针
while (p != NULL) {
Chunk* temp = p; // 保存当前块的地址
p = p->next; // 移动指针到下一个块
free(temp); // 释放当前块的内存
}
S->head = S->rear = NULL; // 将头尾指针设置为NULL
S->curlen = 0; // 重置当前长度为0
}
int main()
{
char* chars = "abcdef";
LString S, T, Sub;
InitString(&S);
printf("StrAssign: \n");
StrAssign(&S, chars);
StrPrint(S);
InitString(&T);
printf("StrCopy: \n");
StrCopy(&T, S);
StrPrint(T);
printf("SubString: \n");
SubString(&Sub, S, 2, 3);
StrPrint(Sub);
ClearString(&S);
ClearString(&T);
ClearString(&Sub);
return 0;
}
4.3 串的模式匹配算法
4.3.1 求子串位置的定位函数Index (S, T,pos)
定长顺序结构表示串时的算法
简单算法 - 时间复杂度 O(s_len * t_len)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int Index(char* S, char* T, int pos, int s_len, int t_len)
{
int i = pos-1; //i,j皆为数组索引,从0开始
int j = 0;
while (i < s_len && j < t_len)
{
if (S[i] == T[j])
{
++i;
++j;
}
else
{
i = i - j + 1;
j = 0;
}
}
if (j >= t_len)
return i - t_len; //返回索引值
else
{
return -1;
}
}
int main()
{
char S[] = "abcdabcfabcbdfac";
char T[] = "abcbd";
int s_len = strlen(S);
int t_len = strlen(T);
int index = Index(S, T, 1, s_len, t_len); // pos=1,相当于索引为0
printf("%d", index);
return 0;
}
首尾匹配算法 - 时间复杂度 O(s_len * t_len)
先比较模式串的第一个字符,再比较模式串的最后一个字符,最后比较模式串中从第二个到第n-1个字符
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int Index_FL(char* S, char* T, int pos, int s_len, int t_len)
{
char patStartChar = T[0];
char patEndChar = T[t_len - 1];
int i = pos - 1;
while (i <= s_len - t_len + 1)
{
if (S[i] != patStartChar)
{
++i;
}
else if (S[i + t_len - 1] != patEndChar)
{
++i;
}
else
{
int k = 1, j = 1;
while (j < t_len && S[i + k] == T[j])
{
++k;
++j;
}
if (j == t_len)
{
return i;
}
else
{
++i;
}
}
}
}
int main()
{
char S[] = "abcdabcfabcbdfac";
char T[] = "abc";
int s_len = strlen(S);
int t_len = strlen(T);
int index = Index_FL(S, T, 1, s_len, t_len); // 首尾匹配算法
printf("%d", index);
return 0;
}
4.3.2 模式匹配的一种改进算法 - KMP算法 - 时间复杂度 O(s_len + t_len)
-
解决了指针回溯的问题 :利用匹配失败时失败之前的已知部分时匹配的这个有效信息,保持主串的 i 指针不回溯,通过修改模式串(子串)的 j 指针,使模式串尽量地移动到有效的匹配位置
-
思路:通过子串获取最长公共前后缀 -> 通过 长公共前后缀获取next数组(部分匹配表Next)-> 实现KMP
-
获取最长公共前后缀
-
前缀:不包含最后一个字符的所有以第一个字符(索引为0)开头的连续子串
比如字符串 “ABABA” 的前缀有:A,AB,ABA,ABAB
-
后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串
比如字符串 “ABABA” 的后缀有:BABA,ABA,BA,A
-
公共前后缀:一个字符串的 所有前缀连续子串 和 所有后缀连续子串 中相等的子串
比如字符串 “ABABA” 的公共前后缀有:A ,ABA
-
最长公共前后缀:所有公共前后缀 的 长度最长的 那个子串
比如字符串 “ABABA” 的最长公共前后缀有:ABA
例子:一个字符串 str = “ABCABD”
-
对于str从
索引为0
开始的子串 “A
” 而言:- 前缀:不包含
最后一个字符A
的 所有以第一个字符A
开头 的 连续子串 不存在 - 后缀:不包含
第一个字符A
的 所有以最后一个字符A
结尾 的 连续子串 不存在
前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0
- 前缀:不包含
-
对于str从 索引为0 开始的子串 “
AB
” 而言:- 前缀:不包含
最后一个字符B
的 所有以第一个字符A
开头 的 连续子串 有 —— “A” - 后缀:不包含
第一个字符A
的 所有以最后一个字符B
结尾 的 连续子串 有 —— “B”
前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0
- 前缀:不包含
-
对于str从 索引为0 开始的子串 “
ABC
” 而言:- 前缀:不包含
最后一个字符C
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB” - 后缀:不包含
第一个字符A
的 所有以最后一个字符C
结尾 的 连续子串有 —— “BC”,“C”
前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0
- 前缀:不包含
-
对于str从 索引为0 开始的子串 “
ABCA
” 而言:- 前缀:不包含
最后一个字符A
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB”,“ABC” - 后缀:不包含
第一个字符A
的 所有以最后一个字符A
结尾 的 连续子串有 —— “BCA”,“CA”,“A”
前缀与后缀的连续子串中存在相同且最长的子串 A,因此该子串的最长公共前后缀 为 1 (子串A的长度为1)
- 前缀:不包含
-
对于str从 索引为0 开始的子串 “
ABCAB
” 而言:- 前缀:不包含
最后一个字符B
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB”,“ABC”,“ABCA” - 后缀:不包含
第一个字符A
的 所有以最后一个字符B
结尾 的 连续子串有 —— “BCAB”,“CAB”,“AB”,“B”
前缀与后缀的连续子串中存在相同且最长的子串 AB,因此该子串的最长公共前后缀 为 2(子串AB的长度为2)
- 前缀:不包含
-
对于str从 索引为0 开始的子串 “
ABCABD
” 而言:- 前缀:不包含
最后一个字符D
的 所有以第一个字符A
开头 的 连续子串 有 —— “A”,“AB”,“ABC”,“ABCA”,“ABCAB” - 后缀:不包含
第一个字符A
的 所有以最后一个字符D
结尾 的 连续子串有 —— “BCABD”,“CABD”,“ABD”,“BD”,“D”
前缀与后缀的连续子串不存在相同的,因此该子串的最长公共前后缀 为 0
- 前缀:不包含
Next数组:第一个字符开始的每个子串 的 最后一个字符 与 该子串的最长公共前后缀的长度 的对应关系表格
A B C A B D 0 0 0 1 2 0 对应的next数组就是 int[] next = {0, 0, 0, 1, 2, 0}
-
-
实现KMP算法
-
-
根据部分匹配表搜索字符串匹配位置
🌟匹配成功一个就退出
/* 匹配成功一个就退出 */ #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> #include <string.h> // 计算部分匹配表 next 数组 void computeNext(const char* pattern, int* next) { int m = strlen(pattern); //获取模式字符串的长度 next[0] = 0; //初始化部分匹配表的第一个元素为0,因为单个字符的最长公共前后缀长度为0。 int len = 0; //用于记录当前最长公共前后缀的长度。同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]。 int i = 1;//从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度 while (i < m) { /* 上一个pattern的子串 对应索引[0,i-1] 的最长公共前后缀长度为 len 前缀字符串的索引是[0,len-1],后缀字符串是索引[i-len,i-1] */ if (pattern[len] == pattern[i]) { /* 如果相等那么就可以确定当前子串的最长公共前后缀的前缀字符串是[0,len] ,后缀字符串是[i-len,i] */ next[i] = ++len; i++; } else { if (len == 0) { next[i] = 0; i++; } else { len = next[len - 1]; } } } } // KMP 搜索算法 int kmpSearch(const char* text, const char* pattern, int* next) { int n = strlen(text); int m = strlen(pattern); int i = 0, j = 0; //分别用于遍历主串和模式字符串。 while (i < n && j < m) { if (text[i] == pattern[j]) { //相等就继续进行匹配 i++; j++; } else { //如果 patternStr[i] 和 matchStr[j] 不相等 if (j == 0) { /* 表示 matchStr 没有匹配到 patternStr的第一个字符 那直接将 matchStr 的指针 i 向后移动一位即可 */ i++; } else { j = next[j - 1]; // 字符失配,根据 next 跳过子串前面的一些字符 } } } return j == m ? i - j : -1; //如果j等于模式字符串的长度m,则返回匹配的索引i - j;否则返回-1,表示没有找到匹配 } int main() { const char* matchStr = "CACAABACADDABACABABACABAB"; const char* patternStr = "ABACABAB"; int m = strlen(patternStr); int* next = (int*)malloc(sizeof(int) * m); computeNext(patternStr, next); for (int i = 0; i < m; i++) { printf("next[%d] = %d\n", i, next[i]); } int index = kmpSearch(matchStr, patternStr, next); printf("index = %d\n", index); free(next); return 0; }
🌟允许匹配多个
/* 允许匹配多个 */ #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> #include <string.h> // 计算部分匹配表 next 数组 void computeNext(const char* pattern, int* next) { int m = strlen(pattern); //获取模式字符串的长度 next[0] = 0; //初始化部分匹配表的第一个元素为0,因为单个字符的最长公共前后缀长度为0。 int len = 0; //用于记录当前最长公共前后缀的长度。同时知道当前子串的最长公共前后缀的前缀字符串对应索引 [0,len-1]。 int i = 1;//从第二个字符开始遍历,求索引在 [0,i] 的子串的最长公共前后缀长度 while (i < m) { /* 上一个pattern的子串 对应索引[0,i-1] 的最长公共前后缀长度为 len 前缀字符串的索引是[0,len-1],后缀字符串是索引[i-len,i-1] */ if (pattern[len] == pattern[i]) { /* 如果相等那么就可以确定当前子串的最长公共前后缀的前缀字符串是[0,len] ,后缀字符串是[i-len,i] */ next[i] = ++len; i++; } else { if (len == 0) { next[i] = 0; i++; } else { len = next[len - 1]; } } } } // KMP 搜索算法 int* kmpSearch(const char* text, const char* pattern, int* next, int* count) { int n = strlen(text); int m = strlen(pattern); int* firstIndexList = (int*)malloc(sizeof(int) * n); // 假设最多有 n 个匹配 int i = 0, j = 0; while (i < n) { if (text[i] == pattern[j]) { i++; j++; } else { if (j != 0) { j = next[j - 1]; } else { i++; } } if (j == m) { //超出了最大索引值 firstIndexList[*count] = i - j; //调整 j 为 模式字符串pattern (索引 [ 0, j-1 ])的最长公共前缀字符串的下一个索引位置 j = next[j - 1]; //允许匹配多个,且这些匹配的子串可以重叠 /* j = 0; //允许匹配多个,但这些匹配的子串不可以重叠 */ (*count)++; } } return firstIndexList; } int main() { const char* matchStr = "ABACABABCACAABACADDABACABABACABAB"; const char* patternStr = "ABACABAB"; int n = strlen(matchStr); int m = strlen(patternStr); int* next = (int*)malloc(sizeof(int) * m); computeNext(patternStr, next); for (int i = 0; i < m; i++) { printf("next[%d] = %d\n", i, next[i]); } int count = 0; int* indices = kmpSearch(matchStr, patternStr, next, &count); printf("-----------------\n"); printf("Count: %d\n", count); printf("-----------------\n"); printf("Indices: "); for (int i = 0; i < count; ++i) { printf("%d ", indices[i]); } printf("\n"); free(indices); free(next); return 0; }
参考:
教材:严蔚敏《数据结构》(C语言版).pdf
博客:
视频: