数据结构:栈和队列及字符串中的KMP

178 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1、定义及抽象数据类型

栈是一种操作受限的线性表。

栈(Stack)定义:只允许在一端进行插入和删除操作的线性表。
栈顶(Top):在栈顶进行插入和删除操作。
栈底(Bottom):栈底部,不允许操作。

栈的数学性质:n个不同元素进栈,出栈元素不同排列个数为:在这里插入图片描述

ADT 栈(Stack)

Data 
	除了第一个和最后一个元素,每一个元素有且只有一个前驱,有且只有一个后驱。
第一个元素没有前驱,最后一个元素没有后驱。数据元素之间的关系是一一对应的关系。
Operation
	InitStack(&S):初始化栈,建立一个空栈S
	StackEmpty(S):判断栈是否为空
	ClearStack(&S):清空栈
	DestroyStack(&S):销毁栈
	GetTop(S,&e):用e返回栈顶元素
	Push(&S,e):在栈顶插入元素e
	Pop(&S,&e):删除栈顶元素
	StackLength(S):返回栈长度
endADT

2、顺序栈

使用一段连续的空间来存储栈中元素,与顺序表相似。

(1)共享栈

为了使存储空间有效利用,提出了共享栈概念。分别从空间的两端向中间进行入栈和出栈操作,也即两个栈顶向共享空间的中间延申。

3、链栈

链栈就相当于链表,但是一般链栈没有头结点。

队列

1、定义及抽象数据类型

队列也是一种操作受限的线性表,队列只允许一段进一段出。

队列(Queue):一种先进先出的线性表,只允许一段插入,另一端删除。
队头(Front):允许删除的一段。
队尾(Rear):允许插入的一段。
ADT 队列(queue)

Data 
	除了第一个和最后一个元素,每一个元素有且只有一个前驱,有且只有一个后驱。
第一个元素没有前驱,最后一个元素没有后驱。数据元素之间的关系是一一对应的关系。
Operation
	InitQueue(&Q):初始化队列,建立一个空队列Q
	QueueEmpty(Q):判断队列是否为空
	ClearQueue(&Q):清空队列
	DestroyQueue(&Q):销毁队列
	GetHead(Q,&e):用e返回队头元素
	EnQueue(&Q,e):在队尾插入元素e
	DeQueue(&Q,&e):删除队头元素
	StackLength(Q):返回栈队列长度
endADT

2、循环队列

队列的顺序存储也与顺序表相似,有一段连续的存储单元来存放队列中的元素,并且有两个指针,分别为头指针和尾指针。 顺序队列有一个缺点就是存储空间是固定的,在插入和删除的操作中,队头指针不断向后移动,从而会造成假溢出,为解决此问题,提出了循环队列的概念。

	队满条件:(Q.rear+1)%MaxSize == Q.front
	队空条件:Q.front == Q.rear
	队列长度:(Q.rear-Q.front+MaxSize) %MaxSize

3、链式队列

链式队列既是带有头指针和尾指针的单链表。

4、队列扩展

双端队列 循环链式队列

三、栈与队列应用

栈: 1、表达式求解 中缀表达式和后缀表达式 2、递归调用 函数调用 3、迷宫求解 队列: 4、树的层次遍历

一、BF匹配算法

BF模式匹配算法,又称朴素模式匹配算法,简单模式匹配算法,暴力匹配算法。对于字符串,现有一个主串和一个子串,那么子串在主串中的定位操作通常称为串的模式匹配。下面我们来了解一下BF匹配算法。 假设有一主串S = “goodgoogle”,子串T = "google",那么找到子串在主串中的位置,我们直接想到的方法就是如下:

  • 1、先将子串从主串的头部开始比较。其中直线符号为相等,闪电符号为不相等。 在这里插入图片描述
  • 2、遇到不相等的之后,将子串向右滑动一个单位,继续重新开始比较。 在这里插入图片描述
  • 3、依次类推,直到在主串中匹配到子串为止。 在这里插入图片描述 ==注:在C/C++、Java中字符串末尾以’/0‘结尾,所以当匹配到’/0‘还没有匹配上时,那就证明主串中没有子串。== 上述过程也就是我们的BF算法,因为其简单直观,我们即称其为简单算法或暴力算法。下面我们来分析一下BF算法的具体过程:

我们设主串S和子串T的下标都从0开始,i=0;j=0。 在这里插入图片描述 当T[j]=S[i]时,i和j均加一,也就是均往右移动一个单位,继续进行比较。 在这里插入图片描述 当比较到i=j=3时,T[j] != S[i]。那么子串T中j就要回溯到子串的开头,也就是j=0的位置,而主串S中的i需要回溯到比较的下一位,也就是i=i-j+1。 在这里插入图片描述 在这里插入图片描述 如果匹配成功后,返回匹配成功处串的第一个位置,如果不成功就是返回-1. 以此类推,代码如下:

