第3章 串 与 数 组 3.1基 础 实 践 3.1.1串的堆分配存储表示上的基本操作 1. 实践目的 (1) 能够正确描述串的堆分配存储在计算机中的表示。 (2) 能够正确编写堆分配存储表示上基本操作的实现算法。 (3) 能够编写程序验证在堆分配存储表示上实现的基本操作算法的正确性。 2. 实践内容 (1) 创建串的堆分配存储表示。 (2) 在串的堆分配存储表示上实现串初始化、串赋值、求串长、串比较、串连接、求子串、串删除和串插入等操作。 3. 实践要求 1) 数据结构 串的堆分配存储表示的描述如下: typedef struct {char *ch;//若是非空串,则按串长分配存储区,否则ch为NULL int length;//串长 }HString; 2) 函数接口说明 void StrInit(HString& S); //创建一个空串S Status StrAssign(HString& S, const char chars[]); //生成一个其值等于chars的串S int StrLength(HString S); //返回串S的长度 int StrCompare(HString S, HString T); //比较串S和T的大小,如果S=T则返回0; 如果串S>T则返回正值; 如果SS2 ZheJiang 8 SubString failed! Zheng ZhengJiang取子串长度不合法,其他输入的参数都合法 续表 序号输入输出说明 4sun flower 4 4 10 3 4S1>S2 sunflower 9 flow Deletion failed! sunflowerflower删除位置不合法,其他输入的参数都合法 5swan lake 1 4 2 4 -2S1>S2 swanlake 8 swan sake Insertion failed!插入位置不合法,其他输入的参数都合法 4. 解决方案 上述接口的实现方法分析如下。 (1) 串初始化操作StrInit(&S): 即构造一个空串S。根据堆分配存储表示的定义可知,空串满足的条件是指向存储空间的指针ch值为NULL,且串长为0。 (2) 串赋值操作StrAssign(&S,chars[]): 根据给定的字符数组(字符串)chars构造一各堆分配存储表示的串S。首先,如果串S不为空串,则需先释放原有空间; 其次,根据给定的字符数组chars的大小进行存储空间的分配,如果分配失败,则本操作失败; 如果分配成功,则将字符数组chars中的内容逐一复制至分配的存储空间; 最后设置串长。 (3) 求串长操作StrLength(S): 返回串的堆分配存储表示中串长数据域的值。 (4) 串比较操作StrCompare(S, T): 按字典序依次比较给定的两个字符串S和T对应位置上字符的大小,直到遇到不相同字符,则返回不相同字符对应的ASCII码值的差值; 如果其中一个字符串提前结束,则返回两串长的差值。即如果串S>T,则返回正值; 如果S=T,则返回0; 如果S #include #include #define ERROR 0 #define OK 1 #define OVERFLOW -2 typedef int Status; typedef struct{ char* ch;//若是非空串,则按串长分配存储区,否则ch为NULL int length;//串长 }HString; void StrInit(HString& S) //创建一个空串 {S.ch = NULL; S.length = 0; } Status StrAssign(HString& S, const char chars[]) //生成一个其值等于chars的串S {if (S.ch) free(S.ch);//释放旧空间 int len = strlen(chars); if (!(S.ch = (char*)malloc(len * sizeof(char)))) exit(OVERFLOW); for (int i = 0; i < len; i++) S.ch[i] = chars[i]; S.length = len;//设置子串长度 return OK; } int StrLength(HString S) //返回串S的长度 {return S.length; } int StrCompare(HString S, HString T) //比较串S和T的大小, 如果S=T,则返回0;如果串S>T,则返回正值;如果S S.length || len < 0 || len > S.length - pos + 1)//参数合法性检查 return ERROR; if (Sub.ch) free(Sub.ch); if (!(Sub.ch = (char*)malloc(len * sizeof(char)))) exit(OVERFLOW); for (int i = 0; i < len; i++)//将主串中的字符复制到子串的相应位置 Sub.ch[i] = S.ch[pos - 1 + i]; Sub.length = len;//设置子串长度 return OK; } Status StrDelete(HString& S, int pos, int len) //从串S中删除第pos个字符起长度为len的子串。其中: 1≤pos≤S.length - len + 1 {if (pos<1 || pos>S.length - len + 1 || len < 0) return ERROR; for (int i = pos - 1 + len; i < S.length; i++) S.ch[i - len] = S.ch[i]; S.length -= len; return OK; } Status StrInsert(HString& S, int pos, HString T) //在串S中第pos个字符之前插入串T。其中: 1≤pos≤S.length+1 {if (pos < 1 || pos > S.length + 1) return ERROR; int i; char* p; p = S.ch; if (!(S.ch = (char*)malloc((S.length + T.length)* sizeof(char)))) exit(OVERFLOW); for (i = 0; i < pos - 1; i++)//将插入点之前的子串复制到新的位置 S.ch[i] = p[i]; for (i = S.length - 1; i >= pos - 1; i--)//为插入串T而腾出位置 S.ch[i + T.length] = p[i]; free(p); for (i = 0; i < T.length; i++)//插入T S.ch[pos - 1 + i] = T.ch[i]; S.length += T.length; return OK; } void StrPrint(HString S) //输出字符串S {for (int i = 0; i < S.length; i++) putchar(S.ch[i]); } int main() {char s1[100], s2[100]; int pos, len; HString S1, S2, S, Sub; StrInit(S1); StrInit(S2); StrInit(S); StrInit(Sub); printf("输入串S1: "); scanf("%s", s1); StrAssign(S1, s1); printf("输入串S2: "); scanf("%s", s2); StrAssign(S2, s2); printf("\n两个串比较: "); if (StrCompare(S1, S2)< 0) printf("S1S2"); Concat(S, S1, S2); printf("\n\n由S1和S2连接而成的新串S: "); StrPrint(S); printf("\n新串长: "); printf("%d", StrLength(S)); printf("\n\n输入子串在S的起始位置: "); scanf("%d", &pos); printf("输入子串长: "); scanf("%d", &len); if (SubString(Sub, S, pos, len)) {printf("子串为: "); StrPrint(Sub); } else printf("SubString failed!"); printf("\n\n输入删除串S的起始位置: "); scanf("%d", &pos); printf("输入删除子串的长度: "); scanf("%d", &len); if (StrDelete(S, pos, len)) {printf("删除后的串S为: "); StrPrint(S); } else printf("Deletion failed!\n"); printf("\n\n输入在串S中插入串S2的位置: "); scanf("%d", &pos); if (StrInsert(S, pos, S2)) {printf("插入后的串S为: "); StrPrint(S); } else printf("Insertion failed!\n"); } 6. 运行结果参考 程序部分测试用例的运行结果如图31所示。 图31程序部分测试用例的运行结果 7. 延伸思考 (1) 在串的操作中,串赋值StrAssign、求串长StrLength、串比较StrCompare、串连接Concat以及求子串SubString这5种操作构成串类型的最小操作子集,其他串操作可在这个最小操作子集上实现,那么如何利用这些最小操作子集实现串的删除操作? (2) 根据上述已实现的串的基本操作,编写算法模仿实现串的置换操作StrReplace(&S,T,V),即以串V替代串S中出现的所有和串T相同的子串。 3.1.2稀疏矩阵的转置操作 1. 实践目的 (1) 能够正确描述稀疏矩阵的三元组顺序表在计算机中的表示。 (2) 能够正确编写在稀疏矩阵的三元组顺序表上基本操作的实现算法。 (3) 能够编写程序验证在稀疏矩阵的三元组顺序表上实现的基本操作算法的正确性。 2. 实践内容 (1) 创建已知稀疏矩阵的三元组顺序表存储表示。 (2) 在稀疏矩阵的三元组顺序表上实现矩阵转置操作。 3. 实践要求 1) 数据结构 在稀疏矩阵的三元组顺序表上实现指定操作,先要将稀疏矩阵的三元组顺序表中每一非零元的存储结构描述如下: #define MAXSIZE 100//预定义的最大非零元个数 typedef struct {int i, j;//该非零元的行下标和列下标 ElemType e;//非零元素值 } Triple; 再将稀疏矩阵三元组顺序表的存储结构描述如下: typedef struct {Triple data[MAXSIZE];//非零元三元组表 intm; //矩阵的行数 intn; //矩阵的列数 intt; //矩阵的非零元个数 } TSMatrix; 2) 函数接口说明 void InitMatrix(int mat[MAXLEN][MAXLEN], int m, int n, TSMatrix& TSM); //根据已知的稀疏矩阵mat,创建三元组表TSM,其中m和n分别是稀疏矩阵的行数和列数 void TransposeSMatrix(TSMatrix M, TSMatrix& T); //求稀疏矩阵M的转置矩阵T 3) 输入输出说明 输入说明: 输入信息分为2部分。第1部分共1行,输入稀疏矩阵的行列数m和n,中间用一个空格隔开; 第2部分根据第1行输入的m和n的值对应输入m行n列的矩阵数据,数据之间用一个空格隔开。 输出说明: 输出分为2部分。第1部分输出原矩阵的行数、列数和非零元素的个数,及其对应的三元组表; 第2部分为转置矩阵输出原矩阵的行数、列数和非零元素的个数,及其对应的三元组表。 4) 测试用例 测试用例信息如表32所示。 表32测试用例 序号输入输出说明 15 6 0 0 80 00 0 0 00 00 5 0 00 160 0 0 180 00 0 0 09 00原稀疏矩阵: 5行6列共5个非零元素 行列元素值 02 8 20 5 2416 3218 43 9 转置后的稀疏矩阵: 6行5列共5个非零元素 行列元素值 02 5 20 8 2318 34 9 4216无 4. 解决方案 上述接口的实现方法分析如下。 (1) 创建稀疏矩阵的三元组顺序表操作InitMatrix(mat,m,n,&TSM): 首先设置稀疏矩阵对应的三元组顺序表的行数、列数分别为m、n; 再依次将二维数组mat中的每个非零元所在的行、列和元素值存储在三元组表中并对非零元计数,最后将三元组顺序表的非零元个数设置为计数器值。 (2) 稀疏矩阵转置操作TransposeSMatrix(M,&T): 矩阵转置是一种简单的矩阵运算,指的是将矩阵中每个元素的行列号互换一下。对于一个m×n的矩阵M,它的转置矩阵T是一个n×m的矩阵,且T(i,j)= M(j,i)。当稀疏矩阵用三元组顺序表来表示时,是以行主序的原则存放非零元素的,这样存放有利于稀疏矩阵的运算。那么,根据转置的概念,首先可设置转置后矩阵T的行数、列数分别为原矩阵M的列数和行数,非零元素个数不变; 再依次将原矩阵M中存储的非零元的三元组中的每个元素转存到转置矩阵T的非零元三元组中,注意每一非零元转置后的行号是转置前的列号,转置后的列号是转置前的行号,转置前后元素值不变。 然而,若按行列序号直接互换进行转置,则所得的三元组顺序表就不再满足行主序的原则。例如,图32(a)中的三元组所表示的矩阵,转置后如图32(b)所示,不再满足行主序的原则。为解决此问题,可按照以下方法进行矩阵转置: 扫描转置前的三元组表,并按先列序、后行序的原则转置三元组。例如,对图32(a)中的三元组,从第0行开始向下搜索列序号为0的元素,找到三元组(2,0,5),则转置为(0,2,5),并存入转置后的三元组顺序表中。接着搜索列序号为1的元素,没找到; 再搜索列序号为2的元素,找到(0,2,8),转置为(2,0,8),并存入转置后的三元组顺序表中。以此类推,直到扫描完所有列,即可完成矩阵转置,并且转置后的三元组表应满足先行序、后列序的原则。正确转置后的三元组如图32(c)所示。 图32矩阵转置 5. 程序代码参考 #include #define MAXLEN 255//用户可在255以内定义数据最大长度 #define MAXSIZE 12500//假设非零元个数的最大值为12500 typedef int ElemType; typedef struct { int i, j;//该非零元的行下标和列下标 ElemType e; } Triple; typedef struct { Triple data[MAXSIZE];//非零元三元组表 intm; //矩阵的行数 intn; //矩阵的列数 intt; //矩阵的非零元个数 } TSMatrix; //从一个稀疏矩阵创建三元组表,mat为稀疏矩阵 void InitMatrix(int mat[MAXLEN][MAXLEN], int m, int n, TSMatrix& TSM) {int i, j, k = 0, count = 0; TSM.m = m; TSM.n = n; for (i = 0; i < m; i++) for (j = 0; j < n; j++) if (mat[i][j] != 0) {TSM.data[count].i = i; TSM.data[count].j = j; TSM.data[count].e = mat[i][j]; count++; } TSM.t = count; } void TransposeSMatrix(TSMatrix M, TSMatrix& T) // 采用三元组存储表示,求稀疏矩阵M的转置矩阵T {int p, q, col; T.m = M.n; T.n = M.m; T.t = M.t; if (T.t) {q = 0; for (col = 0; col < M.n; col++) for (p = 0; p < M.t; p++) if (M.data[p].j == col) {T.data[q].i = M.data[p].j; T.data[q].j = M.data[p].i; T.data[q].e = M.data[p].e; q++; } } } void PrintSMatrix(TSMatrix M) //输出稀疏矩阵M {int i; printf("%d行%d列共%d个非零元素\n", M.m, M.n, M.t); printf("行 列 元素值\n"); for (i = 0; i < M.t; i++) printf("%2d%4d%8d\n", M.data[i].i, M.data[i].j, M.data[i].e); } int main() {int mat[MAXLEN][MAXLEN]; int m, n; printf("输入稀疏矩阵的行列数: "); scanf("%d %d", &m, &n); printf("输入稀疏矩阵的元素值: \n"); for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) scanf("%d", &mat[i][j]); TSMatrix M, T; InitMatrix(mat, m, n, M); printf("\n原稀疏矩阵: "); PrintSMatrix(M); TransposeSMatrix(M, T); printf("\n转置后的稀疏矩阵: "); PrintSMatrix(T); } 6. 运行结果参考 程序测试用例的运行结果如图33所示。 图33程序部分测试用例的运行结果 7. 延伸思考 (1) 分析一下上面实现的矩阵转置算法的时间复杂度,想想看这个转置算法的时间主要花费在什么地方?如何改进才能实现矩阵的快速转置? (2) 如何在三元组顺序表存储表示下实现两个稀疏矩阵的加法运算? 3.2进 阶 实 践 3.2.1字符串加密、解密 1. 实践目的 (1) 能够正确分析字符串加密、解密涉及的关键问题及其解决思路。 (2) 能够根据字符串加密、解密的特点选择适当的存储结构。 (3) 能够运用串的基本操作方法设计字符串加密、解密中关键操作算法。 (4) 能够编写程序验证字符串加密、解密程序的正确性。 (5) 能够对实践结果的性能进行辩证分析或优化。 2. 实践内容 对给定的字符串按照指定的密钥进行加密处理,加密后生成密码串; 另一方面,程序也可对给定的加密后的密码串按照密钥进行解密处理,还原成明文。加密规则是: 根据输入的密钥,将需加密的字符串中每个字符与密钥进行异或运算。由于异或运算具有对称性,解密时,也只需要将加密后的字符串与密钥进行异或运算即可还原出原始字符串。 3. 实践要求 (1) 为使字符串加密、解密具有普适性,要求字符串由用户自主设定。 (2) 根据字符串加密、解密规则及其操作特性,自主选择恰当的存储结构。 (3) 给出关键操作功能模块的接口描述及其实现算法。 (4) 输入输出说明: 本实践输入内容有2行,第1行为输入需要加密的一串字符,第2行输入一个数,作为加密时使用的密钥。输出内容也分2行,第1行为加密后的字符串,第2行为解密后的字符串。 (5) 测试用例见“运行结果”。 4. 解决方案 1) 数据结构 由于字符串加密、解密程序中处理的对象是字符串,一般情况下长度限定在一定范围内,而且在做加密、解密操作时,需要对各个字符进行运算,因此涉及随机存取串中的各个元素,为此本实践中最后拟采用定长顺序存储表示,其存储结构描述如下。 #define MAXSTRLEN 255 //用户可在255以内定义最大串长 typedef unsigned char SString[MAXSTRLEN + 1];//0号单元存放串的长度 2) 关键操作实现要点 从实践内容及上述分析可知本实践的关键操作包括一个加密操作和一个解密操作,其中需要对字符串中每个字符与密钥进行异或运算。它们的实现要点简要说明如下: (1) 加密操作。 本实践中的加密规则: 根据输入的密钥,将需加密的字符串中每个字符与密钥进行异或运算。 (2) 解密操作。 由于异或运算具有对称性,解密时,也只需要将加密好的字符串与密钥进行异或运算即可还原出原始字符串。 3) 关键操作接口描述 Status StrEncode(SString S, int key, SString& T); //串加密操作,其中S是原串,T是加密后的串,key为密钥 Status StrDecode(SString S, int key, SString& T); //串解密操作,其中S是需解密的串,T是解密后的串,key为密钥 4) 关键操作算法参考 (1) 加密操作算法。 void StrEncode(SString S, int key, SString& T) //串加密操作,其中S是原串,T是加密后的串,key为密钥 {T[0] = S[0]; for (int i = 1; i <= S[0]; i++) T[i] = S[i] ^ key; } (2) 解密操作算法。 void StrDecode(SString S, int key, SString& T) //串解密操作,其中S是需解密的串,T是解密后的串,key为密钥 {T[0] = S[0]; for (int i = 1; i <= S[0]; i++) T[i] = S[i] ^ key; } 5. 程序代码参考 扫码查看: 321.cpp 6. 运行结果参考 程序测试用例的运行结果如图34所示。 图34程序测试用例的运行结果 7. 延伸思考 (1) 如果加密规则改为恺撒加密法,则如何实现加密、解密算法? (2) 如果需要对长文本进行加密、解密操作,则应该如何设计本实践案例? 3.2.2求字符串的最小循环节问题 1. 实践目的 (1) 能够正确分析字符串的最小循环节问题所涉及的关键问题及其解决思路。 (2) 能够根据字符串的最小循环节问题的特点选择适当的存储结构。 (3) 能够运用模式匹配方法设计字符串的最小循环节问题中的关键算法。 (4) 能够编写程序验证求解字符串的最小循环节问题的正确性。 (5) 能够对实践结果的性能进行辩证分析或优化。 2. 实践内容 对于一个长度为|S|的字符串S,如果S是由长度为|T|的字符串T循环k次构成,那么字符串T就是字符串S的最小循环节。 本问题要求编程实现求字符串的最小循环节,如组成字符串“ababab”的最小循环节就是“ab”。 3. 实践要求 (1) 为使求解字符串的最小循环节问题具有普适性,要求字符串由用户自主设定。 (2) 根据求解字符串的最小循环节问题的规则及其操作特性,自主选择恰当的数据存储结构。 (3) 给出关键操作功能模块的接口描述及其实现算法。 (4) 输入输出说明: 本问题要求输入的字符串必须是由某个循环节循环若干次构成的。输出分两行,第1行为最小循环节长度,第2行为最小循环节。 4. 解决方案 1) 数据结构 本问题的处理对象是由某个循环节循环若干次构成的字符串,一般情况下长度限定在一定范围内,而且本问题涉及计算串的next[j]函数值,需要随机存取串中的各个字符,所以本实践中串也适用定长顺序存储表示,但考虑到串的堆分配存储表示其空间的大小可根据实际需要进行动态分配,其利用率更高,为此本实践中的串拟采用堆分配存储表示,其存储结构描述如下: typedef struct {char* ch;//若是非空串,则按串长分配存储区,否则ch为NULL int length;//串长 }HString; 2) 关键操作实现要点 假定字符串S的长度为|S|,如果它是由长度为|T|的字符串T(字符串T的最小循环节是其本身)循环k次构成,那么字符串T就是字符串S的最小循环节。 字符串的最小循环节有一个很重要的性质和KMP算法有关,即|S|-next[|S|]为字符串T的长度|T|。 证明: 如果字符串S是由T循环k次构成,则有: |S|/|T|=k 且字符串S中,前k-1次T的循环和后k-1次T的循环构成的字符串相等,即 S[0..|S|-|T|-1]=S[|T|..|S|-1] 考虑到KMP算法中next数组的next[|S|]的值是k-1个循环节的长度,即 next[|S|]=|S|-|T| 得到: |S|-next[|S|]=|T|。 因此,本问题最终就转化为求解串next[j]的函数值的问题,这也是本问题的核心操作。其实现要点说明如下: (1) 求next函数值。 在next函数中,每个j值都有一个k值与之相对应,一般用next[j]的函数值表示j值对应的k值。next函数定义为 next[j]=0,当j=1时 max{k|1k满足上式,因此,可得到 next[j+1]=next[j]+1=k+1 情况2: 若tk≠tj,则表明在模式中存在 "t1…tk-1tk"≠"tj-k+1…tj-1tj" 此时,可以把计算next[j]的函数值的问题看成一个模式匹配过程。而整个串既是主串又是模式,如图35所示。 图35求next[j+1] 在当前匹配过程中,已有"t1…tk-1"="tj-k+1…tj-1"成立,则当tk≠tj时,应将模式T′向右滑动至k′=next[k](1