第3章〓栈和队列





















栈和队列是两种特殊的线性表。从数据逻辑结构角度看,栈和队列的元素均呈现一种线性关系; 从运算的角度看,栈和队列是操作受限的线性表。本章介绍栈和队列的概念、存储结构和基本运算的实现算法。






3.1栈
3.1.1栈的基本概念


栈是一种特殊的线性表,其特殊性体现在元素插入和删除运算上,它的插入和删除运算仅限定在表的某一端进行,不能在表中间和另一端进行。允许进行插入和删除的一端称为栈顶,另一端称为栈底。栈的插入操作称为进栈(或入栈),删除操作称为出栈(或退栈)。处于栈顶位置的数据元素称为栈顶元素。不含任何数据元素的栈称为空栈。

正是这种受限的元素插入和删除运算,使得栈表现出先进后出或者后进先出的特点。举一个例子进行说明,假设有一个很窄的死胡同,胡同里能容纳若干人,但每次只能容许一个人进出。现有5个人,分别编号为①~⑤,按编号的顺序依次进入此死胡同,如图3.1(a)所示。此时若编号为④的人要退出死胡同,必须等⑤退出后才可以。若①要退出,则必须等到⑤、④、③、②依次都退出后才行,如图3.1(b)所示。这里人进出死胡同的原则是先进去的后出来。



图3.1死胡同示意图


在该例中,死胡同就看作是一个栈,栈顶相当于死胡同口,栈底相当于死胡同的另一端,进、出死胡同可看作进栈、出栈操作。插入栈的示意图如图3.2所示。


栈的基本运算主要包括以下6种。

(1) 初始化栈InitStack(st): 建立一个空栈st。

(2) 销毁栈DestroyStack(st): 释放栈st占用的内存空间。

(3) 进栈Push(st,x): 将元素x插入栈st中,使x成为栈st的栈顶元素。

(4) 出栈Pop(st,x): 当栈st不空时,将栈顶元素赋给x,并从栈中删除当前栈顶元素。

(5) 取栈顶元素GetTop(st,x): 若栈st不空,取栈顶元素x并返回1; 否则返回0。

(6) 判断栈空StackEmpty(st): 判断栈st是否为空栈。

包含基本运算的栈如图3.3所示,其中,op1~op6表示上述6个基本运算。



图3.2栈的示意图




图3.3包含基本运算的栈





【例3.1】设一个栈的输入序列为a、b、c、d,借助一个栈(假设栈大小足够大)所得到的出栈序列不可能是。



A.   a、b、c、dB.   b、d、c、aC.   a、c、d、bD.   d、a、b、c


a、b、c、d序列经过栈的情况如图3.4所示,根据栈的特点,很容易得出d、a、b、c是不可能的,因为d先出栈,说明a、b、c均已在栈中,按照进栈顺序,从栈顶到栈底的顺序应为c、b、a,出栈的顺序只能是d、c、b、a。所以不可能的出栈序列是D。


【例3.2】已知一个栈的进栈序列是1,2,3,…,n,其出栈序列是p1,p2,…,pn,若p1=n,则pi的值为。

A.  iB.   n-iC.   n-i+1D.  不确定

p1=n,则出栈序列是唯一的,即为n,n-1,…,2,1,这样有pi+i=n+1,由此推出pi=n-i+1。本题答案为C。

【例3.3】元素a、b、c、d、e依次进入初始为空的栈中,假设栈大小足够大。若元素进栈后可停留、可立即出栈,直到所有的元素都出栈,则所有可能的出栈序列中,以元素d开头的出栈序列个数是。

A.  3B.   4C.   5D.   6

若元素d第一个出栈,则a、b、c均在栈中,从栈顶到栈底的顺序应为c、b、a,如图3.5所示,此后合法的栈操作如下。

(1) e进栈,e出栈,c出栈,b出栈,a出栈,得到的出栈序列decba。

(2) c出栈,e进栈,e出栈,b出栈,a出栈,得到的出栈序列dceba。

(3) c出栈,b出栈,e进栈,e出栈,a出栈,得到的出栈序列dcbea。

(4) c出栈,b出栈,a出栈,e进栈,e出栈,得到的出栈序列dcbae。

以元素d开头的出栈序列个数为4,本题答案为B。



图3.4序列经过一个栈的情况




图3.5元素出栈的情况







3.1.2栈的顺序存储结构

栈是一种特殊的线性表,和线性表存储结构类似,栈也有两种存储结构: 顺序存储结构和链式存储结构。

栈的顺序存储结构称为顺序栈。顺序栈通常由一个一维数组data和一个记录栈顶元素位置的变量top组成。习惯上将栈底放在数组下标小的那端,栈顶元素由栈顶指针top所指向。顺序栈类型声明如下。

#define MaxSize 100//顺序栈的初始分配空间大小

typedef struct

{ElemType data[MaxSize];//保存栈中元素,这里假设ElemType为char类型

int top;//栈顶指针

} SqStack;

在上述顺序栈定义中,ElemType为栈元素的数据类型,MaxSize为一个常量,表示data数组中最多可放的元素个数,data元素的下标范围为0~MaxSize-1。当top=-1时表示栈空; 当top=MaxSize-1时表示栈满。

图3.6说明了顺序栈st的几种状态(假设MaxSize=5)。图3.6(a)表示顺序栈为栈空,这也是初始化运算得到的结果。此时栈顶指针top=-1。如果做出栈运算,则会“下溢出”。



图3.6顺序栈的几种状态


图3.6(b)表示栈中只含一个元素a,在图3.6(a)的基础上执行进栈运算Push(st,'a')可以得到这种状态。此时栈顶指针top=0。

图3.6(c)表示在图3.6(b)基础上又有两个元素b、c先后进栈,此时栈顶指针top=2。

图3.6(d)表示在图3.6(c)状态下执行一次Pop(st,x)运算得到。此时栈顶指针top=1。故b为当前的栈顶元素。

图3.6(e)表示在图3.6(d)状态下执行两次Pop(st,x)运算得到。此时栈顶指针top=-1,又变成栈空状态。

归纳起来,对于顺序栈st,其初始时置st.top=-1,它的4个要素如下。

(1) 栈空条件: st.top==-1。

(2) 栈满条件: st.top==MaxSize-1。

(3) 元素x进栈操作: st.top++; 将元素x存放在st.data[st.top]中。

(4) 出栈元素x操作: 取出栈元素x=st.data[st.top]; st.top--。

顺序栈的基本运算算法如下。

1. 初始化栈运算算法
其主要操作是设定栈顶指针top为-1,对应的算法如下。

void InitStack(SqStack &st)//st为引用型参数

{

st.top=-1;

}

2. 销毁栈运算算法
这里顺序栈的内存空间是由系统自动分配的,在不再需要时由系统自动释放其空间。对应的算法如下。
void DestroyStack(SqStack st)

