第3章 栈和队列
本章主要介绍以下内容:
1.栈的概念、存储结构及其基本操作
2.队列的概念、存储结构及其基本操作
3.栈与队列的应用举例
4.串的定义、存储结构和基本运算、模式匹配
课时分配:
1四个学时,2、3两个学时,4两个学时,上机两个学时
重点、难点:
栈的存储结构及其基本操作、队列存储结构及其基本操作、模式匹配
第一节 栈
1.栈的定义
栈是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端进行。如下所示:
结论:后进先出(Last In First Out),简称为LIFO线性表。
举例1:家里吃饭的碗,通常在洗干净后一个一个地落在一起存放,在使用时,若一个一个地拿,一定最先拿走最上面的那只碗,而最后拿出最下面的那只碗。
举例2:在建筑工地上,使用的砖块从底往上一层一层地码放,在使用时,将从最上面一层一层地拿取。
下面我们先给出栈结构的基本操作:
(1)初始化栈 InitStack(S)
(2)入栈 Push(S,elem)
(3)出栈 Pop(S,elem)
(4)获取栈顶元素内容 GetTop(S,elem)
(5)判断栈是否为空 StackEmpty(S)
2.栈的顺序存储
栈的顺序存储结构是用一组连续的存储单元依次存放栈中的每个数据元素,并用起始端作为栈底。
类型定义如下所示:
#define MAX_STACK 10 //栈的最大数据元素数目
typedef struct stack{
Elemtype elem[MAX_STACK]; //存放栈中数据元素的存储单元
int top; //栈顶指针
}STACK;
基本操作算法:
(1)初始化栈
void InItStack(STACK *S)
{ s->top=-1; }
(2)入栈
void Push(STACK *S,Elemtype elem)
{
if (S->top==MAX_STACK-1) exit("Stack is full");
else S->elem[++S->top]=elem;
}
(3)出栈
void Pop(STACK *S,Elemtype *elem)
{
if (StackEmpty(*S)) exit("Stack is empty");
else *elem=S->elem[S->top--];
}
(4)获取栈顶元素内容
void GetTop(STACK S,Elemtype *elem)
{
if (StackEmpty(S)) exit("Stack is empty");
else *elem=S.elem[S.top];
}
(5)判断栈S是否为空
int StackEmpty(STACK S)
{
if(S.top==-1) return TRUE;
else FALSE;
}
结论:由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题,但栈容量难以扩充的弱点仍就没有摆脱。
3.栈的链式存储
若是栈中元素的数目变化范围较大或不清楚栈元素的数目,就应该考虑使用链式存储结构。人们将用链式存储结构表示的栈称作"链栈"。链栈通常用一个无头结点的单链表表示。如图所示:
下面我们将给出链栈各项基本操作的算法。
(1)初始化栈S
void InitStack(STACK *S)
{
S->top=NULL;
}
(2)入栈
void Push(STACK *S,Elemtype elem)
{
p=(LINKLIST*)malloc(sizeof(LINKLIST));
if (!p) exit(OVERFLOW);
else { p->elem=elem;
p->next=S->top;
S->top=p;
}
}
(3)出栈
void Pop(STACK*S, Elemtype *elem)
{
if(StackEmpty(*S)) exit("Stack is empty");
else {
*elem=S->top->elem;
p=S->top;
S->top=p->next; free(p);
}}
(4)获取栈顶元素内容
void GetTop(STACK S,Elemtype *elem)
{
if(StackEmpty(S)) exit("Stack is empty");
else *elem=S.top->elem;
}
(5)判断栈S是否空
int StackEmpty(STACK S)
{
if(S.top==NULL) return TRUE;
else FALSE;
}
4.栈的应用举例
【举例1】将从键盘输入的字符序列逆置输出
比如,从键盘上输入:tset a si sihT;算法将输出:This is a test。
下面我们给出解决这个问题的完整算法。
typedef char Elemtype;
void ReverseRead( )
{
STACK S; //定义一个栈结构S
char ch; InitStack(&S); //初始化栈
while((ch=getchar())!='\n') //从键盘输入字符,直到输入换行 //符为止
Push(&S ,ch); //将输入的每个字符入栈
while(!StackEmpty(S)) { //依次退栈并输出退出的字符
Pop(&S,&ch);
putchar(ch);
}
putchar('\n');
}
【举例2】十进制数值转换成二进制
使用展转相除法将一个十进制数值转换成二进制数值。即用该十进制数值除以2,并保留其余数;重复此操作,直到该十进制数值为0为止。最后将所有的余数反向输出就是所对应的二进制数值。
比如:(692)10 = (1010110100)2,其展转相除的过程如图所示:
下面给出解决这个问题的完整算法。
void Decimal _ Binary ( )
{
STACK S; //定义栈结构S
InitStack(&S); //初始化栈S
scanf("%d",data); //输入十进制正整数
while (data) {
Push(&S,data%2); //余数入栈
data/=2; //被除数data整除以2,得到新的被除数
}
while(!StackEmpty(S)) { //依次从栈中弹出每一个余数,并输出
Pop(&S,&data);
printf("%d",data);
}
}
【举例3】检验表达式中的括号匹配情况
假设在一个算术表达式中,可以包含三种括号:圆括号"("和")",方括号"["和"]"和花括号"{"和"}",并且这三种括号可以按任意的次序嵌套使用。比如,...[...{...}...[...]...]...[...]...(...)..。现在需要设计一个算法,用来检验在输入的算术表达式中所使用括号的合法性。
算术表达式中各种括号的使用规则为:出现左括号,必有相应的右括号与之匹配,并且每对括号之间可以嵌套,但不能出现交叉情况。我们可以利用一个栈结构保存每个出现的左括号,当遇到右括号时,从栈中弹出左括号,检验匹配情况。在检验过程中,若遇到以下几种情况之一,就可以得出括号不匹配的结论。
(1)当遇到某一个右括号时,栈已空,说明到目前为止,右括号多于左括号;
(2)从栈中弹出的左括号与当前检验的右括号类型不同,说明出现了括号交叉情况;
(3)算术表达式输入完毕,但栈中还有没有匹配的左括号,说明左括号多于右括号。
下面是解决这个问题的完整算法。
typedef char Elemtype;
int Check( )
{
STACK S; //定义栈结构S
char ch;
InitStack(&S); //初始化栈S
while((ch=getchar())!='\n') { //以字符序列的形式输入表达式
switch (ch) {
case (ch=='('||ch== '['||ch== '{'): Push(&S,ch);break; //遇左括号入栈
//在遇到右括号时,分别检测匹配情况
case (ch== ')'): if(StackEmpty(S)) retrun FALSE;
else {Pop(&S,&ch);
if (ch!= '(') return FALSE; }
break;
case (ch== ']'): if(StackEmpty(S)) retrun FALSE;
else {Pop(&S,&ch);
if(ch!= '[') return FALSE; }
break;
case (ch== '}'): if(StackEmpty(S)) retrun FALSE;
else {Pop(&S,&ch);
if(ch!= '{') return FALSE; }
break;
default:break;
}
}
if(StackEmpty(S)) return TRUE;
else return FALSE;
}
第二节 队列
1.队列的定义
插入端和删除端都是浮动的。通常我们将插入端称为队尾,用一个"队尾指针"指示;而删除端被称为队头,用一个"队头指针"指示。
结论:先进先出(First In First Out),简称为FIFO线性表。
举例1:到医院看病,首先需要到挂号处挂号,然后,按号码顺序救诊。
举例2:乘坐公共汽车,应该在车站排队,车来后,按顺序上车。
举例3:在Windows这类多任务的操作系统环境中,每个应用程序响应一系列的"消息",像用户点击鼠标;拖动窗口这些操作都会导致向应用程序发送消息。为此,系统将为每个应用程序创建一个队列,用来存放发送给该应用程序的所有消息,应用程序的处理过程就是不断地从队列中读取消息,并依次给予响应。
下面我们给出队列结构的基本操作:
(1)初始化队列 InitQueue(Q)
(2)入队 EnQueue(Q,elem)
(3)出队 DeQueue(Q,elem)
(4)获取队头元素内容 GetFront(Q,elem)
(5)判断队列是否为空 QueueEmpty(Q)
2.队列的顺序存储
假设为队列开辟的数组单元数目为MAX_QUEUE,在C语言中,它的下标在0~MAX_QUEUE-1之间,若增加队头或队尾指针,可以利用取模运算(一个整数数值整除以另一个整数数值的余数)实现。如下所示:
front=(front+1)%MAX_QUEUE;
rear=(rear+1)%MAX_QUEUE;
当front或rear为MAXQUEUE-1时,上述两个公式计算的结果就为0。这样,就使得指针自动由后面转到前面,形成循环的效果。
队空和队满的标志问题:
队列变为空,队头和队尾指针相等。
解决方法:一是为队列另设一个标志,用来区分队列是"空"还是"满";二是当数组只剩下一个单元时就认为队满,此时,队尾指针只差一步追上队头指针,即:(rear+1)%MAX_QUEUE==front。
类型定义:
#define MAX_QUEUE 10 //队列的最大数据元素数目
typedef struct queue{ //假设当数组只剩下一个单元时认为队满
Elemtype elem[MAX_QUEUE]; //存放队列中数据元素的存储单元
int front,rear; //队头指针、队尾指针
}QUEUE;
各项基本操作算法。
(1)初始化队列Q
void InitQueue(QUEUE *Q)
{
Q->front=-1;
Q->rear=-1;
}
(2)入队
void EnQueue(QUEUE *Q,Elemtype elem)
{
if((Q->rear+1)%MAX_QUEUE==Q->front) exit(OVERFLOW);
else { Q->rear=(Q->reasr+1)%MAX_QUEUE;
Q->elem[Q->rear]=elem; }
}
(3)出队
void DeQueue(QUEUE*Q,Elemtype *elem)
{
if(QueueEmpty(*Q)) exit("Queue is empty.");
else {
Q->front=(Q->front+1)%MAX_QUEUE;
*elem=Q->elem[Q->front];
}
}
(4)获取队头元素内容
void GetFront(QUEUE Q,Elemtype *elem)
{
if(QueueEmpty(Q)) exit("Queue is empty.");
else *elem=Q.elem[(Q.front+1)%MAX_QUEUE];
}
(5)判断队列Q是否为空
int QueueEmpty(Queue Q)
{
if(Q.front==Q.rear) return TRUE;
else return FALSE;
}
3.队列的链式存储
入队需要执行下面三条语句:
s->next=NULL; rear->next=s;rear=s;
下面是在C语言中,实现队列链式存储结构的类型定义:
type struct linklist { //链式队列的结点结构
Elemtype Entry; //队列的数据元素类型
struct linklist *next; //指向后继结点的指针
}LINKLIST;
typedef struct queue{ //链式队列
LINKLIST *front; //队头指针
LINKLIST *rear; //队尾指针
}QUEUE;
下面我们给出链式队列的基本操作算法。
(1)初始化队列Q
void InitQueue(QUEUE *Q)
{
Q->front=(LINKLIST*)malloc(sizeof(LINKLIST));
if (Q->front==NULL) exit(ERROR);
Q->rear= Q->front;
}
(2)入队
void EnQueue(QUEUE *Q,Elemtype elem)
{
s=(LINKLIST*)malloc(sizeof(LINKLIST));
if(!s) exit(ERROR);
s->elem=elem;
s->next=NULL;
Q->rear->next=s;
Q->rear=s;
}
(3)出队
void DeQueue(QUEUE *Q,Elemtype *elem)
{
if(QueueEmpty(*Q)) exit(ERROR);
else {
*elem=Q->front->next->elem;
s=Q->front->next;
Q->front->next=s->next;
free(s);
}
}
(4)获取队头元素内容
void GetFront(QUEUE Q,Elemtype *elem)
{
if(QueueEmpty(Q)) exit(ERROR);
else *elem=Q->front->next->elem;
}
(5)判断队列Q是否为空
int QueueEmpty(QUEUE Q)
{
if(Q->front==Q->rear) return TRUE;
else return FALSE;
}
4.队列的应用举例
【举例1】汽车加油站。
随着城市里汽车数量的急速增长,汽车加油站也渐渐多了起来。通常汽车加油站的结构基本上是:入口和出口为单行道,加油车道可能有若干条。每辆车加油都要经过三段路程,第一段是在入口处排队等候进入加油车道;第二段是在加油车道排队等候加油;第三段是进入出口处排队等候离开。实际上,这三段都是队列结构。若用算法模拟这个过程,就需要设置加油车道数加2个队列。
【举例2】模拟打印机缓冲区。
在主机将数据输出到打印机时,会出现主机速度与打印机的打印速度不匹配的问题。这时主机就要停下来等待打印机。显然,这样会降低主机的使用效率。为此人们设想了一种办法:为打印机设置一个打印数据缓冲区,当主机需要打印数据时,先将数据依次写入这个缓冲区,写满后主机转去做其他的事情,而打印机就从缓冲区中按照先进先出的原则依次读取数据并打印,这样做即保证了打印数据的正确性,又提高了主机的使用效率。由此可见,打印机缓冲区实际上就是一个队列结构。
【举例3】CPU分时系统
在一个带有多个终端的计算机系统中,同时有多个用户需要使用CPU运行各自的应用程序,它们分别通过各自的终端向操作系统提出使用CPU的请求,操作系统通常按照每个请求在时间上的先后顺序,将它们排成一个队列,每次把CPU分配给当前队首的请求用户,即将该用户的应用程序投入运行,当该程序运行完毕或用完规定的时间片后,操作系统再将CPU分配给新的队首请求用户,这样即可以满足每个用户的请求,又可以使CPU正常工作。
第三节 串
1.串的定义和基本运算
串是字符串的简称。它是一种在数据元素的组成上具有一定约束条件的线性表,即要求组成线性表的所有数据元素都是字符,所以,人们经常又这样定义串:串是一个有穷字符序列。
串一般记作:
s= "a1a2...an" (n30)
其中,s是串的名称,用双引号("")括起来的字符序列是串的值;ai可以是字母、数字或其他字符;串中字符的数目n被称作串的长度。当n=0时,串中没有任何字符,其串的长度为0,通常被称为空串。
s1= ""
s2= " "
s1中没有字符,是一个空串;而s2中有两个空格字符,它的长度等于2,它是由空格字符组成的串,一般称此为空格串。
概念:
子串、主串:串中任意连续的字符组成的子序列被称为该串的子串。包含子串的串又被称为该子串的主串。
例如,有下列四个串a,b,c,d:
a= "Welcome to Beijing"
b= "Welcome"
c= "Bei"
d= "welcometo"
子串的位置:子串在主串中第一次出现的第一个字符的位置。
两个串相等:两个串的长度相等,并且各个对应的字符也都相同。
例如,有下列四个串a,b,c,d:
a= "program"
b= "Program"
c= "pro"
d= "program "
串的基本操作:
(1)
创建串 StringAssign (s,string_constant)
(2)判断串是否为空 StringEmpty(s)
(3)计算串长度 Length(s)
(4)串连接 Concat(s1,s2)
(5)求子串 SubStr(s1,s2start,len)
(6)串的定位 Index(s1,s2)
例如1:将s2串插入到串s1的第i个字符后面。
SubStr(s3,s1,1,i);
SubStr(s4,s1,i+1,Length(s1)-i);
Concat(s3,s2);
Concat(s3,s4);
StringAssign (s1,s3);
例如2:删除串s中第i个字符开始的连续j个字符。
SubStr(s1,s,1,i-1);
SubStr(s2,s,i+j,Length(s)-i-j+1);
Concat(s1,s2);
StringAssign(s,s1);
2.串的存储结构
2.1顺序存储结构
串的顺序存储结构与线性表的顺序存储类似,用一组连续的存储单元依次存储串中的字符序列。在C语言中,有两种实现方式:
第一种是事先定义字符串的最大长度,字符串存储在一个定长的存储区中。类型定义如下所示:
#define MAX_STRING 255
//0号单元存放串的长度,字符从1号单元开始存放
type unsigned char String[MAX_STRING];
第二种是在程序执行过程中,利用标准函数malloc和free动态地分配或释放存储字符串的存储单元,并以一个特殊的字符作为字符串的结束标志,它的好处在于:可以根据具体情况,灵活地申请适当数目的存储空间,从而提高存储资源的利用率。类型定义如下所示:
typedef struct{
char *str;
int length;
}STRING;
不同的定义形式,算法中的处理也略有不同。下面我们将给出在第二种顺序存储方式下串的几个基本操作的算法。
(1)
串的赋值
int StringAssign(STRING*s,char *string_constant)
{
if(s->str) free(s->str); //若s已经存在,将它占据的空间释放掉
for(len=0,ch=string_constant;ch;len++,ch++); //求 string_constant串的长度
if(!len) { s->str=(char*)malloc(sizeof(char));s->str[0]='\0'; s->length=0; } //空串
else {
s->str=(char*)malloc((len+1)*sizeof(char)); //分配空间
if(!s->str) return ERROR;
s->str[0..len]=string_constant[0..len]; //对应的字符赋值
s->length=len; //赋予字符串长度
}
return OK;
}
(2)判断串是否为空
int StringEmpty(STRING s)
{
if(!s.length) return TRUE;
else return FALSE;
}
(3)求串的长度
int Length(STRING s)
{
return s.length;
}
(4)串连接
int Concat(STRING *s1,STRING s2)
{
STRING s;
StringAssign(&s,s1->str); //将s1原来的内容保留在s中
len=Length(s1)+Length(s2); //计算s1和s2的长度之和
free(s1->str); //释放s1原来占据的空间
s1->str=(char*)malloc((len+1)*sizeof(char));//重新为s1分配空间
if (!s1) return ERROR;
else { //连接两个串的内容
s1->str[0..Length(s)-1]=s.str[0..Length(s)-1)];
s1->str[Length(s)..len+1]=s2.str[0..Length(s2)];
s1->length=len;
free(s->str); //释放为临时串s分配的空间
return OK;
}
}
(5)求子串
int SubStr(STRING *s1,STRING s2,int start,int len)
{
len2=Length(s2); //计算s2的长度
if (start<1||start>len2||len2<=0||len>len2-start+1) { //判断start和len的合理性
s1->str=(char*)malloc(sizoef(char));s1->str[0]='\0';s1- >length=0;return ERROR;}
s1->str=(char*)malloc((len+1)*sizeof(char));
if (!s1.str) return ERROR;
s1->str[0..len-1]=s2.str[start-1..start+len -2];
s1->str[len]='\0';
s1->length=len;
return OK;
}
(6)串的定位
int Index(STRING s1,STRING s2)
{
len1=Length(s1); len2=Length(s2); //计算s1和s2的长度
i=0; j=0; //设置两个扫描指针
while (i<len1&&j<len2) {
if (s1.str[i]==s2.str[j]) { i++; j++; }
else {i=i-j+1; j=0;} //对应字符不相等时,重新比较
}
if (j==len2) return i-len2+1;
else return 0;
}
2.2链式存储结构
由于串中的字符个数不一定是每个结点存放字符个数的整倍数,所以,需要在最后一个结点的空缺位置上填充特殊的字符。
这种存储形式优点是存储密度高于结点大小为1 的存储形式;不足之处是做插入、删除字符的操作时,可能会引起结点之间字符的移动,算法实现起来比较复杂。
2.3 串的模式匹配算法
子串定位运算又称为模式匹配(Pattern Matching)或串匹配(String Matching),此运算的应用在非常广泛。例如,在文本编辑程序中,我们经常要查找某一特定单词在文本中出现的位置。显然,解此问题的有效算法能极大地提高文本编辑程序的响应性能。
在串匹配中,一般将主串称为目标串,子串称之为模式串。设S为目标串,T为模式串,且不妨设:
S="s0s1s2…sn-1" T="t0t1…tm-1"
串的匹配实际上是对于合法的位置0≦i≦n-m依次将目标串中的子串s[i..i+m-1]和模式串t[0..m-1]进行比较,若s[i..i+m-1]=t[0..m-1],则称从位置i开始的匹配成功,亦称模式t在目标s中出现;若s[i..i+m-1] ≠t[0..m-1],则称从位置i开始的匹配失败。上述的位置i又称为位移,当s[i..i+m-1]=t[0..m-1]时,i称为有效位移;当s[i..i+m-1] ≠t[0..m-1]时,i称为无效位移。这样,串匹配问题可简化为是找出某给定模式T在一给定目标T中首次出现的有效位移。
串匹配的算法很多,这里我们只讨论一种最简单的称为朴素的串匹配算法。其基本思想是用一个循环来依次检查n-m+1个合法的位移i(0≦i≦n-m)是否为有效位移,其算法段为:
for(i=0;i<=n-m;i++)
if(S[i..i+m-1]=T[0..m-1]
return i;
下面以第二种定长的顺序串类型作为存储结构,给出具体的串匹配算法。
int index(sstring s,sstring t,int pos){
int i,j,k;
int m=s.length;
int n=t.length;
for(i=0;i<=n-m;i++){
j=0;k=i;
while(j<m && s.ch[k]==t.ch[j]{
k++;j++;
}
if(j==m)
return i;
}
return -1;
}
显然,该算法在最坏情况下的时间复杂度为O((n-m)m)。