数据结构(王道考研) 第四章串 笔记

串的定义及基本操作

串的定义

以下是串的相关概念

  • 串,即字符串是由0个或多个字符组成的有限序列。
  • 串名
  • 串的值
  • 串的长度
  • 空串
  • 子串,串中任意个连续的字符组成的子序列
  • 主串,包含子串的串
  • 字符在主串中的位置,字符在串中的序号,没说一般默认为第一次出现的位置。

注意位序从1开始而不是从0开始

  • 子串在主串中的位置,子串的第一个字符在主串中的位置

串与线性表

串是一种特殊的线性表。串的数据对象限定为字符集(如中文字符,英文字符,数字字符,标点字符等)。但是相比线性表,串的基本操作一般以子串为操作对象。

串的基本操作

操作 描述
StrAssign(&T,chars) 赋值操作。把串T赋值为chars
Strcopy(&T,S) 复制操作,由串S复制得到串T
StrEmpty(S) 判空操作。若S为空串,则返回True,否则返回False
StrLength(S) 求串长。返回串S的元素个数
ClearString(&S) 清空操作。将S清为空串
DestroyString(&S) 销毁串。将串S销毁(回收存储空间)
Concat(&T,S1,S2) 串联接。用T返回由S1和S2联接而成的新串
SubString(&Sub,S,pos,len) 求子串。用Sub返回串S的第pos个字符起长度为len的子串
Index(S,T) 定位操作。若主串S中存在与串T相同的子串,则返回它在主串S中第一次出现的位置;否则函数值为0
StrCompare(S,T) 比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S

串的存储结构

串的顺序存储

静态数组实现(定长顺序存储)

#define MAXLEN 255     //预定义最大串长为255
typedef struct{
    char ch[MAXLEN];   //每个分量存储一个字符
    int length;        //串的实际长度
}SString;

动态数组实现(堆分配存储)

typedef struct{
    char *ch;        //按串长分配空间,ch指向串的基地址
    int length;      //串的长度
}HString; 

HString S;
S.ch=(char*)malloc(MAXLEN*sizeof(char));
S.length=0;

不同教材里顺序存储实现的细节会有些不同

数据结构(王道考研) 第四章串 笔记_第1张图片

方案一是我们刚才提到的方案,即申请一个数组并有专门的一个int类型变量记录字符串长度

方案二的优点是字符的位序和数组下标相同。缺点是由于数组是char型的,因此字符串的长度要被限制在255以内,否则char[0]表示不了那么多的数字。

方案三不记录串的长度,而是在串的末尾处插入特殊字符’\0’(对应ASCII码的0)。这种方案的缺点是想要知道串的长度需要从头到尾进行遍历

方案四是教材的方案。即舍弃char[0]不用,设置额外的int类型变量来记录字符串的长度

串的链式存储

typedef struct StringNode{
    char ch;                    //每个结点存1个字符
    struct StringNOde *next;
}StringNode,*String;

这种存储方式每个字符占1B,每个指针4B,因此存储密度较低。因此可以适当改进,让存储密度提高

typedef struct StringNode{
    char ch[4];                //每个结点存多个字符,没有字符的位置用'#'或'\0'补足
    struct StringNode *next;
}StringNode,*String;

基本操作的实现(基于静态数组)

求子串

bool SubString(SString &Sub,SString S,int pos,int len){
    //子串范围越界
    if(pos+len-1>S.length){
        return false;
    }
    for(int i=pos;i<pos+len;i++){
        Sub.ch[i-pos+1]=S.ch[i];
    }
    Sub.lengrh=len;
    return true;
}

比较操作

int Strcompare(SString S,SString T){
    for (int i=1,i<=S.length&&i<=T.length;i++){
        if(S.ch[i]!=T.ch[i]){
            return S.ch[i]-T.ch[i];
        }
    }
    //扫描过的所有字符都相同,则长度长的串更大
    return S.length-T.length;
}

定位操作

int Index(SString S,SString T){
    int i=1,n=StrLength(S),m=StrLength(T);
    SString sub;         //用于暂存子串
    while(i<=n-m+1){
        SubString(sub,S,i,m);
        if(StrCompare(sub,T)!=0){
            i++;
        }else{
            return i;   //返回子串在主串中的位置
        }
    }
    return 0;        //S中不存在与T相等的子串
}

串的朴素模式匹配算法

串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置

上一节的定位操作就是用基本操作实现模式匹配,这一节则是不使用其他的基本操作,直接使用数组元素来实现这个过程。

朴素匹配算法

int Index(SString S,SString T){
    int k=1;
    int i=k,j=1;
    while(i<=S.length&&j<=T.length){
        if(S.ch[i]==T.ch[j]){
            i++;
            j++;       //继续比较后续字符
        }else{
            k++;       //检查下一个子串
            i=k;
            j=1;
        }
    }
    if(j>T.length){
        return k;
    }else{
        return 0;
    }
}

课本的代码实现

int Index(SString S,SString T){
    int i=1,j=1;
    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;
    }
}

朴素模式匹配算法性能分析

设模式串长度为m,主串长度为n,则

  • 匹配成功的最好时间复杂度:O(m)

第一次匹配就成功

  • 匹配失败的最好时间复杂度:O(n)

全部匹配失败且每个子串的第1个个字符就与模式串不匹配。要匹配n-m+1次,时间复杂度为O(n-m+1)=O(n-m),考虑到许多情况下主串长度远大于模式串长度,时间复杂度可进一步约等于O(n)

  • 最坏时间复杂度:O(mn)

每个子串的前m-1个字符都和模式串匹配,只有第m个字符不匹配。匹配成功/匹配失败最多需要(n-m+1)*m次比较。

KMP算法

朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描制针i经常回溯,造成时间开销增大。KMP算法的改进思路是当子串和模式串不匹配时,主串指针i不回溯,只有模式串指针按照j=next[j]回溯

数据结构(王道考研) 第四章串 笔记_第2张图片

j=1时发现不匹配则令j=next[1]=0,但是右边却说要让j依然是1,这样设计是为了代码实现。我们可以先令j=0,再让i和j都同时++,这样就能让指针i往后移一位,同时j也保持为1

KMP算法的关键在于搞出一个和模式串相对应的数组next(数组怎么求等下再来探讨)

数据结构(王道考研) 第四章串 笔记_第3张图片

注意为什么要把next[1]设为0,我们可以利用这个信息做一个特殊的判断,当j=0时就说明主串的指针i应该往右移动了

求next数组

数据结构(王道考研) 第四章串 笔记_第4张图片

代码实现如下

//求模式串T的next数组
void get_next(SString T,int next[]){
    int i=1,j=0;
    next[1]=0;
    while(i<T.length){
        if(j==0||T.ch[i]==T.ch[j]){
            i++;
            j++;  
            //若pi=pj,则next[j+1]=next[j]+1
            next[i]=j;
        }else{
            //否则令j=next[j],循环继续
            j=next[j];
        }
    }
}

//KMP算法
int Index_KMP(SString S,SString T){
    int i=1,j=1;
    int next[T.length+1];
    get_next(T,next); //求模式串的next数组,时间复杂度O(m)
    while(i<=S.length&&j<=T.length){  //时间复杂度O(n)
        if(j==0||S.ch[i]==T.ch[j]){
            i++;
            j++;
        }else{
            j=next[j];
        }
    }
    if(j>T.length){
        return i-T.length;
    }else{
        return 0;
    }
}

KMP算法平均时间复杂度:O(m+n)

KMP算法的进一步优化

利用nextval数组替换next数组进行优化,可以减少无意义的对比

数据结构(王道考研) 第四章串 笔记_第5张图片
数据结构(王道考研) 第四章串 笔记_第6张图片

数据结构(王道考研) 第四章串 笔记_第7张图片

你可能感兴趣的:(数据结构与算法)