{}

3. 进栈运算算法
其主要操作是: 栈顶指针加1,将进栈元素存放在栈顶处。对应的算法如下。

int Push(SqStack &st,ElemType x)

{if (st.top==MaxSize-1)//栈满上溢出返回0

return 0;

else

{st.top++;

st.data[st.top]=x;

return 1;//成功进栈返回1

}

}

4. 出栈运算算法
其主要操作是: 先将栈顶元素取出,然后将栈顶指针减1。对应的算法如下。

int Pop(SqStack &st,ElemType &x)//x为引用型参数

{if (st.top==-1)//栈空返回0

return 0;

else 

{x=st.data[st.top];

st.top--;

return 1;//成功出栈返回1

}

}

5. 取栈顶元素运算算法
其主要操作是: 将栈顶指针top处的元素取出赋给变量x,top保持不动。对应的算法如下。

int GetTop(SqStack st,ElemType &x)//x为引用型参数

{if (st.top==-1)//栈空返回0

return 0;

else 

{x=st.data[st.top];

return 1;//成功取栈顶元素返回1

}

}

6. 判断栈空运算算法
其主要操作是: 若栈为空(top==-1)则返回值1,否则返回值0。对应的算法如下。

int StackEmpty(SqStack st)

{if (st.top==-1) return 1;//栈空返回1

else return 0;//栈不空返回0

}

提示: 将顺序栈类型声明和栈基本运算函数存放在SqStack.cpp文件中。

当顺序栈的基本运算函数设计好后,给出以下程序调用这些基本运算函数,读者可以对照程序执行结果进行分析,进一步体会顺序栈的各种运算的实现过程。

#include "SqStack.cpp"//包含前面的顺序栈基本运算函数

int main()

{SqStack st;//定义一个顺序栈st

ElemType e;

printf("初始化栈st\n");

InitStack(st);

printf("栈%s\n",(StackEmpty(st)==1?"空":"不空"));

printf("a进栈\n");Push(st,'a');

printf("b进栈\n");Push(st,'b');

printf("c进栈\n");Push(st,'c');

printf("d进栈\n");Push(st,'d');

printf("栈%s\n",(StackEmpty(st)==1?"空":"不空"));

GetTop(st,e);

printf("栈顶元素:%c\n",e);

printf("出栈次序:");

while (!StackEmpty(st))//栈不空循环

{Pop(st,e);//出栈元素e并输出

printf("%c ",e);

}

printf("\n");

DestroyStack(st);

}




图3.7程序执行结果


上述程序的执行结果如图3.7所示。


【例3.4】若一个栈用数组data[1..n]存储(n为一个大于2的正整数),初始栈顶指针top为n+1,则以下元素x进栈的正确操作是。



A.  top++;data[top]=x;B.  data[top]=x;top++;

C.   top--;data[top]=x;D.  data[top]=x;top--;

一个栈中栈元素是向一端伸展的,即从栈底向栈顶方向伸展。这里初始栈顶指针top为n+1,说明data[n]端作为栈底,在进栈时top应递减,由于不存在data[n+1]的元素,所以在进栈时应先将top递减,再将x放在top处。本题答案为C。






3.1.3栈的链式存储结构

栈的链式存储结构是采用某种链表结构,栈的链式存储结构简称为链栈。这里采用单链表作为链栈,如图3.8所示,该单链表是不带头结点的,通过首结点指针ls唯一标识整个单链表。同时,单链表的首结点就是链栈的栈顶结点(图中首结点值为an表示最后进栈的元素是an),所以ls也作为栈顶指针,栈中的其他结点通过next域链接起来,栈底结点的next域为NULL。因链栈本身没有容量限制,所以不必考虑栈满的情况,这是链栈相比顺序栈的一个优点。



图3.8链栈示意图


链栈的类型定义如下。

typedef struct node

{ElemType data;//结点值,假设ElemType为char类型

struct node *next;//指针域

} LinkStack;

归纳起来,链栈ls初始时ls=NULL,其4个要素如下。

(1) 栈空条件: ls==NULL。

(2) 栈满条件: 不考虑。

(3) 元素x进栈操作: 创建存放元素x的结点p,将其插入栈顶位置上。

(4) 出栈元素x操作: 置x为栈顶结点的data域,并删除该结点。

链栈的基本运算算法如下。

1. 初始化栈运算算法
其主要操作是: 置s为NULL标识栈为空栈。对应的算法如下。

void InitStack(LinkStack *&ls)//ls为引用型参数

{

ls=NULL;

}

2. 销毁栈运算算法
链栈的所有结点空间都是通过malloc函数分配的,在不再需要时需通过free函数释放所有结点的空间,与单链表销毁算法类似,只是这里链栈是不带头结点的。对应的算法如下。

void DestroyStack(LinkStack *&ls)

{LinkStack *pre=ls,*p;

if (pre==NULL) return;//考虑空栈的情况

p=pre->next;

while (p!=NULL)

{free(pre);//释放pre结点

pre=p;p=p->next;//pre、p同步后移

}

free(pre);//释放尾结点

}

3. 进栈运算算法
其主要操作是: 先创建一个新结点p,其data域值为x; 然后将该结点插入开头作为新的栈顶结点。对应的算法如下。

int Push(LinkStack *&ls,ElemType x)//ls为引用型参数

{LinkStack *p;

p=(LinkStack *)malloc(sizeof(LinkStack));

p->data=x;//创建结点p用于存放x

p->next=ls;//插入p结点作为栈顶结点

ls=p;

return 1; 

}

4. 出栈运算算法
其主要操作是: 将栈顶结点(ls所指结点)的data域值赋给x,然后删除该栈顶结点。对应的算法如下。

int Pop(LinkStack *&ls,ElemType &x)//ls为引用型参数

{LinkStack *p;

if (ls==NULL) //栈空,下溢出返回0

return 0;

else//栈不空时出栈元素x并返回1

{p=ls;//p指向栈顶结点

x=p->data;//取栈顶元素x

ls=p->next;//删除结点p

free(p);//释放p结点

return 1;

}

}

说明: 尽管链栈操作时不考虑上溢出,但仍然需要考虑出栈操作时的下溢出。

5. 取栈顶元素运算算法
其主要操作是: 将栈顶结点(即ls所指结点)的data域值赋给x。对应的算法如下。

int GetTop(LinkStack *ls,ElemType &x)

{if (ls==NULL)//栈空,下溢出时返回0

return 0;

else//栈不空,取栈顶元素x并返回1

{x=ls->data;

return 1;

}

}

6. 判断栈空运算算法
其主要操作是: 若栈为空(即ls==NULL)则返回值1,否则返回值0。对应的算法如下。

int StackEmpty(LinkStack *ls)

{if (ls==NULL) return 1;

else return 0;

}

