让数据结构不再难,一篇文章让你重新了解串、数组和广义表,以及尝试暴力BF算法和KMP算法的应用

串、数组和广义表

1.串

1.串的定义

串(String)——零个或多个任意字符组成的有限序列

  

若n=0,则为空串

串的定义:术语

  1. 子串:一个串中任意个连续字符组成的子序列(含空串),称为该串的子串。例如"abcde"子串有""、"a"、"ab"、"abc"、"abcd"和"abcde"等

    真子串是指不包含自身的所有子串

  2. 主串:包含子串的串相应地称为主串

  3. 字符位置:字符在序列中的序号为该字符在串中的位置

  4. 子串位置:子串第一个字符在主串中的位置

  5. 空格串:由一个或多个空格组成的串,与空串不同

串的类型定义

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.串的链式存储结构——块链结构

  1. 串的长度和存储空间的大小:如果字符串很长,链表申请的存储空间有限,应尽可能地让各个节点多存储字符,提高空间的利用率;

  2. 程序实现的功能:实际场景中,如果需要对存储的字符串做大量的插入或删除操作,应尽可能地减少各个节点存储字符的数量,提高程序的执行效率。

#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.二维数组

 

二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。

二维数组的逻辑结构

  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. 可以对其元素进行随机存取;

  2. 矩阵运算非常简单;存储的密度为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.广义表的性质

  1. 广义表中的数据元素有相对次序;一个直接前驱和一个直接后继

  2. 广义表的长度定义为最外层所包含元素的个数;

    如:C=(a,(b,c))是长度为2的广义表。表头为a,表尾为((b,c))

  3. 广义表的深度定义为该广义表展开后所包含括号的重数;

    A = (b,c)的深度为1,B = (A,d)的深度为2,C = (f,B,h)的深度为3;

    注意:“原子”的深度为0,“空表”的深度为1

  4. 广义表可以为其他广义表共享;如:广义表B就共享表A。在B中不必列出A的值,而是通过名称来引用,B=(A)。

  5. 广义表可以是一个递归的表。如:F(a,⑥=(a,(a,(a,…)))。注意:递归表的深度是无穷值,长度是有限值。

  6. 广义表是多层次结构,广义表的元素可以是单元索,也可以是子表,而子表的元素还可以是子表。

3.与线性表的区别

广义表可以看成是线性表的推广,线性表是广义表的特例。

广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构等各种常用的数据结构。

当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表。

另外,树和有向图也可以用广义表来表示。

由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。

4.广义表的运算

  1. 求表头GetHead(L):非空广义表的第一个元素,可以是一个原子,也可以是一个子表

  2. 求表尾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)

  1. 将主串的第pos个字符和模式串的第一个字符比较,若相等,继续逐个比较后续字符;若不等,从主串的下一字符起,重新与模式串的第一个字符比较。

  2. 直到主串的一个连续子串字符序列与模式串相等。返回值为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个字符与主串中相应字符“失配”时,在模式中需要重新和主串中的该字符进行比较的字符位置

算法剖析

 

  1. 第一位的nextval值必定为0,第二位如果于第一位相同则为0,如果不同则为1。

  2. 第三位的next值为1,那么将第三位和第一位进行比较,均为a,相同,则第三位的nextval值为第一位的next值,为0。

  3. 第四位的next值为2,那么将第四位和第二位进行比较,不同,则第四位的nextvalf值为其next值,为2。

  4. 第五位的next值为2,那么将第五位和第二位进行比较,相同,第二位的next值为1,则继续将第二位与第一位进行比较,不同,则第五位的nextval值为第二位的next值,为1。

  5. 第六位的next值为3,那么将第六位和第三位进行比较,不同,则第六位的nextvalf值为其next值,为3。

  6. 第七位的next值为1,那么将第七位和第一位进行比较,相同,则第七位的nextval值为0

  7. 第八位的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算法的案例,希望各位看官老爷们海涵啊。

猜你喜欢

转载自blog.csdn.net/Williamtym/article/details/129871134