本文已参与「新人创作礼」活动,一起开启掘金创作之路。
栈
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语言版) 严蔚敏 吴伟民 清华大学出版社 大话数据结构 程杰 清华大学出版社