提示: 将链栈结点类型声明和链栈基本运算函数存放在LinkStack.cpp文件中。

当链栈的基本运算函数设计好后,给出以下程序调用这些基本运算函数,读者可以对照程序执行结果进行分析,进一步体会顺序栈的各种运算的实现过程。

#include "LinkStack.cpp"//包含前面的链栈基本运算函数

int main()

{ElemType e;

LinkStack *st;//定义一个链栈st

printf("初始化栈st\n");

InitStack(st);

printf("栈%s\n",(StackEmpty(st)==1?"空":"不空"));

printf("a进栈\n");Push(st,'a');

printf("b进栈\n");Push(st,'b');

printf("c进栈\n");Push(st,'c');

printf("d进栈\n");Push(st,'d');

printf("栈%s\n",(StackEmpty(st)==1?"空":"不空"));

GetTop(st,e);

printf("栈顶元素:%c\n",e);

printf("出栈次序:");

while (!StackEmpty(st))//栈不空循环

{Pop(st,e);//出栈元素e并输出

printf("%c ",e);

}

printf("\n");

DestroyStack(st);

}

上述程序的执行结果如图3.7所示。

【例3.5】以下各链表均不带有头结点,其中最不适合用作链栈的链表是。



A.  只有表尾指针没有表头指针的循环单链表

B.   只有表头指针没有表尾指针的循环单链表

C.   只有表头指针没有表尾指针的循环双链表

D.   只有表尾指针没有表头指针的循环双链表

由于各链表均不带有头结点,这里表头指针就是首结点指针。采用一种链表作为链栈时,通常将链表首结点处作为栈顶。一个链表适不适合作为链栈,就看它是否能够高效地实现栈的基本运算,而栈的主要操作是进栈和出栈。

考虑选项A,只有表尾指针没有表头指针的循环单链表,假设表尾指针为rear,它指向循环单链表的尾结点,如图3.9所示。元素x进栈的操作是: 创建存放x的结点p,将结点p插入在rear结点之后作为栈顶结点,不改变rear; 出栈的操作是: 删除rear结点之后的结点。两种操作的时间复杂度均为O(1)。



图3.9只有表尾指针没有表头指针的循环单链表


考虑选项B,只有表头指针没有表尾指针的循环单链表,假设表头指针为first,它指向循环单链表的首结点,如图3.10所示。元素x进栈的操作是: 创建存放x的结点p,将结点p插入在first结点之前,即p->next=first,first=p,但还要将其改变为循环单链表,而尾结点需要遍历所有结点找到,遍历的时间为O(n),所以进栈操作的时间复杂度为O(n); 出栈的操作是: 删除first结点,删除后仍然需要将其改变为循环单链表,所以出栈操作的时间复杂度为O(n)。两种操作的时间复杂度均为O(n)。



图3.10只有表头指针没有表尾指针的循环单链表


考虑选项C和D,由于都是循环双链表,可以通过表头结点直接找到尾结点,在插入和删除后改为循环双链表的时间均为O(1),所以它们的进栈和出栈操作的时间复杂度都是O(1)。

比较起来,只有表头指针没有表尾指针的循环单链表作为链栈时,进栈和出栈操作的时间性能最差。本题答案为B。


3.1.4栈的应用示例

在较复杂的数据处理过程中,通常需要保存多个临时产生的数据,如果先产生的数据后进行处理,那么需要用栈来保存这些数据。本节通过几个经典示例说明栈的应用方法,并且算法中均采用顺序栈,实际上采用链栈完全相同。

【例3.6】假设以I和O分别表示进栈和出栈操作,栈的初态和终态均为空,进栈和出栈的操作序列可表示为仅由I和O组成的序列。

(1) 下面所示的序列中哪些是合法的?



A.   IOIIOIOOB.   IOOIOIIOC.   IIIOIOIOD.   IIIOOIOO

(2) 通过对(1)的分析,设计一个算法利用链栈的基本运算判定操作序列str是否合法。若合法返回1; 否则返回0。

(1) A、D均合法,而B、C不合法。因为在B中,先进栈一次,立即出栈两次,这会造成栈下溢。在C中共进栈5次,出栈3次,栈的终态不为空。

归纳起来,一个操作序列是合法的,当且仅当其中所有I的个数与O的个数相等,而且任何前缀中I的个数大于或等于O的个数。

(2) 本例用一个顺序栈st来判断操作序列是否合法,其中,str为存放操作序列的字符数组,n为该数组的元素个数。对应的算法如下。

#include "Sqstack.cpp"//包含前面的顺序栈基本运算函数

int Judge(char str[ ],int n)

{int i; ElemType x;

SqStack st;//定义一个顺序栈st

InitStack(st);//栈初始化

for (i=0;i<n;i++)//遍历str的所有字符

{if (str[i]=='I')//为'I'时进栈

Push(st,str[i]);

else if (str[i]=='O')//为'O'时出栈

{if (!Pop(st,x))//若栈下溢出,则返回0

{DestroyStack(st);

return 0;

}

}

}

if (StackEmpty(st))//栈为空时返回1,否则返回0

{DestroyStack(st);

return 1;

}

else

{DestroyStack(st);

return 0;

}

}

说明:  本例的另一种解法是用cnt累计I与O的个数差,cnt从0开始,循环遍历str,遇到I增1,遇到0减1。当cnt减1后小于0时返回0。循环结束后cnt为0则返回1。


【例3.7】回文指的是一个字符串从前面读和从后面读都一样,如"abcba"、"123454321"都是回文。设计一个算法利用栈判断一个字符串是否为回文。

由于回文是从前到后以及从后到前都是一样的,所以只要将待判断的字符串颠倒,然后与原字符串相比较,就可以决定是否回文了。

将字符串str从头到尾的各个字符依次进栈到顺序栈st中,由于栈的特点是后进先出,则从栈顶到栈底的各个字符正好与字符串str从尾到头的各个字符相同; 然后将字符串str从头到尾的各个字符,依次与从栈顶到栈底的各个字符相比较,如果两者不相同,则表明str不是回文,在相同时继续比较; 如果相应字符全部匹配,则说明str是回文。对应的算法如下。

#include "SqStack.cpp"//包含前面的顺序栈基本运算函数

int Palindrome(char str[ ],int n)

{SqStack st;//定义一个顺序栈st

InitStack(st);//栈初始化

int i;

char ch;

for (i=0;i<n;i++)//所有字符依次进栈

Push(st,str[i]);

i=0;//从头开始遍历str

while (!StackEmpty(st))//栈不空循环

{Pop(st,ch);//出栈元素ch

if (ch!=str[i++])//两字符不相同时返回0

{DestroyStack(st);

return 0;

}

}

DestroyStack(st);//销毁栈st

return 1;//所有相应字符都相同时返回1

}





【例3.8】设计一个算法,判断一个可能含有小括号(“(”与“)”、)、中括号(“[”与“]”)和大括号(“{”与“}”)的表达式中各类括号是否匹配。若匹配,则返回1; 否则返回0。

设置一个顺序栈st,定义一个整型flag变量(初始为1)。用i扫描表达式exp,当i<n并且flag=1时循环: 当遇到左括号“(”“[”“{”时,将其进栈; 遇到“}”“]”“)”时,出栈字符ch,若出栈失败(下溢出)或者ch不匹配,则置flag=0退出循环,否则直到exp扫描完毕为止。若栈空并且flag为1则返回1,否则返回0。

例如,对于表达式"[(])",其括号不匹配,匹配过程如图3.11(a)所示; 对于表达式"[()]",其括号是匹配的,匹配过程如图3.11(b)所示。



图3.11判断表达式括号是否匹配


对应的算法如下。

#include "SqStack.cpp"//包含前面的顺序栈基本运算函数

int Match(char exp[ ],int n)//exp存放表达式

{SqStack st;//定义一个顺序栈st

InitStack(st);//栈初始化

int flag=1,i=0;

char ch;

while (i<n && flag==1)//遍历表达式exp

{switch(exp[i])

{

case '(': case '[': case '{'://各种左括号进栈

Push(st,exp[i]); break;

case ')'://判断栈顶是否为'('

if (!Pop(st,ch) || ch!='(')//出栈操作失败或者不匹配

flag=0;

break;

case ']'://判断栈顶是否为'['

if (!Pop(st,ch) || ch!='[')//出栈操作失败或者不匹配

flag=0;

break;

case '}'://判断栈顶是否为'{'

if (!Pop(st,ch) || ch!='{')//出栈操作失败或者不匹配

flag=0;

break;

}

i++;

}

if (StackEmpty(st) && flag==1)//栈空且符号匹配则返回1

{DestroyStack(st);//销毁栈st

return 1;

}

else

{DestroyStack(st);//销毁栈st

return 0;//否则返回0

}

}

【例3.9】设计一个算法将一个十进制正整数转换为相应的二进制数。

将十进制正整数转换成二进制数通常采用除2取余数法(称为辗转相除法)。在转换过程中,二进制数是从低位到高位的次序得到的,这和通常的从高位到低位输出二进制的次序相反。为此设计一个栈,用于暂时存放每次得到的余数,当转换过程结束时,退栈所有元素便得到从高位到低位的二进制数。如图3.12所示是十进制数12转换为二进制数1100的过程。



图3.1212转换为二进制数的过程



对应的算法如下。

#include "SqStack.cpp"//包含前面的顺序栈基本运算函数

void trans(int d,char b[ ]) 

//b用于存放d转换成的二进制数的字符串

{SqStack st;//定义一个顺序栈st

InitStack(st);//栈初始化

char ch;

int i=0;

while (d!=0)

{ch='0'+d%2;//求余数并转换为字符

Push(st,ch);//字符ch进栈

d/=2;//继续求更高位

}

while (!StackEmpty(st))

{Pop(st,ch);//出栈并存放在数组b中

b[i]=ch;

i++;

}

b[i]='\0';//加入字符串结束标志

DestroyStack(st);//销毁栈st

}

设计如下主函数。

int main()

{int d;

char str[MaxSize];

do//保证输入一个正整数

{printf("输入一个正整数:");

scanf("%d",&d);

} while (d<0);

trans(d,str);

printf("对应的二进制数:%s\n",str);

}

本程序的一次执行结果如下。

输入一个正整数:120↙

对应的二进制数:1111000







3.2队列
3.2.1队列的基本概念


队列(简称队)也是一种运算受限的线性表,在这种特殊的线性表上,插入限定在表的某一端进行,删除限定在表的另一端进行。队列的插入操作称为进队,删除操作称为出队。允许插入的一端称为队尾,允许删除的一端称为队头。新插入的元素只能添加到队尾,被删除的只能是排在队头的元素。

正是这种受限的元素插入和删除运算,使得队列表现出先进先出或者后进后出的特点。举一个例子进行说明,如图3.13所示是顾客①~⑤排队买车票的情况,排队围栏里能容纳若干人,但每次只能容许一个人进出,进入排队队列只能从进口进,某顾客买完车票后只能从出口出。从中看到,顾客进入排队队列的顺序是①~⑤,那么买票并离开队列的顺序也只能是①~⑤。也就是说,顾客进出排队队列的原则是先进去的先出来。

归纳起来,一个队列的示意图如图3.14所示。



图3.13顾客排队买车票




图3.14队列的示意图



队列的基本运算如下。

(1) 初始化队列InitQueue(qu): 建立一个空队qu。

(2) 销毁队DestroyQueue(qu): 释放队列qu占用的内存空间。

(3) 进队EnQueue(qu,x): 将x插入队列qu的队尾。

(4) 出队DeQueue(qu,x): 将队列qu的队头元素出队并赋给x。




图3.15包含基本运算的队列


(5) 取队头元素GetHead(qu,x): 取出队列qu的队头元素并赋给x,但该元素不出队。

(6) 判断队空QueueEmpty(qu): 判断队列qu是否为空。

包含基本运算的队列如图3.15所示,其中,op1~op6表示上述6个基本运算。


【例3.10】以下属于队列的基本运算的是。



A.  对队列中的元素排序B.  取出最近进队的元素

C.   在队列中某元素之前插入元素D.  删除队头元素

删除队头元素即出队,属队列的一种基本运算,其他均不是队列的基本运算。本题答案为D。

【例3.11】设栈S和队列Q的初始状态均为空,元素a、b、c、d、e、f、g依次进入栈S。若每个元素出栈后立即进入队列Q,且7个元素出列的顺序是b、d、c、f、e、a、g,则栈S的容量至少是。

A.   1B.   2C.   3D.   4

由于队列不改变进、出队元素的次序,即a1、a2、…、an依次进入一个队列,出队序列只有a1、a2、…、an一种,所以本题变为通过一个栈将a、b、c、d、e、f、g序列变为b、d、c、f、e、a、g序列时栈空间至少多大。其过程如表3.1所示,从中可以看到,栈中最多有三个元素,即栈大小至少为3。本题答案为C。

表3.1由a、b、c、d、e、f、g序列通过一个栈得到b、d、c、f、e、a、g序列的过程



操作栈S出栈序列

a进栈a
b进栈ab
b出栈ab
c进栈acb
d进栈acdb
d出栈acbd
c出栈abdc
e进栈aebdc
f进栈aefbdc
f出栈aebdcf
e出栈abdcfe
a出栈bdcfea
g进栈gbdcfea
g出栈bdcfeag







3.2.2队列的顺序存储结构

与栈类似,队列通常有两种存储结构,即顺序存储结构和链式存储结构。队列的顺序存储结构简称为顺序队,它由一个一维数组(用于存储队列中元素)及两个分别指示队头和队尾的变量组成(因为队列不同于栈,栈只要一端发生改变,而队列的两端都可能发生改变),这两个变量分别称为“队头指针”和“队尾指针”。通常约定队尾指针指示队尾元素的当前位置,队头指针指示队头元素的前一个位置。

顺序队的类型声明如下。

#define MaxSize 20//指定队列的容量

typedef struct

{ElemType data[MaxSize];//保存队元素,假设ElemType为char类型

int front,rear;//队头和队尾指针

} SqQueue;

顺序队的定义为一个结构类型,该类型变量有三个域: data、front、rear。其中,data为存储队列中元素的一维数组。队头指针front和队尾指针rear定义为整型变量,有效的取值范围均为0~MaxSize-1。

图3.16表示了顺序队列sq(假设MaxSize=5)的几种状态。

图3.16(a)表示队列的初始状态,sq.rear=sq.front=-1。

图3.16(b)表示元素a进队后,sq.rear=0,sq.front=-1。

图3.16(c)表示元素b、c、d、e依次进队后,sq.rear=4,sq.front=-1。

图3.16(d)表示元素a、b出队后,sq.rear=4,sq.front=1。

图3.16(e)表示元素c、d、e出队后,sq.rear=sq.front=4。



图3.16顺序队的几种状态


从图3.16中可以看到,在队列刚建立时,先对它进行初始化,令front=rear=-1。每当进队一个元素时,让队尾指针rear增1,再将新元素放在rear所指位置,也就是说元素进队只会引起rear的变化,而不会导致front的变化,而且rear指示刚进队的元素(队尾元素)位置。队头指针front则不同,每当出队一个元素时,让队头指针front增1,再把front所指位置上的元素取出,也就是说元素出队只会引起front的变化,而不会导致rear的变化,而且front所指示的元素已出队了,它实际指示的是当前队列中队头元素的前一位置。

从图3.16中可以看到,图3.16(a)和图3.16(e)都是队空的情况,均满足front==rear的条件,所以可以将front==rear作为队空的条件。那么队满的条件如何设置呢?受顺序栈的启发,似乎很容易得到队满的条件为rear==MaxSize-1。显然这里有问题,因为图3.16(d)和图3.16(e)都满足这个“队满”的条件,而实际上队列并没有满,这种因为队满条件设置不合理而导致的“溢出”称为假溢出,也就是说这种“溢出”并不是真正的溢出,尽管队满条件成立了,但队列中还有多个存放元素的空位置。

为了能够充分地使用数组中的存储空间,可以把数组的前端和后端连接起来,形成一个环形的表,即把存储队列元素的表从逻辑上看成一个环,这个环形的表叫作循环队列或环形队列。图3.17表示了循环队列sq的几种状态。

图3.17(a)表示队列的初始状态,sq.rear=sq.front=0。

图3.17(b)表示元素a进队后,sq.rear=1,sq.front=0。

图3.17(c)表示元素b、c、d、e依次进队后,sq.rear=0,sq.front=0。

图3.17(d)表示元素a、b出队后,sq.rear=0,sq.front=2。

图3.17(e)表示元素c、d,e出队后,sq.rear=sq.front=0。



图3.17循环队列的几种状态


循环队列首尾相连,当队头指针front==MaxSize-1后,再前进一个位置就自动到0,这可以利用除法取余的运算(MOD,C/C++语言中运算符为%)来实现。所以队头队尾指针增1的操作如下。

队头指针进1: front=(front+1) MOD MaxSize

队尾指针进1: rear=(rear+1) MOD MaxSize

循环队列的队头指针和队尾指针初始化时都置为0: front=rear=0。在队尾插入新元素和删除队头元素时,相关指针都循环进1。当它们进到MaxSize-1时,并不表示队空间满了,只要有需要,利用MOD运算可以推进到数组的0号位置。

如果循环队列读取元素的速度快于存入元素的速度,队头指针会很快追上队尾指针,一旦到达front==rear时,则队列变成空队列。反之,如果队列存入元素的速度快于读取元素的速度,队尾指针很快就会赶上队头指针,一旦队列满就不能再加入新元素了。

在循环队列中仍不能区分队空和队满,因为图3.17(a)、图3.17(c)和图3.17(e)都满足条件front==rear,而图3.17(a)和图3.17(e)为队空,图3.17(c)为队满。那么如何解决这一问题呢?仍设置队空条件为front==rear,将队满条件设置为(rear+1) MOD MaxSize==front,也就是说,当rear指到front的前一位置时就认为队列满了,显然在这样设置的队满条件下,队满条件成立时队中还有一个空闲单元,也就是说这样的队中最多只能进队MaxSize-1个元素。

说明: 上述设置循环队列队满条件的方法不是最理想的,因为队中最多只能放入MaxSize-1个元素,但它是一种最简单的方法,后面例3.15就在这个基础上进行了改进,读者可以体会两种方法的差异。

归纳起来,上述设置的循环队列sq的4个要素如下。

(1) 队空条件: sq.front==sq.rear。

(2) 队满条件: (sq.rear+1)%MaxSize==sq.front。

(3) 进队操作: sq.rear循环进1; 元素进队。

(4) 出队操作: sq.front循环进1; 元素出队。





循环队列的基本运算算法如下。

1. 初始化队列运算算法
其主要操作是: 设置sq.front=sq.rear=0。对应的算法如下。

void InitQueue(SqQueue &sq)//sq为引用型参数

{

sq.rear=sq.front=0;//指针初始化

}

2. 销毁队列运算算法
这里顺序队的内存空间是由系统自动分配的,在不再需要时由系统自动释放其空间。对应的算法如下。
void DestroyQueue(SqQueue sq)

{}

3. 进队运算算法
其主要操作是: 先判断队列是否已满,若不满,让队尾指针循环进1,在该位置存放x。对应的算法如下。

int EnQueue(SqQueue &sq,ElemType x)

{if ((sq.rear+1) % MaxSize==sq.front)//队满上溢出

return 0;

sq.rear=(sq.rear+1) % MaxSize;//队尾循环进1

sq.data[sq.rear]=x;

return 1;

}

4. 出队运算算法
其主要操作是: 先判断队列是否已空,若不空,让队头指针循环进1,将该位置的元素值赋给x。对应的算法如下。

int DeQueue(SqQueue &sq,ElemType &x)//x为引用型参数

{if (sq.rear==sq.front)//队空下溢出

return 0;

sq.front=(sq.front+1) % MaxSize;//队头循环进1

x=sq.data[sq.front];

return 1;

}

5. 取队头元素运算算法
其主要操作是: 先判断队列是否已空,若不空,将队头指针前一个位置的元素值赋给x。对应的算法如下。

int GetHead(SqQueue sq,ElemType &x)//x为引用型参数

{if (sq.rear==sq.front)//队空下溢出

return 0;

x=sq.data[(sq.front+1) % MaxSize];

return 1;

}

6. 判断队空运算算法
其主要操作是: 若队列为空,则返回1,否则返回0。对应的算法如下。

int QueueEmpty(SqQueue sq)

{if (sq.rear==sq.front) return 1;

else return 0;

}

提示: 将顺序队类型声明及其基本运算函数存放在SqQueue.cpp文件中。

当顺序队的基本运算函数设计好后,给出以下程序调用这些基本运算函数,读者可以对照程序执行结果进行分析,进一步体会顺序队的各种运算的实现过程。

#include "SqQueue.cpp"//包含前面的顺序队基本运算函数

int main()

{SqQueue sq;//定义一个顺序队sq

ElemType e;

printf("初始化队列\n");

InitQueue(sq);

printf("队%s\n",(QueueEmpty(sq)==1?"空":"不空"));

printf("a进队\n");EnQueue(sq,'a');

printf("b进队\n");EnQueue(sq,'b');

printf("c进队\n");EnQueue(sq,'c');

printf("d进队\n");EnQueue(sq,'d');

printf("队%s\n",(QueueEmpty(sq)==1?"空":"不空"));

GetHead(sq,e);

printf("队头元素:%c\n",e);

printf("出队次序:");

while (!QueueEmpty(sq))//队不空循环

{DeQueue(sq,e);//出队元素e

printf("%c ",e);//输出元素e

}

printf("\n");

DestroyQueue(sq);//销毁顺序队sq

}



图3.18程序的执行结果


上述程序的执行结果如图3.18所示。


说明: 顺序队有循环队列和非循环队列两种,前者把存储队列元素的表从逻辑上看成一个环,从而新进队的元素可以覆盖已出队元素的空间,

提高存储空间利用率。但有些情况下要利用所有进队的元素求解时,只能采用非循环队列。





【例3.12】若用一个大小为6的数组来实现循环队列,队头指针front指向队列中队头元素的前一个位置,队尾指针rear指向队尾元素的位置。若当前rear和front的值分别为0和3,当从队列中删除一个元素,再加入两个元素后,rear和front的值分别为。



A.   1和5B.   2和4C.   4和2D.   5和1

当前有rear=0,进队两个元素后,rear循环递增2,rear=2; 当前有front=3,出队一个元素后,front循环递增1,front=4。本题答案为B。

【例3.13】对于循环队列,写出求队列中元素个数的公式,并编写相应的算法。

循环队列中队头、队尾指针变化主要有如图3.19所示的两种情况,归纳起来,循环队列元素个数的计算公式如下。

(rear-front+MaxSize)%MaxSize

对应的算法如下。

int Count(SqQueue sq)

{

return (sq.rear-sq.front+MaxSize) % MaxSize;

}



图3.19求循环队中元素个数的两种情况




【例3.14】已知循环队列存储在一维数组A[0..n-1]中,且队列非空时front和rear分别指向队头元素和队尾元素。若初始时队列空,且要求第一个进入队列的元素存储在A[0]处,则初始时front和rear的值分别是。


A.   0,0B.   0,n-1C.   n-1,0D.   n-1,n-1

在循环队列中,进队操作是队尾指针rear循环加1,再在该处放置进队的元素,本题要求第一个进入队列的元素存储在A[0]处,则rear应为n-1,因为这样有(rear+1)%n=0。而队头指针front指向队头元素,此时队头位置为0,所以front的初值为0。本题答案为B。

提示: 在一般的数据结构教科书中,循环队列的队头指针front设计为指向队列中队头元素的前一个位置,而队尾指针rear指向队尾元素的位置,本题的front和rear有所不同。

【例3.15】如果用一个大小为MaxSize的数组表示环形队列,该队列只有一个队头指针front,不设队尾指针rear,而改置一个计数器count,用以记录队列中的元素个数。

(1) 队列中最多能容纳多少个元素?

(2) 设计实现队列基本运算的算法。

依题意,设计队列的类型如下。

typedef struct

{ElemType data[MaxSize];//存放队列中的元素

int front;//队头指针

int count;//队列中元素个数

} SQueue;

(1) 队列中最多可容纳MaxSize个元素,因为这里不需要空出一个位置以区分队列空和队列满的情况。

(2) 队列sq为空的条件是: sq.count==0; 队列为满的条件是: sq.count==MaxSize。在队头指针sq.front和队中元素个数sq.count已知时,计算队尾元素的位置的公式是: 

队尾元素位置=(sq.front+sq.count)%MaxSize

在这种队列上实现队列的基本运算算法如下。

//----队初始化算法----

void InitQueue(SQueue &qu)

{qu.front=qu.count=0;  }

//----销毁队列算法----

void DestroyQueue(SQueue sq)

{  }

//----元素进队算法----

int EnQueue(SQueue &sq,ElemType x)

{if (sq.count==MaxSize) return 0;//队满

sq.count++;//队中元素个数增1

sq.data[(sq.front+sq.count)%MaxSize]=x;

return 1;

}

//----出队元素算法----

int DeQueue(SQueue &sq,ElemType &x)

{if (sq.count==0) return 0;//队空

sq.count--;//队中元素个数减1

sq.front=(sq.front+1)%MaxSize;

x=sq.data[sq.front];

return 1;

}

//----取队头元素算法----

int GetHead(SQueue sq,ElemType &x)

{if (sq.count==0) return 0;//队空

x=sq.data[(sq.front+1)%MaxSize];

return 1;

}


//----判队空算法----

int QueueEmpty(SQueue sq) 

{if (sq.count==0) return 1;//队空返回1

else return 0;//队不空返回0

}





3.2.3队列的链式存储结构

队列的链式存储结构简称为链队,它实际上是一个同时带有队头指针front和队尾指针rear的单链表。队头指针指向队头结点,队尾指针指向队尾结点即单链表的最后一个结点,并将队头和队尾指针结合起来构成链队结点,如图3.20所示。



图3.20链队示意图


其中,链队的数据结点类型声明如下。

typedef struct QNode 

{ElemType data;//存放队中元素

struct QNode *next;//指向下一个结点的指针

} QType;//链队中数据结点的类型

链队结点的类型声明如下。

typedef struct qptr

{QType *front;//队头指针

QType *rear;//队尾指针

} LinkQueue;//链队结点类型

在这样的链队中,队空的条件是lq->front==NULL或lq->rear==NULL(这里采用lq->front==NULL的队空条件)。一般情况下,链队是不会出现队满的情况的。归纳起来,链队lq的4个要素如下。

(1) 队空条件: lq->front==NULL。

(2) 队满条件: 不考虑(因为每个结点是动态分配的)。

(3) 进队操作: 创建结点p,将其插入队尾,并由lq->rear指向它。

(4) 出队操作: 删除队头结点。

在链队上实现队列基本运算算法如下。

1. 初始化队列运算算法
其主要操作是: 创建链队结点,并置该结点的rear和front均为NULL。对应的算法如下。

void InitQueue(LinkQueue *&lq)//lq为引用型参数

{lq=(LinkQueue *)malloc(sizeof(LinkQueue));

lq->rear=lq->front=NULL;//初始时队头和队尾指针均为空

}

2. 销毁队列运算算法
链队的所有结点空间都是通过malloc函数分配的,在不再需要时需通过free函数释放所有结点的空间。在销毁队列lq时,先像释放单链表一样释放队中所有数据结点,然后释放链队结点lq。对应的算法如下。

void DestroyQueue(LinkQueue *&lq)

{QType *pre=lq->front,*p;

if (pre!=NULL)//非队空的情况

{if (pre==lq->rear)//只有一个数据结点的情况

free(pre);//释放pre结点

else//有两个或多个数据结点的情况

{p=pre->next;

while (p!=NULL)

{free(pre);//释放pre结点

pre=p; p=p->next;//pre、p同步后移

}

free(pre);//释放尾结点

}

}

free(lq);//释放链队结点

}

3. 进队运算算法
其主要操作是: 创建一个新结点s,将其链接到链队的末尾,并由rear指向它。对应的算法如下。

int EnQueue(LinkQueue *&lq,ElemType x) //lq为引用型参数

{QType *s;

s=(QType *)malloc(sizeof(QType));//创建新结点s,插入链队的末尾

s->data=x;s->next=NULL;

if (lq->front==NULL)//原队为空队的情况

lq->rear=lq->front=s;//front和rear均指向s结点

else//原队不为队空的情况

{lq->rear->next=s;//将结点s链到队尾

lq->rear=s;//rear指向它

}

return 1;

}

4. 出队运算算法
其主要操作是: 将front指向结点的data域值赋给x,并删除该结点。对应的算法如下。

int DeQueue(LinkQueue *&lq,ElemType &x)//lq,x均为引用型参数

{QType *p;

if (lq->front==NULL)//原队为队空的情况

return 0;

p=lq->front;//p指向队头结点

x=p->data;//取队头元素值

if (lq->rear==lq->front)//若原队列中只有一个结点,删除后队列变空

lq->rear=lq->front=NULL;

else//原队有两个或以上结点的情况

lq->front=lq->front->next;

free(p);

return 1;

}

5. 取队头元素运算算法
其主要操作是: 将front指向结点的data域值赋给x。对应的算法如下。

int GetHead(LinkQueue *lq,ElemType &x)//x为引用型参数

{if (lq->front==NULL)//原队为队空的情况

return 0;

x=lq->front->data;

return 1;

}

6. 判断队空运算算法
其主要操作是: 若链队为空,则返回1; 否则返回0。对应的算法如下。

int QueueEmpty(LinkQueue *lq)

{if (lq->front==NULL) return 1;//队空返回1

else return 0;//队不空返回0

}

提示: 将链队结点类型声明及其基本运算函数存放在LinkQueue.cpp文件中。

当链队的基本运算函数设计好后,给出以下程序调用这些基本运算函数,读者可以对照程序执行结果进行分析,进一步体会链队的各种运算的实现过程。

#include "LinkQueue.cpp"//包含前面的链队基本运算函数

int main()

{LinkQueue *lq;//定义一个链队lq

ElemType e;

printf("初始化队列\n");

InitQueue(lq);

printf("队%s\n",(QueueEmpty(lq)==1?"空":"不空"));

printf("a进队\n");EnQueue(lq,'a');

printf("b进队\n");EnQueue(lq,'b');

printf("c进队\n");EnQueue(lq,'c');

printf("d进队\n");EnQueue(lq,'d');

printf("队%s\n",(QueueEmpty(lq)==1?"空":"不空"));

GetHead(lq,e);

printf("队头元素:%c\n",e);

printf("出队次序:");

while (!QueueEmpty(lq))//队不空循环

{DeQueue(lq,e);//出队元素e

printf("%c ",e);//输出元素e

}

printf("\n");

DestroyQueue(lq);

}




上述程序的执行结果如图3.18所示。




【例3.16】若使用不带头结点的循环链表来表示队列,lq是这样的链表中尾结点指针,如图3.21所示。试基于此结构给出队列的相关运算算法。



图3.21用循环单链表表示链队



这里使用的循环链表不带头结点,lq始终指向队尾结点,lq->next即为队头结点。当lq==NULL时队列为空,lq->next==lq表示队列中只有一个结点。队列的相关运算算法如下。

typedef struct node

{ElemType data;//数据域

struct node *next;//指针域

} QNode;//链队中数据结点类型


//---初始化队列运算算法---

void InitQueue(QNode *&lq)

{lq=NULL;  }


//----销毁链队----

void DestroyQueue(QNode *&lq)

{QNode *pre,*p;

if (lq!=NULL)

{if (lq->next==lq)//原队中只有一个结点

free(lq);

else//原队中有两个或以上的结点

{pre=lq;

p=pre->next;

while (p!=lq)

{free(pre);

pre=p;

p=p->next;//pre和p同步后移

}

free(pre);//释放最后一个结点

}

}

}


//----进队运算算法----

void EnQueue(QNode *&lq,ElemType x)

{QNode *s;

s=(QNode *)malloc(sizeof(QNode));

s->data=x;//创建存放x的结点s

if (lq==NULL)//原队为队空的情况

{lq=s;

lq->next=lq;//构成循环单链表

}

else//原队不空,结点s插到队尾,并由lq指向它

{s->next=lq->next; 

lq->next=s;

lq=s;//lq指向结点s

}

}


//-----出队运算算法-----

int DeQueue(QNode *&lq,ElemType &x)

{QNode *s;

if (lq==NULL) return 0;//原队为队空的情况

if (lq->next==lq)//原队只有一个结点

{x=lq->data;

free(lq);

lq=NULL;

}

else//原队有两个或以上的结点,删除队头结点

{s=lq->next;//将结点lq之后的结点s删除

x=s->data;

lq->next=s->next;

free(s);

}

return 1;

}


//----取队头元素运算算法----

int GetHead(QNode *lq,ElemType &x)

{if (lq==NULL) return 0;//原队为队空的情况

x=lq->next->data;

return 1;

}


//----判断队空运算算法----

int QueueEmpty(QNode *lq)

{if (lq==NULL) return 1;//队空返回1

else return 0;//队不空返回0

}



【例3.17】以下各种不带头结点的链表中最不适合用作链队的是。


A.  只带队首指针的非循环双链表B.  只带队首指针的循环双链表

C.   只带队尾指针的循环双链表D.  只带队尾指针的循环单链表

队列最基本的运算是进队和出队。链队的队头和队尾分别在链表的前、后端,即进队在链尾进行,出队在链首进行。

对于选项A的只带队首指针的非循环双链表,在末尾插入一个结点(进队)的时间复杂度为O(n),其他选项的链表完成同样操作的时间复杂度均为O(1),所以相比较而言,选项A的存储结构最不适合用作链队。本题答案为A。





3.2.4队列的应用示例

在较复杂的数据处理过程中,通常需要保存多个临时产生的数据,如果先产生的数据先进行处理,那么需要用队列来保存这些数据。下面通过一个典型示例说明队列的应用。

【例3.18】设计一个程序,反映病人到医院看病、排队看医生的过程。

(1) 设计存储结构。

病人排队看医生采用先到先看的方式,所以要用到一个队列。由于病人人数具有较大的不确定性,这里采用一个带头结点的单链表作为队列的存储结构。为了简单,病人通过其姓名来唯一标识。例如,有Smith、John和Mary三个病人依次排队的病人队列如图3.22所示。



图3.22病人队列


病人链队结点类型如下。

typedef struct 

{QType *front;//指向队头病人结点

QType *rear;//指向队尾病人结点

} LQueue;//病人链队类型

病人链队中的结点类型如下。

typedef struct Lnode

{char data[10];//存放患者姓名

struct Lnode *next;//指针域

} QType;//链队中结点类型

(2) 设计运算算法。

在病人队列设计好后,设计相关的基本运算算法,如队列初始化、进队和出队等,这些算法如下。

//---初始化队列运算算法---

void InitQueue(LQueue *&lq)

{lq=(LQueue *)malloc(sizeof(LQueue));

lq->rear=lq->front=NULL;//初始时队头和队尾指针均为空

}


//----销毁链队----

void DestroyQueue(LQueue *&lq)

{QType *pre=lq->front,*p;

if (pre!=NULL)//非队空的情况

{if (pre==lq->rear)//只有一个数据结点的情况

free(pre);//释放pre结点

else//有两个或多个数据结点的情况

{p=pre->next;

while (p!=NULL)

{free(pre);//释放pre结点

pre=p; p=p->next;//pre、p同步后移

}

}

free(pre);//释放尾结点

}

free(lq);//释放链队结点

}


//----进队运算算法----

void EnQueue(LQueue *&lq,char x[ ])

{QType *s;

s=(QType *)malloc(sizeof(QType));   //创建新结点,插入链队的末尾

strcpy(s->data,x);s->next=NULL;

if (lq->front==NULL)//原队为队空的情况

lq->rear=lq->front=s;//front和rear均指向s结点

else//原队不为队空的情况

{lq->rear->next=s;//将结点s链到队尾

lq->rear=s;//rear指向结点s

}

}


//-----出队运算算法-----

int DeQueue(LQueue *&lq,char x[ ])

{QType *p;

if (lq->front==NULL)//原队为队空的情况

return 0;

p=lq->front;//p指向队头结点

strcpy(x,p->data);//取队头元素值

if (lq->rear==lq->front)//若原队列中只有一个结点,删除后队列变空

lq->rear=lq->front=NULL;

else//原队有两个或以上结点的情况

lq->front=lq->front->next;

free(p);

return 1;

}


//----判断队空运算算法----

int QueueEmpty(LQueue *lq)

{if (lq->front==NULL) return 1;//队空返回1

else return 0;//队不空返回0

}


//----输出队中所有元素的算法----

int DispQueue(LQueue *lq)

{QType *p;

if (QueueEmpty(lq)) return 0; //队空返回0

else

{p=lq->front;

while (p!=NULL)

{printf("%s ",p->data);

p=p->next;

}

printf("\n");

return 1;//队不空返回1

}

}

(3) 设计主函数。

然后设计如下主函数通过简单的提示性菜单方式来操作各个功能。

int main()

{int sel,flag=1;

char name[10];

LQueue *lq;//定义一个病人队列

InitQueue(lq);//初始化病人队列

while (flag==1) //未下班时循环执行

{printf("1:排队 2:看医生 3:查看排队 0:下班  请选择:");

scanf("%d",&sel);//选择一项操作

switch(sel)

{

case 0://医生下班

if (!QueueEmpty(lq))

printf(">>请排队的患者明天就医\n");

DestroyQueue(lq);

flag=0;

break;

case 1://一个病人排队

printf(">>输入患者姓名:");

scanf("%s",name);

EnQueue(lq,name);

break;

case 2://一个病人看医生

if (!DeQueue(lq,name))

printf(">>没有排队的患者\n");

else

printf(">>患者%s看医生\n",name);

break;

case 3://查看目前病人排队情况

printf(">>排队患者:");

if (!DispQueue(lq))

printf(">>没有排队的患者\n");

break;

}

}

}

(4) 执行结果。

本程序的一次执行结果如图3.23所示。



图3.23看病程序的一次执行结果


小结

(1) 栈和队列的共同点是,它们的数据元素都呈线性关系,且只允许在端点处插入和删除元素。

(2) 栈是一种“后进先出”或者“先进后出”的数据结构,只能在一端进行元素的插入和删除。


(3) 栈可以采用顺序栈和链栈两类存储结构。

(4) n个不同元素的进栈顺序和出栈顺序不一定相同。

(5) 在顺序栈中,通常用栈顶指针指向栈顶元素,栈顶指针类型为int类型。

(6) 在顺序栈中,进栈和出栈操作仅改变栈顶元素不涉及栈中其他元素的移动。

(7) 无论是顺序栈还是链栈,进栈和出栈运算的时间复杂度均为O(1)。

(8) 队列是一种“先进先出”或者“后进后出”的数据结构,只能从一端插入元素,另一端删除元素。

(9) 队列可以采用顺序队和链队两类存储结构。

(10) n个元素进队的顺序和出队顺序总是一致的。

(11) 顺序队中的元素个数可以由队头指针和队尾指针计算出来。

(12) 循环队列也是一种顺序队,是通过逻辑方法使其首尾相连,解决非循环队列的假溢出现象。

(13) 无论是顺序队还是链队,进队和出队运算的时间复杂度均为O(1)。

(14) 在算法设计中通常用栈或者队列保存临时数据,如果先保存的元素先处理,采用队列; 如果后保存的元素先处理,采用栈。

练习题













上机实验题









华为的专利






专利的质量与数量是企业创新能力和核心竞争能力的体现。据国家知识产权局知识产权发展研究中心公布的数据显示,在当前全球声明的21万余件5G标准专利中,中国声明的专利占比近40%,排名世界第一,其中华为公司5G标准专利族6500余项,占比14%,位居全球首位。在全球申请专利的约3.8万项6G技术中,中国以35%的占有率居首位,而其中大部分专利都是华为申请的。