数据结构学习笔记(三)串和数组、BF与KMP算法


学习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就是表头
  • 表尾:除表头之外的其余元素组成的表就是表尾
  • 长度:最外层所包含的元素的个数
  • 深度:广义表展开后括号的重数(就是套娃层数)
  • 广义表可以共享和递归,递归表深度是无穷值,长度是有限值

					


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM