串、数组和广义表
1.串
1.串的定义
串(String)——零个或多个任意字符组成的有限序列
若n=0,则为空串
串的定义:术语
-
子串:一个串中任意个连续字符组成的子序列(含空串),称为该串的子串。例如"abcde"子串有""、"a"、"ab"、"abc"、"abcd"和"abcde"等
真子串是指不包含自身的所有子串
-
主串:包含子串的串相应地称为主串
-
字符位置:字符在序列中的序号为该字符在串中的位置
-
子串位置:子串第一个字符在主串中的位置
-
空格串:由一个或多个空格组成的串,与空串不同
串的类型定义:
ADT String{
数据对象:D={a|a∈CharacterSet,,i=1,2,…,n,n≥0}
数据关系:R1={ka1va,>|a1va,∈D,i=1,2,…,n}
基本操作:
(1)StrAssign (&T,chars)
/串赋值
(2)StrCompare(S,T)
/串比较
(3)StrLength(S)
/求串长
(4)Concat(&T,S1,S2)
/串连结
(5)SubString(&Sub,S,pos,len)
/求子串
(6)StrCopy(&T,S)
/串拷贝
(7)StrEmpty(S)
/串判空
(8)ClearString (&S)
/清空串
(9)Index(A+S,T,pos)
/子串的位置
(11)Replace(&S,T,V)
/串替换
(12)Strlnsert(&S,pos,T)
/子串插入
(12)StrDelete(&S,pos,len)
/子串删除
(13)DestroyString(&S)
/串销毁
}
1.串的顺序存储结构
#define MAXLEN 255
typedef struct
{
char ch[MAXLEN + 1];//存储串的一维数组[0~255]
int length;//串的当前长度
}SString;
2.串的链式存储结构
优点:操作方便
缺点:存储密度较低
存储密度串值所占的内存实际分配的内存
3.串的链式存储结构——块链结构
-
串的长度和存储空间的大小:如果字符串很长,链表申请的存储空间有限,应尽可能地让各个节点多存储字符,提高空间的利用率;
-
程序实现的功能:实际场景中,如果需要对存储的字符串做大量的插入或删除操作,应尽可能地减少各个节点存储字符的数量,提高程序的执行效率。
#define CHUNKSIZE 80//块的大小可由用户定义
typedef struct Chunk
{
char ch[CHUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct
{
Chunk *head, *tail;//串头指针
int curlen;//串的当前长度
}LString;//字符串的块链结构
完整代码:
#include<iostream>
#include<cstring>
using namespace std;
#define ChunSize 3//块的大小
struct link
{
char a[ChunSize];//数据域可存放ChunSize个字符
link * next;//代表指针域,指向直接后继结点
};
//初始化块链
link * initLink(link * head, char * str)
{
int length = strlen(str);//计算出链表中使用结点的个数
int i;
link* temp = NULL;
int num = length / ChunSize;//根据字符串的长度,计算出链表中使用结点的个数
if(length % ChunSize)
{
num++;
}
head->next = NULL;
temp = head;
//初始化链表
for(int i = 0;i < length; i++)
{
int j = 0;
for(;j < ChunSize; j++)
{
if(i * ChunSize + j < length)
{
temp->a[j] = str[i * ChunSize + j];
}
else
{
temp->a[j] = '#';
}
}
if(i * ChunSize + j < length)
{
link* newLink = new link;
newLink->next = NULL;
temp->next = newLink;
temp = newLink;
}
}
return head;
}
//打印链表
void printLink(link* head)
{
link* temp = head;
while(temp)
{
int i;
for(i = 0;i < ChunSize; i++)
{
cout << temp->a[i];
}
temp = temp->next;
}
cout << endl;
}
int main()
{
link * head = new link;
head = initLink(head,"hello,i am Chinese!");
printLink(head);
return 0;
}
2.串的应用
串的应用非常广泛,过算机的非数值处理的对象太部分是字符串数据,例如:文字编辑、符号处理、各种信息处理系统等等。
1.检测病毒案例
然后检测某种病毒DNA序列是否在患者的DNA序列中出现过,一如果出现过则此人感染了该病毒,否则没有感染。 例如:假设病毒的DNA序列为baa,患者1的DNA序列为aaabbba,则感染 患者2的DNA序列为babbba,则未感染。(注意,人的DNA序列是线性的,而病毒的DNA序列是环状的)
题解:将DNA的序列拷贝两份,从baabaa中可以找出所有病毒子串,和患者DNA进行匹配。
#include<iostream>
#include<cstring>
using namespace std;
bool Index_BF(char * S, char * T)
{
int length1 = strlen(S);
int length2 = strlen(T);
for(int i = length1;i < 2*length1; i++)
{
S[i] = S[i - 3];
}
for(int m = 0;m < length1; m++)
{
int i = 0, j = 0;
while(i < length1 && j < length2)
{
cout << S[m + i] << " " << T[j] << endl;
if(S[m + i] == T[j])
{
++i;
++j;//主串和子串依次分配下一个字符
}
else
{
i = 0;
j = j - i + 1;
}
}
if(i >= length1)
return true;
}
return false;
}
void menu()
{
cout << "病毒案例" << endl;
cout << "0.退出" << endl;
cout << "1.测试" << endl;
cout << "2.清屏" << endl;
cout << "请输入:";
}
int main()
{
while(true)
{
menu();
char virus[10];
char humanDNA[20];
int val;
cin >> val;
switch(val)
{
case 0:
{
cout << "欢迎下次使用!" << endl;
return 0;
}
case 1:
{
cout << "输入病毒的DNA序列:";
cin >> virus;
cout << "请输入人的DNA序列:";
cin >> humanDNA;
cout << endl;
int pos = Index_BF(virus,humanDNA);
if(pos != 0)
{
cout << "已感染" << endl;
}
else
{
cout << "未感染" << endl;
}
break;
}
case 2:
{
system("cls");
break;
}
default:
{
cout << "输入错误,请重新输入!" << endl;
break;
}
}
}
return 0;
}
可以查看容器里的内容,估计有匹配,但是应该不能直接进行病毒的循环形式
2.串的模式匹配算法
算法目的:
确定主串中所含子串(模式串)第一次出现的位置(定位)
算法应用:
搜索引擎、拼写检查、语言翻译、数据压缩
算法种类:
BF算法(Brute-Force,又称古典的、经典的、朴素的、穷举的)
KMP算法(特点:速度快)
详细的部分将在BF算法和KMP算法上进行呈现
2.数组
数组:按一定格式排列起来的、具有相同类型的数据元素的集合。
1.一维数组
一维数组:若线性表中的数据元素为非结构的简单元素,则称为一维数组。
一维数组的逻辑结构:线性结构。定长的线性表。
声明格式:数据类型 变量名称[长度];
2.二维数组
二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。
二维数组的逻辑结构:
-
非线性结构 每一个数据元素既在一个行表中,又在一个列表中。
-
线性结构定长的线性表 该线性表的每个数据元素也是一个定长的线性表。
声明格式:数据类型 变量名称[行数] [列数]
在C语言中,一个二维数组类型也可以定义为一维数组类型(其分量类型为一维数组类型)
即:typedef elemtype array2[m][n];
等价于:typedef elemtype array1[n];
typedef array1 array2[m];
3.n维数组
三维数组:若二维数组中的元素又是一个一维数组,则称作三维数组。
推广n维数组:若n-1维数组中的元素又是一个一维数组结构,则称作n维数组。
结论:线性表结构是数组结构的一个特例,而数组结构又是线性表结构的扩展。
数组特点:结构足千定义后,维数和维界不冉玫受。
数组基本操作:除了结构的初始化和销毁之外,只有取元素和修改元素值的操作。
4.数组的顺序存储
数组特点:结构固定——维数和维界不变。
数组基本操作:初始化、销毁、取元素、修改元素值。一般不做插入和删除操作。
一般都是采用顺序存储结构来表方数组。
注意:数组可以是多维的,但存储数据元素的内存单地址是一维的,因此,在存储数组结构之前,需要解决将多维关系映射到一维关系的问题。
对于二维数组映射到一维也就是如图关系
以行序为主序:
设数组开始存储位置LOC(0,0),存储每个元素需要L个存储单元
数组元素a[i] [j]的存储位置是:LOC(i,j)=LOC(0,0)+(n * i + i) * L
例:设有一个二维数组A [m] [n] 按行优先顺序存储,假设A[0]] [0]存放位置在644(10),A[2] [2]存放位置在676(10),每个元素占一个空间 问A[3] [3] (10)存放在什么位置?(脚注(10)表示用10进制表示)
答案:692(10) 不需要死记硬背公式,灵活使用即可。
5.特殊矩阵的压缩存储
矩阵:一个由m×n个元素排成的m行n列的表。
矩阵的常规存储:将矩阵描述为一个二维数组。
矩阵的常规存储的特点:
-
可以对其元素进行随机存取;
-
矩阵运算非常简单;存储的密度为1。
不适宜常规存储的矩阵:值相同的元素很多且呈某种规律分布;零元素多。
矩阵的压缩存储:为多个相同的非零元素只分配一个存储空间;对零元素不分配空间。
压缩存储:若多个数据元素的值都相同,则只分配一个元素值的存储空间,且零元素不占存储空间。
能压缩的矩阵有对称矩阵、对角矩阵、三角矩阵、稀疏矩阵等
稀疏矩阵:矩阵中非零元素的个数较少(一般小于5%)
1.对称矩阵
特点:在n×n的矩阵a中,满足如下性质:aij=aji (1≤i,j≤n)
存储方法:只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。
对称矩阵的存储结构:对称矩阵上下三角中的元素数均为:(n+1)/2
可以以行序为主序将元素存放在一个一维数组sa[n(n+1)/2]中。
2.三角矩阵
特点:对角线以下(或者以上)的数据元素(不包括对角线)全部为常数c。
存储方法:重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素
空间:sa[1....n(n+1)/2+1]
3.稀疏矩阵
稀疏矩阵:设在m×n的矩阵中有t个非零元素。
令δ = t/(m×n),当δ ≤ 0.05时称为稀疏矩阵。
三元顺序表
三元组顺序表又称有序的双下标法。
三元组顺序表的优点:非零元在表中按行序有序存储,因此便于进行依行顺序处理的矩阵运算。
三元组顺序表的缺点:不能随机存取。若按行号存取某一行中的非零元,则需从头开始进行查找。
稀疏矩阵的链式存储结构:十字链表
优点:它能够灵活地插×因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的各种运算。
3.广义表
1.广义表的定义
广义表(又称列表Lists)是n>0个元素a0,a1,....,an-1的有限序列,其中每一个ai或者是原子,或者是一个广义表。
广义表内的元素不一定是同一类型的数据,也可能是自己定义自己。
例如:中国举办的国际足球邀请赛,参赛以名单可表示如下: (阿根廷,巴西,德国,法国,( 空元素 ),西班牙,意大利,英国,(国家队,山东鲁能,广州恒大))
广义表:通常记作:LS = (a1, a2, ... , an);
其中:LS为表名,n为表的长度,每一个ai为表的元素。
习惯上,一般用大写字母表示广义表,小写字母表示原子。
表头:若LS非空,则其第一个元素a就是表头。记作head(L) = a。注:表头可以是原子,也可以是子表。
表尾:除表头之外的其它元素组成表。记作tail(L)=(a2 , ...., an)。 注:表尾不是最后一个元素,而是一个子表。
2.广义表的性质
-
广义表中的数据元素有相对次序;一个直接前驱和一个直接后继
-
广义表的长度定义为最外层所包含元素的个数;
如:C=(a,(b,c))是长度为2的广义表。表头为a,表尾为((b,c))
-
广义表的深度定义为该广义表展开后所包含括号的重数;
A = (b,c)的深度为1,B = (A,d)的深度为2,C = (f,B,h)的深度为3;
注意:“原子”的深度为0,“空表”的深度为1
-
广义表可以为其他广义表共享;如:广义表B就共享表A。在B中不必列出A的值,而是通过名称来引用,B=(A)。
-
广义表可以是一个递归的表。如:F(a,⑥=(a,(a,(a,…)))。注意:递归表的深度是无穷值,长度是有限值。
-
广义表是多层次结构,广义表的元素可以是单元索,也可以是子表,而子表的元素还可以是子表。
3.与线性表的区别
广义表可以看成是线性表的推广,线性表是广义表的特例。
广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构等各种常用的数据结构。
当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表。
另外,树和有向图也可以用广义表来表示。
由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。
4.广义表的运算
-
求表头GetHead(L):非空广义表的第一个元素,可以是一个原子,也可以是一个子表
-
求表尾GetTail(L):非空广义表除去表头元素以外其它元素所构成的表,表尾一定是一个表。
看这张图,是不是觉得很线段密集,实际上暴力算法就像这样,每个元素不管三七二十一,都和目标部分进行比较。
BF算法
1.BF算法定义
Brute-Force简称为BF算法,亦称简单匹配算法。采用穷举法的思路。
BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。
2.BF时间复杂度
例:S = '0000000001',T = '0001',pos = 1
若n为主串长度,m为子串长度,最坏情况是主串前面n-m个位置都部分匹配到子串的最后一位,即在此这个n-m位各比较了m次。最后m位也比较1次
总次数为: ( n - m ) * m + m = (n - m + 1) * m
若m<<n,则算法复杂度O(n * m)
3.BF算法例题
1.S和T
例如,设目标串S=“aaaaab”,模式串T=“aaab”。
S的长度为n(n=6),T的长度为m(m=4)。
BF算法的匹配过程如下:
Index(S,T,pos)
-
将主串的第pos个字符和模式串的第一个字符比较,若相等,继续逐个比较后续字符;若不等,从主串的下一字符起,重新与模式串的第一个字符比较。
-
直到主串的一个连续子串字符序列与模式串相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。否则,匹配失败,返回值0。
代码实现:
#include<iostream>
#include<cstring>
using namespace std;
bool Index_BF(char * S, char * T)
{
int length1 = strlen(S);
int length2 = strlen(T);
int i = 0, j = 0;
while(i < length1 && j < length2)
{
cout << S[i] << " " << T[j] << endl;
if(S[i] == T[j])
{
++i;
++j;//主串和子串依次匹配下一个字符
}
else
{
i = i - j + 1;
j = 0;//主串和子串指针回溯重新开始下一次匹配
}
}
if(j >= length2)
return true;
else
return false;
}
int main()
{
char S[] = "aaaaab";
char T[] = "aaab";
int pos = Index_BF(S,T);
if(pos == 0)
{
cout << "不是子串!" << endl;
}
else
{
cout << "是子串!" << endl;
}
return 0;
}
string容器的比较方法:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str1 = "aaaaab";
int pos = str1.find("aaab");
if(pos == -1)
{
cout << "未找到所需字符串!" << endl;
}
else
{
cout << "找到所需字符串!" << endl;
}
return 0;
}
上面的病毒案例实际上就是使用了暴力算法实现的
检测病毒案例
然后检测某种病毒DNA序列是否在患者的DNA序列中出现过,一如果出现过则此人感染了该病毒,否则没有感染。 例如:假设病毒的DNA序列为baa,患者1的DNA序列为aaabbba,则感染 患者2的DNA序列为babbba,则未感染。(注意,人的DNA序列是线性的,而病毒的DNA序列是环状的)
题解:将DNA的序列拷贝两份,从baabaa中可以找出所有病毒子串,和患者DNA进行匹配。
#include<iostream>
#include<cstring>
using namespace std;
bool Index_BF(char * S, char * T)
{
int length1 = strlen(S);
int length2 = strlen(T);
for(int i = length1;i < 2*length1; i++)
{
S[i] = S[i - 3];
}
for(int m = 0;m < length1; m++)
{
int i = 0, j = 0;
while(i < length1 && j < length2)
{
cout << S[m + i] << " " << T[j] << endl;
if(S[m + i] == T[j])
{
++i;
++j;//主串和子串依次分配下一个字符
}
else
{
i = 0;
j = j - i + 1;
}
}
if(i >= length1)
return true;
}
return false;
}
void menu()
{
cout << "病毒案例" << endl;
cout << "0.退出" << endl;
cout << "1.测试" << endl;
cout << "2.清屏" << endl;
cout << "请输入:";
}
int main()
{
while(true)
{
menu();
char virus[10];
char humanDNA[20];
int val;
cin >> val;
switch(val)
{
case 0:
{
cout << "欢迎下次使用!" << endl;
return 0;
}
case 1:
{
cout << "输入病毒的DNA序列:";
cin >> virus;
cout << "请输入人的DNA序列:";
cin >> humanDNA;
cout << endl;
int pos = Index_BF(virus,humanDNA);
if(pos != 0)
{
cout << "已感染" << endl;
}
else
{
cout << "未感染" << endl;
}
break;
}
case 2:
{
system("cls");
break;
}
default:
{
cout << "输入错误,请重新输入!" << endl;
break;
}
}
}
return 0;
}
可以查看容器里的内容,估计有匹配,但是应该不能直接进行病毒的循环形式
相比之下,KMP算法思想更深邃,但是不是很容易理解的。
KMP算法
KMP算法是D.E.Knuth、J.H.Morris和VR.Pratt共同提出的,简称KMP算法。
该算法较BF算法有较大改进,从而使算法效率有了某种程度的提高。
KMP算法设计思想
利用部分已匹配的结果而加快模式串的滑动速度,且主串S的指针i不必回溯,可提速到O(n+m)。
为此,定义next[j]函数,表明当模式中第j个字符与主串中相应字符“失配”时,在模式中需要重新和主串中的该字符进行比较的字符位置
算法剖析:
-
第一位的nextval值必定为0,第二位如果于第一位相同则为0,如果不同则为1。
-
第三位的next值为1,那么将第三位和第一位进行比较,均为a,相同,则第三位的nextval值为第一位的next值,为0。
-
第四位的next值为2,那么将第四位和第二位进行比较,不同,则第四位的nextvalf值为其next值,为2。
-
第五位的next值为2,那么将第五位和第二位进行比较,相同,第二位的next值为1,则继续将第二位与第一位进行比较,不同,则第五位的nextval值为第二位的next值,为1。
-
第六位的next值为3,那么将第六位和第三位进行比较,不同,则第六位的nextvalf值为其next值,为3。
-
第七位的next值为1,那么将第七位和第一位进行比较,相同,则第七位的nextval值为0
-
第八位的next值为2,那么将第八位和第二位进行比较,不同,则第八位的nextval值为其next值,为2。
KMP算法:
int Index_KMP(SString S, SString T , int pos)
{
i = pos, j = 1;
while(i < S.length && j < T.length)
{
if(j == 0 || S.ch[i] == T.ch[j])
{
i++;
j++;
}
else
{
j = next[j];//i不变,j后退
}
}
if(j > T.length) return i - T.length;//匹配成功
else return 0;//返回匹配不成功标志
}
void get_next(SString T, int &next[])
{
i = 1;
next[1] = 0;
j++;
while(i < T.length)
{
if(j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
void get_nextval(SString T, int &nextval[])
{
i = 1;
nextval[1] = 0;
j = 0;
while(i < T.length)
{
if(j === 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
if(T.ch[i] != T.ch[j]) nextval[i] = j;
else nextval[i] = nextval[j];
}
else j = nextval[j];
}
}
还没能来得及编写相应的KMP算法的案例,希望各位看官老爷们海涵啊。