文章目录
串,也叫字符串(计算机里,说串就是说字符串,没有数字串噻),是0或多个字符组成的有限 序列(相邻字符具有前驱后继关系)。
相同:听着是不是很像线性表,串的逻辑结构确实和线性表相似,一对一,前驱后继,只不过串只存字符,就算串中存的是“123”,里面的每一个元素也都是字符,而并不是数字。
不同:就因为存储的是字符,所以串和线性表的基本操作差距非常大,线性表基本操作是添删改查,关注的是单个元素。但是串关注的却是子串(非单个字符)的应用,比如查找子串位置,得到指定位置子串,替换子串等操作。
没错,字符串也是一种数据结构,字符的存储形式嘛。
很多高级语言都有串的函数可以直接调用。
串相关的数据结构,最喜欢研究的就是回文,中国古代就已经有回文诗了:
倒过来
计算机最开始被发明时只用作数值计算,但是后来逐渐徐亚偶作越来越多的非数值处理,所以必须引入字符串的概念。
搜索引擎的字符串查找匹配:
串的比较:串的大小取决于挨个字母的先后顺序
比如silly和stupid,i在t前面(本质上是因为串的比较是通过比较字符之间的编码来进行的),所以silly < stupid 。字符编码就是字符在对应字符集的序号。
用英语的国家,用ASCII字符集就足够了。但是我们中国光汉字就大几千个,还有多种少数民族语言,所以有了Unicode字符集,用16位编码表示一个字符,所以总共可以表示2的16次方个字符,约65万多个。但是为了和ASCII兼容,所以Unicode字符集的前256个字符和ascii一样。
其实用字典查单词就需要比较字符串的大小,前面的单词更小。如果不用纸质字典,其实电子词典查找单词的实现原理,还是用了字符串这种数据结构。
两串相等n=m
n小于m (分两种)
串的抽象数据类型
基本操作比如:查找子串位置,得到指定位置子串,替换子串等操作
ADT string
Data //串的元素是单个字符,相邻元素具有前驱后继关系
Operation
StrAssign(T, *chars);//生成一个值等于字符串常量chars的串T
StrCopy(T, S);//串S存在,由串S复制得串T
ClearString(S);//串S存在,清空它
StringEmpty(S);
StrLength(S);
StrCompare(S, T);//若S>T,返回值大于0;S=T,返回0;S<T,返回值小于0
Concat(T, S1, S2);//用T返回S1和S2连接而得的子串
SubString(Sub, S, pos, len);//1<=pos<=StrLength(S),用Sub返回串S的第pos个字符起长度为len的子串
Index(S, T, pos);//返回T在S中第pos之后第一次出现的位置,如果S没有子串T则返回0
StrInsert(S, pos, T);//在串S的第pos个字符之前插入串T
StrDelete(S, pos, len);//从串S中删去第pos个字符起长度为len的子串
endADT
/*T是非空串,若主串S中第pos个字符之后存在与T相等的子串,则返回第一个这样的子串在S中的位置,否则返回0*/
int Index(String S, String T, int pos)
{
int m, n, i;
String sub;
if (pos > 0)
{
m = StrLength(S);
n = StrLength(T);
i = pos;
while (i <= m-n+1)
{
SubString(sub, S, i, m);
if (StrCompare(sub, T) != 0)
{
++i;//两串不相等
}
else
{
return i;
}
}
}
return 0;
}
串的存储结构
和线性表一样,分为两种。各有千秋,没有一个是完美的。
顺序存储结构,用定长数组实现
用一组地址连续的存储单元
要申请一个长度为n的数组,然后把串存在里面,但是还需要知道串的结束,有两个办法,C语言喜欢在字符串末尾加一个\0字符,如果要知道串的长度,则需要遍历一遍串;还有的语言倾向于把串的长度专门存在一个位置,占用一个字符的位置。两种办法都要使用一个字符位置以确定长度,但个人觉得C语言的方式不如后者,因为不用遍历。
顺序存储由于使用数组,所以很不灵活,这个不灵活总会遇到各种问题,比如Concat, StrInsert等方法都很有可能使得串序列的长度超出了数组大小,造成内存越界,很不安全。如果想要坚决杜绝内存越界,针对这种上溢,可以要么给出错误提示,要么直接把超出的部分截断扔掉,但是很明显,这两种解决办法都不是釜底抽薪,根本矛盾还是存在。
可是字符串的操作中,几乎到处都要用到Concat, StrInsert等方法啊,所以顺序存储这么死板的存储方式在需要用这些方法时,驾驭不了字符串多变的灵魂。
但是顺序结构也有好处,因为串的基本元素是一个个字符,链式结构需要让一个结点存储一个字符或者接个字符,同时需要耗费多一些内存来存指针。并且方法们的代码也要不好写一点点。
动态链式存储结构,用堆内存
所以我们对字符串,要用动态分配内存的方式存储。计算机有一个内存区叫做自由存储区,也叫做堆,C的malloc和free,C++的new和delete管理的就是这个堆内存区的存储单元。
一般让一个结点只放一个字符有点浪费空间了,毕竟每一个结点都需要一个指针。所以一般会让每一个结点存储多个字符,但是每一个结点的长度是一样的,相当于每一个结点是一个小数组,最后一个结点如果占用不完就补充#号占位置。
其实,在方法的代码编写上,串的链式结构并不是很香,他的顺序结构更香一点,但是链式结构确实可以避免内存越界这种严重问题。所以两种存储方式对于串来说真的是各有千秋,分庭抗礼。说不出谁压谁一头。
串的模式匹配算法:子串的定位操作
即找一个单词在一篇文章中的定位。
串的模式匹配算法是串中最重要的操作之一。
朴素版,纯粹遍历
遍历是大多数人首先想到的办法,对于这个问题,基本也可能是普通人能想到的唯一办法了。但是聪明的科学家研究出了后面要说的KMP算法,大幅提高了子串定位的效率。
这其实就是上面的Index函数,但是上面用了很多函数,这里纯粹自己写遍历
/*返回子串T在主串S中第pos个字符之后的位置,若不存在,则函数返回值为0*/
int Index(String S, String T, int pos)
{
int i = pos;
int j = 1;
while (i <= S[0] && j <= T[0])
{
if (S[i] == T[j])
{
++i;
++j;
}
else
{
i = i-j+2;//i退回到上次匹配首位的下一位
j = 1;//j退回到子串T的首位
}
}
if (j > T[0])
{
return i - T[0];
}
else
return 0;
}
时间复杂度
- 最好:O(1),比如GoogleGood中找Google,一下就找到了,一上来就匹配成功
- 最坏:O((n-m+1)*m),即每一次不成功的匹配都要遍历到串T的最后一个字符才知道,比如
- 比最好情况略差的:每次不匹配都在首字母就发现了,因此不用对T串循环,比如在abcdefgood中找good,时间复杂度是O(n+m),n是主串长度,m是子串长度。根据等概率原则??平均需要(n+m)/2次查找,所以时间复杂度是O(n+m)。
可以看到,这种通过直接遍历的方式,是非常低效的,所以还是需要动动脑筋,想出更高明的解决办法。