540 likes | 759 Views
第 3 章 栈和队列. 本章主要介绍以下内容: 栈的概念、存储结构及其基本操作 队列的概念、存储结构及其基本操作 栈与队列的应用举例. 退出. 3.1 栈 3.2 队列. 3.1 栈. 3.1.1 栈的定义 栈是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端进行。如下所示:
E N D
第3章 栈和队列 • 本章主要介绍以下内容: • 栈的概念、存储结构及其基本操作 • 队列的概念、存储结构及其基本操作 • 栈与队列的应用举例 退出
3.1 栈 • 3.2 队列
3.1 栈 • 3.1.1 栈的定义 • 栈是一种特殊的线性表。其特殊性在于限定插入和删除数据元素的操作只能在线性表的一端进行。如下所示: • 进行插入和删除的一端是浮动端,通常被称为栈顶,并用一个“栈顶指针”指示;而另一端是固定端,通常被称为栈底。我们经常将栈用下图3-1的形式描述: a1, a2, a3, ..., an 插入和删除端
结论:后进先出(Last In First Out),简称为LIFO线性表。 • 举例1:家里吃饭的碗,通常在洗干净后一个一个地落在一起存放,在使用时,若一个一个地拿,一定最先拿走最上面的那只碗,而最后拿出最下面的那只碗。 • 举例2:在建筑工地上,使用的砖块从底往上一层一层地码放,在使用时,将从最上面一层一层地拿取。 • 下面我们先给出栈结构的基本操作: • (1)初始化栈 InitStack(S) • (2)入栈 Push(S,item) • (3)出栈 Pop(S,item) • (4)获取栈顶元素内容 GetTop(S,item) • (5)判断栈是否为空 StackEmpty(S)
3.1.2 栈的顺序存储 • 栈的顺序存储结构是用一组连续的存储单元依次存放栈中的每个数据元素,并用起始端作为栈底。 • 类型定义如下所示: • #define MAX_STACK 10 • //栈的最大数据元素数目 • typedef struct stack{ • StackEntry item[MAX_STACK]; • //存放栈中数据元素的存储单元 • int top; //栈顶指针 • }STACK;
基本操作算法: • 1. 初始化栈S • void InItStack(STACK *S) • { s->top=-1; } • 2. 入栈 • void Push(STACK *S,StackEntry item) • { • if (S->top==MAX_STACK-1) exit(“Stack is full”); • else S->item[++S->top]=item; • }
3. 出栈 • void Pop(STACK *S,StackEntry *item) • { • if (StackEmpty(*S)) exit(“Stack is empty”); • else *item=S->item[S->top--]; • } • 4. 获取栈顶元素内容 • void GetTop(STACK S,StackEntry *item) • { • if (StackEmpty(S)) exit(“Stack is empty”); • else *item=S.item[S.top]; • }
5. 判断栈S是否为空 • int StackEmpty(STACK S) • { • if (S.top==-1) return TRUE; • else FALSE; • } • 结论:由于栈的插入和删除操作具有它的特殊性,所以用顺序存储结构表示的栈并不存在插入删除数据元素时需要移动的问题,但栈容量难以扩充的弱点仍就没有摆脱。
3.1.3 栈的链式存储 • 若是栈中元素的数目变化范围较大或不清楚栈元素的数目,就应该考虑使用链式存储结构。人们将用链式存储结构表示的栈称作“链栈”。链栈通常用一个无头结点的单链表表示。如图3-3所示。 • 由于栈的插入删除操作只能在一端进行,而对于单链表来说,在首端插入删除结点要比尾端相对地容易一些,所以,我们将单链表的首端作为栈顶端,即将单链表的头指针作为栈顶指针。
top ^ 图 3-3
栈的链式存储结构在C语言中可用下列类型定义实现:栈的链式存储结构在C语言中可用下列类型定义实现: • type struct node { //链栈的结点结构 • StackEntry item; //栈的数据元素类型 • struct node *next; //指向后继结点的指针 • }NODE; • typedef struct stack{ • NODE *top; • }STACK;
下面我们将给出链栈各项基本操作的算法。 • 1. 初始化栈S • void InitStack(STACK *S) • { • S->top=NULL; • }
2. 入栈 • void Push(STACK *S,StackEntry item) • { • p=(NODE*)malloc(sizeof(NODE)); • if (!p) exit(OVERFLOW); • else { p->item=item; • p->next=S->top; • S->top=p; • } • }
3. 出栈 • void Pop(STACK*S, StackEntry *item) • { • if (StackEmpty(*S)) exit(“Stack is empty”); • else { • *item=S->top->item; • p=S->top; • S->top=p->next; free(p); • } • }
4. 获取栈顶元素内容 • void GetTop(STACK S,StackEntry *item) • { • if (StackEmpty(S)) exit(“Stack is empty”); • else *item=S.top->item; • } • 5. 判断栈S是否空 • int StackEmpty(STACK S) • { • if (S.top==NULL) return TRUE; • else FALSE; • }
3.1.4 栈的应用举例 • 【举例1】将从键盘输入的字符序列逆置输出 • 比如,从键盘上输入:tset a si sihT;算法将输出:This is a test • 下面我们给出解决这个问题的完整算法。 • typedef char StackEntry; • 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,其展转相除的过程如图3-4所示:
下面给出解决这个问题的完整算法。 • 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)当遇到某一个右括号时,栈已空,说明到目前为止,右括号多于左括号;(1)当遇到某一个右括号时,栈已空,说明到目前为止,右括号多于左括号; • (2)从栈中弹出的左括号与当前检验的右括号类型不同,说明出现了括号交叉情况; • (3)算术表达式输入完毕,但栈中还有没有匹配的左括号,说明左括号多于右括号。 • 下面是解决这个问题的完整算法。 • typedef char StackEntry; • 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; • }
3.2 队列 • 3.2.1 队列的定义 • 队列特殊性在于限定插入在线性表的一端进行,删除在线性表的另外一端进行。如图3-5所示: 图 3-5
插入端和删除端都是浮动的。通常我们将插入端称为队尾,用一个“队尾指针”指示;而删除端被称为队头,用一个“队头指针”指示。插入端和删除端都是浮动的。通常我们将插入端称为队尾,用一个“队尾指针”指示;而删除端被称为队头,用一个“队头指针”指示。 • 结论:先进先出(First In First Out),简称为FIFO线性表。 • 举例1:到医院看病,首先需要到挂号处挂号,然后,按号码顺序救诊。 • 举例2:乘坐公共汽车,应该在车站排队,车来后,按顺序上车。 • 举例3:在Windows这类多任务的操作系统环境中,每个应用程序响应一系列的“消息”,像用户点击鼠标;拖动窗口这些操作都会导致向应用程序发送消息。为此,系统将为每个应用程序创建一个队列,用来存放发送给该应用程序的所有消息,应用程序的处理过程就是不断地从队列中读取消息,并依次给予响应。
下面我们给出队列结构的基本操作: • (1)初始化队列 InitQueue(Q) • (2)入队 EnQueue(Q,item) • (3)出队 DeQueue(Q,item) • (4)获取队头元素内容 GetFront(Q,item) • (5)判断队列是否为空 QueueEmpty(Q)
3.2.2 队列的顺序存储 • 队列的顺序存储结构如下图3-6所示: 图 3-6
问题1:当队空时,队头和队尾指针都为-1,队列将处于下图3-7所示的状态:问题1:当队空时,队头和队尾指针都为-1,队列将处于下图3-7所示的状态: 图 3-7
此时若进行入队操作,就需要让队头和队尾指针都增1,再将新数据元素放入该位置。也就是说,这样设置队头、队尾指针位置,在进行入队操作时,空队与非空队状态所需要执行的操作不完全一样。此时若进行入队操作,就需要让队头和队尾指针都增1,再将新数据元素放入该位置。也就是说,这样设置队头、队尾指针位置,在进行入队操作时,空队与非空队状态所需要执行的操作不完全一样。 • 解决方法:在算法中,需要对这两种情况加以区分,这势必增加了算法的复杂性。因此,人们设想了一种解决方法,即让队头指针指向队列真正队头元素的前一个位置,如下图3-8所示。
问题2:由于顺序存储结构的存储空间属于静态分配,所以,在添加数据元素时,可能会出现没有剩余单元的情况。对于队列来说,这一点又有它的特殊性。下面我们讨论一下下图3-10所示的队列。问题2:由于顺序存储结构的存储空间属于静态分配,所以,在添加数据元素时,可能会出现没有剩余单元的情况。对于队列来说,这一点又有它的特殊性。下面我们讨论一下下图3-10所示的队列。 图 3-10
“假溢出”现象。 • 解决方法:将存储队列元素的一维数组首尾相接,形成一个环状。如图3-11所示。我们将这种形式表示的队列称之为循环队列。
rear 6 7 a8 a7 5 0 a6 a5 4 1 2 3 front 图 3-11
假设为队列开辟的数组单元数目为MAX_QUEUE,在C语言中,它的下标在0~MAX_QUEUE-1之间,若增加队头或队尾指针,可以利用取模运算(一个整数数值整除以另一个整数数值的余数)实现。如下所示:假设为队列开辟的数组单元数目为MAX_QUEUE,在C语言中,它的下标在0~MAX_QUEUE-1之间,若增加队头或队尾指针,可以利用取模运算(一个整数数值整除以另一个整数数值的余数)实现。如下所示: • front=(front+1)%MAX_QUEUE; • rear=(rear+1)%MAX_QUEUE; • 当front或rear为MAXQUEUE-1时,上述两个公式计算的结果就为0。这样,就使得指针自动由后面转到前面,形成循环的效果。 • 队空和队满的标志问题: • 队列变为空,队头和队尾指针相等。
front rear rear 7 6 7 6 front a8 a7 5 0 0 5 1 4 4 1 3 2 3 2 (b) (a) 图 3-12 队列变为满,队头和队尾指针也相等。
front front rear 6 rear 7 7 6 a8 a7 5 5 0 0 a6 a6 a1 a1 a2 a2 a5 1 4 a5 4 1 a4 a3 a4 a3 3 2 3 2 (a) (b) 图 3-13
解决方法:一是为队列另设一个标志,用来区分队列是“空”还是“满”;二是当数组只剩下一个单元时就认为队满,此时,队尾指针只差一步追上队头指针,即:(rear+1)%MAX_QUEUE==front。解决方法:一是为队列另设一个标志,用来区分队列是“空”还是“满”;二是当数组只剩下一个单元时就认为队满,此时,队尾指针只差一步追上队头指针,即:(rear+1)%MAX_QUEUE==front。 • 类型定义: • #define MAX_QUEUE 10 • //队列的最大数据元素数目 • typedef struct queue{ • //假设当数组只剩下一个单元时认为队满 • QueueEntry item[MAX_QUEUE]; • //存放队列中数据元素的存储单元 • int front,rear; //队头指针、队尾指针 • }QUEUE;
各项基本操作算法。 • (1)初始化队列Q • void InitQueue(QUEUE *Q) • { • Q->front=-1; • Q->rear=-1; • }
(2)入队 • void EnQueue(QUEUE *Q,QueueEntry item) • { • if ((Q->rear+1)%MAX_QUEUE==Q->front) exit(OVERFLOW); • else { Q->rear=(Q->rear+1)%MAX_QUEUE; • Q->item[Q->rear]=item; } • }
(3)出队 • void DeQueue(QUEUE*Q,QueueEntry *item) • { • if (QueueEmpty(*Q)) exit(“Queue is empty.”); • else { • Q->front=(Q->front+1)%MAX_QUEUE; • *item=Q->item[Q->front]; • } • }
(4)获取队头元素内容 • void GetFront(QUEUE Q,QueueEntry *item) • { • if (QueueEmpty(Q)) exit(“Queue is empty.”); • else *item=Q.item[(Q.front+1)%MAX_QUEUE]; • } • (5)判断队列Q是否为空 • int QueueEmpty(Queue Q) • { • if (Q.front==Q.rear) return TRUE; • else return FALSE; • }
front ^ rear • 3.2.3 队列的链式存储 • 在用链式存储结构表示队列时,需要设置队头指针和队尾指针,以便指示队头结点和队尾结点。 图 3-14
入队需要执行下面三条语句: • s->next=NULL; rear->next=s;rear=s; • 下面是在C语言中,实现队列链式存储结构的类型定义: • type struct node { //链式队列的结点结构 • QueueEntry Entry; //队列的数据元素类型 • struct node *next; //指向后继结点的指针 • }NODE; • typedef struct queue{ //链式队列 • NODE *front; //队头指针 • NODE *rear; //队尾指针 • }QUEUE;
下面我们给出链式队列的基本操作算法。 • (1)初始化队列Q • void InitQueue(QUEUE *Q) • { • Q->front=(NODE*)malloc(sizeof(NODE)); • if (Q->front==NULL) exit(ERROR); • Q->rear= Q->front; • }
(2)入队 • void EnQueue(QUEUE *Q,QueueEntry item) • { • s=(NODE*)malloc(sizeof(NODE)); • if (!s) exit(ERROR); • s->item=item; • s->next=NULL; • Q->rear->next=s; • Q->rear=s; • }
(3)出队 • void DeQueue(QUEUE *Q,QueueEntry *item) • { • if (QueueEmpty(*Q)) exit(ERROR); • else { • *item=Q->front->next->item; • s=Q->front->next; • Q->front->next=s->next; • free(s); • } • }