学习C语言的时候,老师反复说过一个事情——C语言没有字符串变量这一说!
那么,我们写的“hello world”是什么呢?
——是字符串常量
在C语言中如果要用到这种数据类型,就只能用数组来实现。
从这可以看出,串和数组的区别。字符串可以简称为串,但是其本质也是只能包含字符类型,数组可以表示不同类型,但同一个的数组的各元素类型都是相同的。可以把串看作数组的一种。
串
串(string):零个或者多个任意字符组成的有限序列
- 子串:一个串中连续字符组成的子序列,包含空串
- 真子串:不包含自身的所有子串
- 字符位置:字符在序列中的序号,1开头
- 子串位置:子串第一个字符在主串的位置
- 空格串:由一个或者多个空格组成的串,与空串不同
- 串相等:当且仅当两个串长度相等,对应位置的字符都相等
串同样可以采用顺序和链式两种表示方法,和定义一般数组一样。
关于串的相关操作,C语言有一个string.h头文件包含了大部分。比较复杂的是串的匹配算法,在文件校验,密码确认等方面有广泛应用。有BF和KMP两种算法。
BF算法
BF算法全称Brute-Froce算法,又叫暴力破解法,穷举法,就是一个个比对
比如这个字符串,就从第一个开始比对,i和j同时进行循环
发现有不一样的,则主串从第二个开始,即i-j+2的位置,子串又回到第一个开始,即为j置为1,这个过程叫做回溯
以此类推,直到匹配到完全相同的,结束条件就是主串或者子串走到尽头,主串走到尽头表示匹配失败,子串走到尽头表示匹配成功
//字符串结构 typedef struct String{ char ch[MaxLen-1]; int length; }SString; int index_BF(SString S,SString T){ int i=1,j=1;//S为主串,T子串 while(i<=S.length && j<=T.length){ if(S.ch[i]==T.ch[j]) { ++i;++j; } else { i=i-j+2; j=1; } } if(j>=T.length) return i-T.length; else return 0; }
最坏情况下,主串每个元素都要停下来让子串走完一边,复杂度为长度乘积。若设主串长度为n,子串长度为m,则O(n*m)
KMP算法
由于BF算法可能需要回溯的次数太多导致效率很低,事实上,并不是每一次都需要回溯的,比如这个例子
前4个已经匹配了,第5个不一样,但是之前出现了相等的情况,第二次比较直接挪到主串位置3上才是合理的做法,按照BF算法,显然太费时间了
所以又提出了KMP算法,减少不必要的回溯,也就是上述的第二种移动方法,复杂度可缩短到O(n+m)。
但是,怎么确定这个移动的位置呢?这里定义一个数组next[j],来存放这个值
这里举一个例子来计算一个主串的next[j]的值
后面各个位上的值依次计算
next[j]的值等于对应位置上前n-1个数中前缀与后缀相同的最大位数加一
viod get_next(SString T,int &next[]){ int i = 1; next[1] = 0;//next数组的位置0不使用,从1开始 int j = 0; while(i<T,length){ if(j==0 || T.ch[i]=T.ch[j]) { ++i;++j;//i和j同时向前推进 next[i]=j;/*i后一位其对应的next数组的值等于j向前推进的值*/ } else { j=next[j];/*如果不相等,i的位置不变,j后退到所在位置的next值*/ } } }
求出了next[]的值,然后带入原来的算法中
int index_KMP(SString S,SString T,int pos){ int i=pos,j=1;//S为主串,T子串 while(i<=S.length && j<=T.length){ if(S.ch[i]==T.ch[j]) { i++;j++; } else { j=next[j] } } if(j>=T.length) return i-T.length; else return 0; }
这就是完整的KMP算法,其实仔细比较发现区别就在于回溯方面上,KMP虽然去除了部分繁琐的操作,但还是不够完美。
改进的KMP算法
可以改进一下,这里直接引用王卓老师的源课件
大概原理就是增加一次将所在位的值与next值位的比较,如果相同的话则进一步取其next位,算法的代码如下
void get_nextval(SString T,int &nextval[]){ int i=1; int j=0; nextval[1] = 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]; } }
数组
数组
数组:按一定格式排列起来的,具有相同类型的数据元素的集合
其实数组这种类型用的比字符串多,在处理一组数据的时候,通常都会用数组,而且数组也可以用来表示字符串,所以数组还是比较常见的。
数据类型 变量名称[长度]…
数组的特点:连续顺序结构,结构固定,维数和界数不变
基本操作:初始化,销毁,取数据,修改元素值,一般不做插入和删除操作
数组可以是多维的,但是存储结构是一维的,所以这里就有了一个多维关系映射到一维的问题。以二维数组为例,可以采用“一行一行的”存取,即以行序为主(C、JAVA、BASIC);或者“一列一列的”存取,即以列序为主(FORTRAN)。
行序和列序为主其实差别不大,最主要的就是定位元素的位置,假设一个[m][n]的数组,则元素a[i][j]的位置为:
行序为主:LOC( i , j ) = LOC( 0 , 0 ) + ( n * i + j )*L
列序为主:LOC( i , j ) = LOC( 0 , 0 ) + ( m* i + j )*L
多维矩阵的存储
其实不只是多维数组,在许多结构中,存在多个数据元素相同,或者空值太多的时候,就会有许多空间看起来像是浪费了,重复的元素太多就会很没有价值。
一般可压缩的矩阵有:对称矩阵、对角矩阵、三角矩阵、稀疏矩阵
例如三角矩阵,由于有一半的部分是相同的,只需要占有一个空间就可以了,对于一个n*n的三角矩阵,压缩后就只需要n(n+1)/2+1个元素空间,节省了几乎一半。但是要注意的是,不能再用原来的公式来确定元素的位置了,要根据情况而定。
三元组表示法
对于稀疏矩阵,矩阵中的大部分元素都是0,就只需要把其中非零元素取出来,标出位置,例如

typedef struct { int i,j; //储存非零元素的行和列信息 ElementType e; //非零元素的值 } Triple; //定义三元组类型 typedef struct { int mu,nu,tu; //矩阵的行、列和非零元素的个数 Triple data[SMAX]; //三元组表 } TSMatrix;
十字链表
十字链表的特点是能够灵活方便的插入和删除元素,比起三元组多了两个指针,分别指向下一个元素,结构如下
用十字链表表示的矩阵是这样的

这样的结构在插入和删除元素的时候,就只需要修改指针就行了。但是对于非稀疏矩阵来说,就比较浪费空间了。
广义表
广义表(又称列表lists)是n>=0个元素a0,a1,a2,……的有限序列,其中的a既可以是原子,也可以是广义表。记为:
LS= ( a0 , a1 , a2 , a3 , …… , an)
常用定义有:
- 表头:LS(n>=1)的第一个元素a1就是表头
- 表尾:除表头之外的其余元素组成的表就是表尾
- 长度:最外层所包含的元素的个数
- 深度:广义表展开后括号的重数(就是套娃层数)
- 广义表可以共享和递归,递归表深度是无穷值,长度是有限值