int BF(char S[],char T[])  //BF算法--模式匹配
{
    int i = 0;
    int j = 0;
    while (S[i] != '\0' && T[j] != '\0')  //只要两个串不到串的末尾就一直循环
    {
        if(S[i] == T[j]){
            i++;
            j++;
        }
        else{
            i = i-j+1;   //i回溯
            j = 0;      //j回溯
        }
    }
    if (S[i] == '\0'){
        return -1;
    }
    else{
        return i-j;
    }
}

那么我们可以看到,在BF算法中,子串‘google’和主串‘goodgoogle’前三个字符是相同的,在i=j=3的时候匹配失败,需要回溯,因为前三个字符相同,并且子串中‘g’不等于后面的’o‘,所以此时子串中的’g‘一定不等于主串中的’o‘,但是BF算法照样进行了一次比较,这就造成了效率的下降。所以我们应该对其进行优化,就有了下面的KMP算法。==同时,理解KMP算法最好从BF算法的缺陷中入手,通过何种方式进行优化。==

二、KMP匹配算法

KMP算法就是为了解决BF算法的低效问题。 上面我们说过子串‘google’和主串‘goodgoogle’前三个字符是相同的,在i=j=3的时候匹配失败,需要回溯。在这里插入图片描述 在这里插入图片描述 但是我们知道,在子串’google‘中,’g‘不等于其后面的两个’0‘,所以上图两个比较判断其实是多余的,这也是为什么BF算法低效的原因,也是BF算法的缺陷。 我们可以直接省略上图两步,将’g‘与’d‘进行比较。 在这里插入图片描述 我们再讨论另一种情况: 假设S = ’abcdefgab‘,T = ’abcdex‘。我们第一次比较前五位主串字符和子串字符相等。 在这里插入图片描述

因为’a‘不等于'b','c','d','e',所以后面的四步可以省略。 在这里插入图片描述

有人可能会问,为什么我们要在i=5处重新进行比较呢,我们不是已经知道了'a'不等于'x'吗?首先T[5] != S[5],再者,我们虽然知道T[0] != T[5],但是我们却不知道T[0]和S[5]是否相等,所以要在i=5处比较。注意此处不同于上面讲的’goodgoodle‘,’google‘。在匹配失败处,子串中T[0] = T[3],而在 S = ’abcdefgab‘,T = ’abcdex‘中,匹配失败处,T[0] != T[5]。

由上面分析我们可以发现,主串i不进行回溯,只有子串中的j进行回溯。那么我们就要知道j应该回溯到那个位置呢? 我们需要对子串进行分析: 下面有一个数学描述: 在这里插入图片描述 如果想要知道证明过程,请参考[ 数据结构(C语言版)严蔚敏 吴伟民 清华大学出版社]

下图是本人查阅参考书籍进行的一些总结,若想弄清楚数学上的证明过程,则可看下图,希望对读者有一点启发。若不想,即可跳过下图,并直接学习如何根据定义来求next数组。 在这里插入图片描述

由上述next[j]定义可以求出子串的next数组的值。 在这里插入图片描述 在这里插入图片描述 那么对于T='abcabx' 在这里插入图片描述 求解next数组代码:

void getNext(char *T, int *next)  //获得next数组的值
{
	int i,j;
    j = 0;
    i = 1;
    next[1] = 0;
    while(i < T.length){
        if(j == 0 || T[i] == T[j]){    //只针对子串T的比较
            i++;
            j++;
            next[i] = j;
        }
        else{
            j = next[j];     
        }
    }
}

求解next数组的代码其实就是上述数学表述(证明)的代码实现 在我们知道next数组如何求后,KMP算法代码如下:

int KMP(char S[],char T[],int pos)  //KMP算法
{
	int i,j;
    i = pos;   /*i为主串S当前元素下标值若i不为1,则从pos值开始*/
    j = 1;    //j为子串T当前元素下标值
    //若i=j=1.则在主串和子串的第一个元素比较,也即串的元素下标均从1开始
    int next[255];
    getNext(T,next);   //获取next数组
    lenS = strlen(S);
    lenT = strlen(T);
    while(i < lenS && j < lenT){   
        if(S[i] == T[i] || j = 0){ 
            i++;
            j++;
        }
        else{
            j = next[j]
        }
    }
    if(S[i] == '\0'){
        return -1
    }
    else
        return i-lenT
}

其中j=0的情况就是子串T中的第一个元素和主串S中对应比较的元素不相等。 并且next数组的第一个元素均为0,注意是next[1]=0。

参考书目: 数据结构 (C语言版) 严蔚敏 吴伟民 清华大学出版社 大话数据结构 程杰 清华大学出版社