文章目录
一、栈
1.栈的定义
栈: 一种只能在一端进行插入或删除的线性表
栈顶:表中允许进行插入、删除操作的一端
栈底:表的另一无法进行上述操作的端
栈顶指针:栈顶的当前位置
空栈:栈中没有数据元素
进栈\入栈:栈的插入操作
出栈\退栈:栈的删除操作
栈的主要特点:后进先出
- 栈的抽象数据类型
ADT Stack{
数据对象:
D={ai | 1≤i≤n,n≥0, ai为ElemType类型}
数据关系:
R={<ai, ai+1> | ai, ai+1∈D且ai≤ai+1, i=1,···,n-1}
基本运算:
InitStack(&S):初始化栈,构造一个空栈
DestroyStack(&S):销毁栈,释放为栈分配的存储空间
StackEmpty(S):判断栈是否为空栈,若栈S为空表,则返回真,否则返回假
Push(&s,e):进栈,将元素e插入栈s中作为栈顶元素
Pop(&s,&e):出栈,从栈s中删除栈顶元素,并将其值赋给e
GetTop(s,&e):取栈顶元素,返回当前的栈顶元素,并将其值赋给e
}
- ps:n个不同的元素通过一个栈产生的出栈序列的个数为
C 2 n n / n + 1 C^n_{2n}/n+1 C2nn/n+1
2.栈的顺序存储结构及其基本运算的实现
顺序栈:采用顺序存储结构的栈
- 声明顺序栈的类型:
typedef struct
{
ElemType data[MaxSize]; //存放栈中的数据元素
int top; //栈顶指针,即存放栈顶元素在data数组中的下标
} SqStack; //顺序栈类型
指示图
-
对于栈的算法设计的四个重要元素
1.栈空的条件:s->top==-1
2.栈满的条件:s->top==MaxSize-1(data数组的最大下标)
3.元素e进栈的操作:先将栈顶指针top增1,然后将元素e放在栈顶指针处
4.出栈的操作:先将栈顶指针top处的元素取出放在e中,然后将栈顶指针减1
-
初始化栈:InitStack(&S)
void InitStack(SqStack *&s) //初始化顺序栈
{
s=(SqStack *)malloc(sizeof(SqStack)); //分配一个顺序栈空间,首地址存放在s中
s->top=-1; //栈顶指针置为1
}
- 销毁栈:DestroyStack(&s)
void DestroyStack(SqStack *&s) //销毁顺序栈
{
free(s);
}
- 判断栈是否为空:StackEmpty(s)
bool StackEmpty(SqStack *s) //判断栈空否
{
return(s->top==-1);
}
- 进栈:Push(&s,e)
bool Push(SqStack *&s,ElemType e) //进栈
{
if (s->top==MaxSize-1) //栈满的情况,即栈上溢出
return false;
s->top++; //栈顶指针增1
s->data[s->top]=e; //元素e放在栈顶指针处
return true;
}
- 出栈:Pop(&s,&e)
bool Pop(SqStack *&s,ElemType &e) //出栈
{
if (s->top==-1) //栈为空的情况,即栈下溢出
return false;
e=s->data[s->top]; //取栈顶元素
s->top--; //栈顶指针增1
return true;
}
- 取栈顶元素:GetTop(s,&e)
bool GetTop(SqStack *s,ElemType &e) //取栈顶元素
{
if (s->top==-1) //栈为空的情况,即栈下溢出
return false;
e=s->data[s->top]; //取栈顶元素
return true;
}
- n个元素连续进栈,产生的连续出栈序列和输出序列正好相反
- 共享栈:如下图,用一个数组实现两个栈
1.共享栈的4个要素:
(1)栈空的条件:栈1空为top1==-1;栈2空为top2== MaxSize
(2)栈满的条件:top1==top2-1(data数组的最大下标)
(3)元素x进栈的操作:进栈1的操作为top1++;data[top1]=x; 进栈2的操作为top2- -;data[top2]=x;
(4)出栈的操作:出栈1的操作为x=data[top1];top1- -; 出栈2的操作为x=data[top2];top2++;
2.实现共享栈的基本运算算法时需要增加一个形参i,指出是对哪个栈进行操作。
3.栈的链式存储结构及其基本运算的实现
链栈:采用链式存储结构的栈
链栈的优点:不存在栈满上溢出的情况。规定栈的所有操作都是在单链表的表头进行的(因为给定链栈后,已知头结点的地址,在其后面插人一个新结点和删除首结点都十分方便,对应算法的时间复杂度均为O(1))。
- 链栈中结点类型声明:
typedef struct linknode
{
ElemType data; //数据域
struct linknode *next; //指针域
} LinkStNode; //链栈结点类型
-
链栈算法设计四要素
1.栈空的条件:s->next==NULL。
2.栈满的条件:由于只有内存溢出时才出现栈满,通常不考虑这样的情况,所以在链栈中可以看成不存在栈满。
3.元素e进栈的操作:新建一个结点存放元素e(由p指向它),将结点p插入头结点之后。
4.出栈的操作:取出首结点的data值并将其删除。
-
初始化栈:InitStack(&s)
void InitStack(LinkStNode *&s) //初始化链栈
{
s=(LinkStNode *)malloc(sizeof(LinkStNode));
s->next=NULL;
}
- 销毁栈:DestroyStack(&s)
void DestroyStack(LinkStNode *&s) //销毁链栈
{
LinkStNode *pre=s,*p=s->next; //pre指向头结点,p指向首结点
while (p!=NULL) //循环到p为空
{
free(pre); //释放p结点
pre=p; //pre,p同步后移
p=pre->next;
}
free(pre); //pre指向尾节点,释放其空间
}
- 判断栈是否为空:StackEmpty(s)
bool StackEmpty(LinkStNode *s) //判断栈空否
{
return(s->next==NULL);
}
- 进栈:Push(&s,e)
void Push(LinkStNode *&s,ElemType e) //进栈
{ LinkStNode *p;
p=(LinkStNode *)malloc(sizeof(LinkStNode)); //新建结点p
p->data=e; //存放元素e
p->next=s->next; //插入p结点作为首结点
s->next=p;
}
- 出栈:Pop(&s,&e)
bool Pop(LinkStNode *&s,ElemType &e) //出栈
{ LinkStNode *p;
if (s->next==NULL) //栈空的情况
return false;
p=s->next; //p指向首结点
e=p->data; //提取首结点的值
s->next=p->next; //删除首结点
free(p); //释放被删结点的存储空间
return true;
}
- 取栈顶元素:GetTop(s,&e)
bool GetTop(LinkStNode *s,ElemType &e) //取栈顶元素
{ if (s->next==NULL) //栈空的情况
return false;
e=s->next->data; //提取首结点的值
return true;
}
4.栈的应用
1.简单表达式求值
1.问题描述
用户输入一个包含十、一、*、/、正整数和圆括号的合法算术表达式,计算该表达式的运算结果。
2.数据组织
简单表达式采用字符数组 exp表示,其中只含有+、-、* 、/、正整数和圆括号。假设该表达式都是合法的算术表达式,如exp=“1+2 * (4+12)”。
3.设计运算算法
中缀表达式是运算符位于两个操作数中间的表达式,如1+2*3。对其运算一般遵循“先乘除,后加减,从左到右计算,先括号内,后括号外”的规则,因此中缀表达式不仅要依赖运算符的优先级,还要处理括号
后缀表达式(逆波兰表达式):在算术表达式中运算符在操作数的后面,例如1+2 * 3的后缀表达式为 123 *+。在后缀表达式中已经考虑了运算符的优先级,没有括号,只有操作数和运算符,而且越放在前面的运算符越优先执行。
前缀表达式:在算术表达式中,如果运算符在操作数的前面,如1+2 * 3的前缀表达式为+ 1 * 23。
后缀表达式将复杂表达式转换为可以依靠简单的操作得到计算结果的表达式。所以对中缀表达式的求值过程是先将中缀算术表达式转换成后缀表达式,然后对该后缀表达式求值
1)将算术表达式转换成后缀表达式。
在将一个中缀表达式转换成后缀表达式时,操作数之间的相对次序是不变的,但运算符的相对次序可能不同,同时还要除去括号。所以在转换时需要从左到右遍历算术表达式,将遇到的操作数直接存放到后缀表达式中,将遇到的每一个运算符或者左括号都暂时保存到运算符栈,而且先执行的运算符先出栈。
while(从 exp读取字符ch, ch!=‘\0’){
ch为数字:将后续的所有数字均依次存放到 postexp中,并以字符’#‘标识数字串结束;
ch为左括号’(’:将此括号进到 Optr栈中;
ch为右括号’)‘:将 Optr中出栈时遇到的第1个左括号’(‘以前的运算符依次出栈并存放到 postexp中,然后将左括号’(‘出栈;
ch为’+‘或’-’:出栈运算符并存放到 postexp中,直到栈空或者栈顶为’(‘,然后将 ch进栈;
ch为’*‘或’/‘:出栈运算符并存放到 postexp中,直到栈空或者栈顶为’(‘、’+‘或’-',然后将ch 进栈;
}
若exp遍历完毕,则将 Optr中的所有运算符依次出栈并存放到 postexp中。
设置运算符栈类型SqStack 中的 ElemType 为char 类型。根据上述原理得到的 trans()算法如下:
void trans(char * exp, char postexp[]) //将算术表达式exp转换成后缀表达式 postexp
{ char e;
SqStack * Optr; //定义运算符栈指针
InitStack(Optr); //初始化运算符栈
int i=0; //i作为 postexp的下标
while(* exp!='\0') //exp表达式未遍历完时循环
{ switch(* exp)
{
case '(':
Push(Optr,'('); //左括号进栈
exp++; //继续遍历其他字符
break;
case ')': //判定为右括号
Pop(Optr,e); //出栈元素e
while(e!='(') //不为'('时循环
{ postexp[i++]=e; //将 e存放到 postexp中
Pop(Optr,e); //继续出栈元素e
}
exp++; //继续遍历其他字符
break;
case '+': //判定为加号或减号
case '-':
while(!StackEmpty(Optr)) //栈不空时循环
{ GetTop(Optr,e); //取栈顶元素 e
if(e!='(') //e不是'('
{ postexp[i++]=e; //将e存放到 postexp中
Pop(Optr,e); //出栈元素e
}
else //e是'('时退出循环
break;
}
Push(Optr, * exp); //将'+'或'-'进栈
exp++; //继续遍历其他字符
break;
case '*': //判定为'*'或'/'号
case '/':
while(!StackEmpty(Optr)) //栈不空时循环
{ GetTop(Optr,e); //取栈顶元素 e
if(e=='*'|| e=='/') //将栈顶'*'或'/'运算符出栈并存放到 postexp中
{ postexp[i++]=e; //将e存放到 postexp中
Pop(Optr,e); //出栈元素 e
}
else //e为非'*'或'/'运算符时退出循环
break;
}
Push(Optr, * exp); //将'*'或'/'进栈
exp++; //继续遍历其他字符
break;
default: //处理数字字符
while(*exp>='0'&&*exp<='9')
{ postexp[i++]=*exp;
exp++;
}
postexp[i++]='#'; //用#标识一个数字串结束
}
}
while(!StackEmpty(Optr)) //此时exp遍历完毕,栈不空时循环
{ Pop(Optr,e); //出栈元素e
postexp[i++]=e; //将e存放到 postexp中
}
postexp[i]='\0'; //给 postexp 表达式添加结束标识
DestroyStack(Optr); //销毁栈
〉
(2)后缀表达式求值。
后缀表达式的求值过程是从左到右遍历后缀表达式postexp,若读取的是一个操作数,将它进操作数栈,若读取的是一个运算符op,从操作数栈中连续出栈两个操作数,假设为a(第1个出栈的元素)和b(第2个出栈的元素),计算b op a的值,并将计算结果进操作数栈。当整个后缀表达式遍历结束时,操作数栈中的栈顶元素就是表达式的计算结果。
在后缀表达式求值算法设计中操作数栈为 Opnd,用于临时存放要进行某种算术运算的操作数。下面给出后缀表达式求值的过程,假设postexp存放的后缀表达式是正确的,在while循环结束后,Opnd栈中恰好有一个操作数,它就是该后缀表达式的求值结果。
while(从 postexp读取字符ch, ch!=‘\0’)
{ ch为’+‘: 从 Opnd栈中出栈两个数值 a和b,计算c=b+a;将c进栈;
ch为’-‘:从 Opnd栈中出栈两个数值 a和b,计算c=b-a;将c进栈;
ch为’':从 Opnd栈中出栈两个数值 a 和b,计算c=ba;将c进栈;
ch为’/':从 Opnd栈中出栈两个数值 a 和b,若a不为零,计算c=b/a;将c进栈;
ch为数字字符:将连续的数字串转换成数值d,将d进栈;
}
返回Opnd栈的栈顶操作数(即后缀表达式的值);
double compvalue(char * postexp) //计算后缀表达式的值
{ double d,a,b,c,e;
SqStack1 * Opnd; //定义操作数栈
InitStack1(Opnd); //初始化操作数栈
while(* postexp!='\0') //postexp字符串未遍历完时循环
{ switch (* postexp)
{
case '+': //判定为'+'号
Pop1(Opnd,a); //出栈元素 a
Pop1(Opnd,b); //出栈元素 b
c=b+a; //计算c
Push1(Opnd,c); //将计算结果c进栈
break;
case '-': //判定为'-'号
Pop1(Opnd,a); //出栈元素 a
Pop1(Opnd,b); //出栈元素 b
c=b-a; //计算c
Push1(Opnd,c); //将计算结果 c进栈
break;
case '*': //判定为'*'号
Pop1(Opnd,a); //出栈元素a
Pop1(Opnd,b); //出栈元素 b
c=b*a; //计算 c
Push1(Opnd,c); //将计算结果c进栈
break;
case '/': //判定为'/'号
Pop1(Opnd,a); //出栈元素 a
Pop1(Opnd,b); //出栈元素 b
if(a!=0)
{ c=b/a; //计算 c
Push1(Opnd,c); //将计算结果 c进栈
break;
}
else
{ printf("\n\t除零错误!\n");
exit(0); //异常退出
}
break;
default: //处理数字字符
d=0; //将连续的数字字符转换成对应的数值存放到d中
while (*postexp>='0'&&* postexp<='9')
{ d=10*d+*postexp-'0';
postexp++;
}
Push1(Opnd,d); //将数值d进栈
break;
}
postexp++; //继续处理其他字符
}
GetTop1(Opnd,e); //取栈顶元素e
DestroyStack1(Opnd); //销毁栈
return e; //返回e
}
4)设计求解程序
int main()
{ char exp[]="(56-20)/(4+2)"; //可将 exp改为键盘输入
char postexp[MaxSize];
trans(exp, postexp); //将 exp转换为 postexp
printf("中缀表达式:%s\n", exp); //输出 exp
printf("后缀表达式:%s\n", postexp);//输出 postexp
printf("表达式的值:%g\n", compvalue(postexp)) ;//求 postexp 的值并输出
return 1;
}
- 运行结果
运行本程序,得到对应的结果如下:
中缀表达式:(56-20)/(4+2)
后缀表达式:56#20#-4#2#+/
表达式的值:6
2.求解迷宫问题
1)问题描述
给定一个M X N的迷宫图,求一条从指定入口到出口的迷宫路径,在行走中一步只能从当前方块移动到上、下、左、右相邻方块中的一个方块。假设一个迷宫图如下图所示(这里M=8,N=8),其中的每个方块用空白表示通道,用阴影表示障碍物。
一般情况下,所求迷宫路径是简单路径,即在求得的迷宫路径上不会重复出现同一个方块。一个迷宫图的迷宫路径可能有多条,这些迷宫路径有长有短,这里仅考虑用栈求一条从指定入口到出口的迷宫路径。
2)数据组织
为了表示迷宫,设置一个数组mg,其中每个元素表示一个方块的状态,为0时表示对应方块是通道,为1时表示对应方块是障碍物(不可走)。为了算法方便,一般在迷宫的外围加一条围墙。下图所示的迷宫对应的迷宫数组mg(由于迷宫的四周加了一道围墙,故mg数组的行数和列数均加上2)如下:
int mg[M+2][N+2]=
(1,1,1,1,1,1,1,1,1,1},{1,0,0,1,0,0,0,1,0,1),
{1,0,0,1,0,0,0,1,0,1},{1,0,0,0,0,1,1,0,0,1),
{1,0,1,1,1,0,0,0,0,1),{1,0,0,0,1,0,0,0,0,1},
(1,0,1,0,0,0,1,0,0,1),(1,0,1,1,1,0,1,1,0,1},
(1,1,0,0,0,0,0,0,0,1},(1,1,1,1,1,1,1,1,1,1));
另外,在算法中用到的栈采用顺序栈存储结构,即将速宫栈声明如下:
typedef struct{
int i;
int j;
int di;
}Box;
typedef struct{
Box data[MaxSize];
int top;
}StType;
3)设计运算算法
求解迷宫中从入口(xi, yi)到出口(xe,ye)的一条迷宫路径的过程如下:
将入口(xi, yi)进栈(具其初始方位设置为-1);
mg[xi][yi]=一1;
while(栈不空)
{ 取栈顶方块(i,j,di);
if((i,j)是出口(xe,ye))
{ 输出栈中的全部方块构成一条迷宫路径;
return true;
}
查找(i,j, di)的下一个相邻可走方块;
if(找到一个相邻可走方块)
{ 该方块位置为(i1,j1),对应方位d;
将栈顶方块的di设置为d;
(i1,j1,-1)进栈;
mg[i1][j1]=-1;
}
if(没有找到(i,j,di)的任何相邻可走方块)
{ 将(i,j,di)出栈;
mg[i][j]=0;
}
}
return false; //没有找到迷宫路径
根据上述过程得到求迷宫问题的算法如下:
//求解路径为(xi, yi)→>(xe,ye)
bool mgpath(int xi,int yi, int xe, int ye)
{ Box path[MaxSize],e;
int i,j,di, i1,j1,k;
bool find;
StType *St;//定义栈st
InitStack(st);//初始化栈顶指针
e.i=xi;e.j=yi;e.di=-1; //设置e为入口
Push(st, e);//方块e进栈
mg[xi][yi]=—1;//将入口的迷宫值置为-1,避免重复走到该方块
while (!StackEmpty(st))//栈不空时循环
{ GetTop(st,e);//取栈顶方块e
i=e.i;je.j; di=e.di;
if(i==xe && j==ye)//找到了出口,输出该路径
{ printf("一条迷宫路径如下:\n");
k=0;//k表示路径中的方块数
while (!StackEmpty(st))
{ Pop(st,e);//出栈方块e
path[k++]=e;//将e添加到 path 数组中
}
while(k>0)
{ printf("\t(%d, %d)",path[k-1].i,path[k-1].j);
if ((k+1)%5==0)//每输出5个方块后换一行
printf("\n");
k--;
}
printf("\n");
DestroyStack(st);//销毁栈
return true;//输出一条迷宫路径后返回true
}
find=false;
while(di<4 && !find)//找方块(i,j)的下一个相邻可走方块(i1,j1)
{ di++;
switch(di)
{
case 0:i1=i-1;j1=j;break;
case 1:i1=i;j1=j+1;break;
case 2:i1=i+1;j1=j;break;
case 3:i1=i;j1=j-l;break;
}
if (mg[i1][j1]==0)find=true;//找到一个相邻可走方块,设置find为真
}
if (find)//找到了一个相邻可走方块(i1,j1)
{ st-> data[st -> top].di= di;//修改原栈顶元素的di值
e.i=i1;e.j=j1;e.di=—1;
Push(st,e);//相邻可走方块e进栈
mg[i1][j1]=-1;//将(i1,j1)迷宫值置为-1,避免重复走到该方块
}
else//没有路径可走,则退栈
{ Pop(st,e);//将栈顶方块退栈
mg[e.i][e.j]=0;//让退栈方块的位置变为其他路径可走方块
}
DestroyStack(st);//销毁栈
return false;//表示没有可走路径,返回false
}
4)设计求解程序
建立以下主函数调用上述算法:
int main()
{ if(!mgpath(1,1,M,N))
printf("该迷宫问题没有解!”);
return 1;
}
5)运行结果
从入口(1,1)到出口(8,8)的求解结果如下:
一条迷宫路径如下:
(1,1) (1,2) (2,2) (3,2) (3,1)
(4,1) (5,1) (5,2) (5,3) (6,3)
(6,4) (6,5) (5,5) (4,5) (4,6)
(4,7) (3,7) (3,8) (4,8) (5,8)
(6,8) (7,8) (8,8)
上述迷宫路径的显示结果如图3.14所示,图中路径上方块(i,j)中的箭头表示从该方块行走到下一个相邻方块的方位,例如方块(1,1)中的箭头是“→”,该箭头表示方位1,即方块(1,1)走方位1到相邻方块(1,2)。显然这个解不是最优解,即不是最短路径,在使用队列求解时可以找出最短路径,这将在后面介绍。
实际上,在使用栈求解迷宫问题时,当找到出口后输出一个迷宫路径,然后可以继续回溯搜索下一条迷宫路径。采用这种回溯方法可以找出所有的迷宫路径。
二、队列
1.队列的定义
队列:仅允许在表的一端进行插入操作,而在表的另一端进行删除操作。
队头或队首:把进行删除的一端
队尾:把进行插入的一端
进队或入队:向队列中插入新元素
出队或离队:从队列中删除元素元素出队后
队列是先进先出表。
ADT Queue{
数据对象:
D={ai |1≤i≤n,n≥0, ai为ElemType类型}
数据关系:
R={<ai, ai+1> | ai, ai+1∈D, i=1,···,n-1}
基本运算:
InitQueue(&q):初始化队列,构造一个空队列q
DestroyQueue(&q):销毁队列,释放为队列q分配的存储空间
QueueEmpty(q):判断队列是否为空队列,若队列q为空表,则返回真,否则返回假
enQueue(&q,e):进队列,将元素e进队作为队尾元素
deQueue(&q,&e):出队列,从队列q中出队一个元素,并将其值赋给e
}
2.队列的顺序存储结构及其基本运算的实现
顺序队:采用顺序存储结构的队列
队首指针和队尾指针:分别存放队首元素和队尾元素的下标的整型变量
typedef struct
{
ElemType data[MaxSize];
int front,rear; //队头和队尾指针
} SqQueue;
1)在顺序队中实现队列的基本运算
对于q所指的顺序队(即顺序队 q),初始时设置q->rear=q->front=-1,可以归纳出对后面的算法设计来说非常重要的4个要素。
- 队空的条件:q->front==q->rear。
- 队满的条件:q->rear==MaxSize-1(data数组的最大下标)。
- 元素e进队的操作:先将 rear增1,然后将元素e 放在 data数组的rear位置。
- 出队的操作:先将 front增1,然后取出 data 数组中front位置的元素。
(1)初始化队列:InitQueue(&q)
void InitQueue(SqQueue *&q)
{ q=(SqQueue *)malloc (sizeof(SqQueue));
q->front=q->rear=-1;
}
(2)销毁队列:DestroyQueue(&q)
void DestroyQueue(SqQueue *&q)
{
free(q);
}
(3)判断队列是否为空:QueueEmpty(q)
bool QueueEmpty(SqQueue *q)
{
return(q->front==q->rear);
}
(4)进队列enQueue(&q, e)
bool enQueue(SqQueue *&q,ElemType e) //进队
{ if (q->rear==MaxSize-1) //队满上溢出
return false; //返回假
q->rear++; //队尾增1
q->data[q->rear]=e; //rear位置插入元素e
return true; //返回真
}
(5)出队列deQueue(&q, &e)
bool deQueue(SqQueue *&q,ElemType &e) //出队
{ if (q->front==q->rear) //队空下溢出
return false;
q->front++;
e=q->data[q->front];
return true;
}
2)在环形队中实现队列的基本运算
- 假溢出:在前面的顺序队操作中,元素进队时队尾指针rear增1,元素出队时队头指针front增1,当队满的条件(即rear== MaxSize-1)成立时,表示此时队满(上溢出)了,不能再进队元素。实际上,当rear==MaxSize-1成立时,队列中可能还有空位置,假溢出就是指这种因为队满条件设置不合理导致队满条件成立而队列中仍然有空位置的情况。
- 环形队列(循环队列):把data数组的前端和后端连接起来,形成一个环形数组,即把存储队列元素的数组从逻辑上看成一个环。
环形队列就可以解决假溢出的情况。 - 环形队列首尾相连后,当队尾指针rear=MaxSize-1后,再前进一个位置就到达0,于是就可以使用另一端的空位置存放队列元素了。实际上存储器中的地址总是连续编号的,为此采用数学上的求余运算(%)来实现:
队头指针front循环增1:front=(front+1)%MaxSize
队尾指针rear循环增1:rear=(rear+1)%MaxSize
- 算法设计的4要素
队空条件:q->rear=q->front
队满条件:(q->rear+1)%MaxSize=q->front
进队操作:队尾循环进1
出队操作:队头循环进1
(1)初始化队列:InitQueue(&q)
void InitQueue(SqQueue *&q) //初始化队列q
{ q=(SqQueue *)malloc (sizeof(SqQueue));
q->front=q->rear=0;
}
(2)销毁队列:DestroyQueue(&q)
void DestroyQueue(SqQueue *&q) //销毁队列q
{
free(q);
}
(3)判断队列是否为空:QueueEmpty(q)
bool QueueEmpty(SqQueue *q) //判断队q是否空
{
return(q->front==q->rear);
}
(4)进队列:enQueue(&q, e)
bool enQueue(SqQueue *&q,ElemType e) //进队
{ if ((q->rear+1)%MaxSize==q->front) //队满上溢出
return false;
q->rear=(q->rear+1)%MaxSize;
q->data[q->rear]=e;
return true;
}
(5)出队列:deQueue(&q, &e)
bool deQueue(SqQueue *&q,ElemType &e) //出队
{ if (q->front==q->rear) //队空下溢出
return false;
q->front=(q->front+1)%MaxSize;
e=q->data[q->front];
return true;
}
- 求队列中的元素个数
A.方法1:在环形队列中增加一个表示队中元素个数的 count域,在进队、出队元素时维护count的正确性,也就是说初始时count=0,在进队一个元素时执行count++,在出队一个元素时执行 count–。
B.方法2:利用环形队列状态求元素的个数,由于队头指针 front指向队头元素的前一个位置,队尾指针 rear指向队尾元素的位置,它们都是已知的,可以求出队中元素的个数=(rear-front+MaxSize)%MaxSize。在前面的环形队列中增加如下求元素个数的算法:
int Count(SqQueue *q) //求队列中元素的个数
{
return (q-> rear -q-> front+MaxSize)%MaxSize;
说明:在环形队列中,随着多次进队和出队,已出队元素的空间可能被新进队的元素覆盖,而非环形队列中已出队的元素仍在其中,不会被覆盖。如果需要利用出队的元素来求解时采用非环形队列为好,例如用队列求解迷宫问题就属于这种情况。
3.队列的链式存储结构及其基本运算的实现
链队:采用链式存储结构的队列
在这样的链队中只允许在单链表的表头进行删除操作(出队)和在单链表的表尾进行插入操作(进队),因此需要使用队头指针front和队尾指针rear 两个指针,用 front 指向队首结点,用rear指向队尾结点。和链栈一样,链队中也不存在队满上溢出的情况
链队的存储结构如图所示。
链队中数据结点的类型 DataNode声明如下:
typedef struct qnode
{ ElemType data; //存放元素
struct qnode * next; //下一个结点指针
} DataNode; //链队数据结点的类型
链队头结点(或链队结点)的类型 LinkQuNode声明如下:
typedef struct
{ DataNode *front; //指向队首结点
DataNode * rear; //指向队尾结点
}LinkQuNode; //链队结点的类型
在以q为链队结点指针的链队(简称链队q)中,可以归纳出对后面的算法设计来说非常重要的4个要素。
- 队空的条件:q->rear == NULL(也可以为q->front==NULL)。
- 队满的条件:不考虑。
- 元素e进队的操作:新建一个结点存放元素e(由p指向它),将结点 p插入作为尾结点。
- 出队的操作:取出队首结点的 data 值并将其删除。
(1)初始化队列:InitQueue(&q)
void InitQueue(LinkQuNode *&q) //初始化队列q
{
q=(LinkQuNode *)malloc(sizeof(LinkQuNode));
q->front=q->rear=NULL;
}
(2)销毁队列:DestroyQueue(&q)
void DestroyQueue(LinkQuNode *&q) //销毁队列q
{
DataNode *pre=q->front,*p; //p指向队首结点
if (pre!=NULL)
{ p=pre->next; //p指向pre结点的后继结点
while (p!=NULL) //p不空时循环
{ free(pre); //释放pre结点
pre=p;p=p->next; //p,pre同步后移
}
free(pre); //释放最后一个数据结点
}
free(q); //释放链队结点占用空间
}
(3)判断队列是否为空:QueueEmpty(q)
bool QueueEmpty(LinkQuNode *q)
{
return(q->rear==NULL);
}
(4)进队列:enQueue(&q, e)
bool enQueue(LinkQuNode *&q,ElemType e) //进队
{ DataNode *p;
p=(DataNode *)malloc(sizeof(DataNode));//创建新结点
p->data=e;
p->next=NULL;
if (q->rear==NULL) //若链队为空,则新节点是队首节点又是队尾节点
q->front=q->rear=p;
else
{ q->rear->next=p; //将p节点链到队尾,并将rear指向它
q->rear=p;
}
return true;
}
(5)出队列:deQueue(&q, &e)
bool deQueue(LinkQuNode *&q,ElemType &e) //出队
{ DataNode *t;
if (q->rear==NULL) //队列为空
return false;
t=q->front; //t指向第一个数据结点
if (q->front==q->rear) //队列中只有一个结点时
q->front=q->rear=NULL;
else //队列中有多个结点时
q->front=q->front->next;
e=t->data;
free(t);
return true;
}
4.队列的应用举例
1.求解报数问题
1)问题描述
设有n个人站成一排,从左向右的编号分别为1~n ,现在从左往右报数“1,2,1,2,…”数到“1”的人出列,数到“2”的立即站到队伍的最右端。报数过程反复进行,直到n个人都出列为止。要求给出他们的出列顺序。
例如,当n=8时初始序列为:12345678,则出列顺序为:13572648
2)数据组织
用一个队列解决出列问题,由于这里不需要使用已经出队后的元素,所以采用环形队列。
3)设计运算算法
采用的算法思想是先将n个人的编号进队,然后反复执行以下操作,直到队列为空。
(1)出队一个元素,输出其编号(报数为1的人出列)。
(2)若队列不空,再出队一个元素,并将刚出列的元素进队(报数为2的人站到队伍的最右端,即队尾)。
void number(int n){
int i;
ElemType e;
SqQueue * q; //环形队列指针q
InitQueue(q); //初始化队列q
for (i=1;i<=n;i++) //构建初始序列
enQueue(q, i);
printf("报数出列顺序:");
while (!QueueEmpty(q)){ //队列不空时循环
deQueue(q, e); //出队一个元素
printf(" %d ", e); //输出元素的编号
if (!QueueEmpty(q)){ //队列不空
deQueue(q,e); //出队一个元素
enQueue(q,e); //将刚出队的元素进队
}
}
printf("\n");
DestroyQueue(q); //销毁队列q
}
4)设计求解程序
int main(){
int i, n=8;
printf("初始序列: ");
for (i=1;i<=n;i++)
printf(" %d", i);
printf("\n");
number(n);
return 1;
}
5)运行结果
初始序列:1 2 3 4 5 6 7 8
报数出列序列:1 3 5 7 2 6 4 8
2.求解迷宫问题
1)问题描述
同栈的该问题的描述
2)数据组织
typedef struct{
int i,j; //方块的位置
int pre; //本路径中上一个方块在队列中的下标
}Box; //方块类型
typedef struct{
Box data[MaxSize];
int front,rear; //队头指针和队尾指针
}QuType; //顺序队类型
将入口(xi,yi)的 pre置为-1并进队;
mg[xi][yi]=-1;
while (队列qu不空){
出队一个方块e,其在队列中的位置是front;
if(方块e是出口){
输出一条迷宫路径;
return true;
}
for(对于方块e的所有相邻可走方块e1){
设置e1的pre为 front;
将方块e1进队;
将方块e1的迷宫数组值设置为一1;
}
}
return false; //没有迷宫路径,返回假
bool mgpath1(int xi, int yi, int xe, int ye){ //搜索路径为(xi,yi)->(xe, ye)
Box e;
int i,j,di,i1,j1;
QuType * qu; //定义顺序队指针qu
InitQueue(qu); //初始化队列 qu
e.i= xi;
e.j= yi;
e.pre= -1;
enQueue(qu, e); //(xi, yi)进队
mg[xi][yi]=-1; //将其赋值-1,以避免回过来重复搜索
while (!QueueEmpty(qu)){ //队不空时循环
deQueue(qu,e); //出队方块e,非环形队列中元素e仍在队列中
i=e.i;
j=e.j;
if (i==xe && j==ye){ //找到了出口,输出路径
dispapath(qu,qu -> front); //调用 dispapath函数输出路径
DestroyQueue(qu); //销毁队列
return true; //找到一条路径时返回真
}
for (di=0;di< 4;di++){ //循环遍历每个方位,把每个相邻可走的方块进队
switch(di){
case 0:i1=i-1;j1=j; break;
case 1:i1=i;j1=j+1; break;
case 2:i1=i+1;j1=j; break;
case 3:i1=i;j1=j-1; break;
}
if (mg[i1][j1]==0){
e.i=i1;
e.j-j1;
e.pre=qu-> front; //指向路径中上一个方块的下标
enQueue(qu, e); //(i1,j1)方块进队
mg[i1][j1]=一l; //将其赋值一1,以避免回过来重复搜索
}
}
}
DestroyQueue(qu); //销毁队列
return false; //未找到任何路径时返回假
}
void dispapath(QuType * qu, int front){ //从队列qu中找到一条迷宫路径并输出
Box path[MaxSize];
int p=front, k=0,i;
while(p!=-1){ //搜索反向路径path[o. .k一1]
path[k++]=qu->data[p];
p=qu->data[p].pre;
}
printf("一条迷宫路径如下:\n");
for(i=k-1;i>=0;i--){ //反向输出path构成正向路径
printf("\t(%d,%d)" , path[i].i, path[i].j);
if((k-i)%5==0) printf("\n"); //每输出5个方块后换一行
}
printf("\n" );
}
4)设计求解程序
int main(){
if (!mgpath1(1,1,M,N))
printf("该迷宫问题没有解!");
return 1;
}
5)运行结果
一条迷宫路径如下:
(1,1) (2,1) (3,1) (4,1) (5,1)
(5,2) (5,3) (6,3) (6,4) (6,5)
(7,5) (8,5) (8,6) (8,7) (8,8)
5.双端队列
双端队列是指两端都可以进行进队和出队操作的队列。
将队列的两端分别称为前端和后端,两端都可以进队和出队。其元素的逻辑关系仍是线性关系。
在双端队列中进队时,从前端进的元素排列在队列中从后端进的元素的前面,从后端进的元素排列在队列中从前端进的元素的后面。在双端队列中出队时,无论是从前端出还是从后端出,先出的元素都排列在后出的元素的前面。
实际上,从前面的双端队列可以看出,从后端进前端出或者从前端进后端出体现先进先出的特点,从前端进前端出或从后端进后端出体现出后进先出的特点。
在实际使用中,还可以有输出受限的双端队列(即允许两端进队,但只允许一端出队的双端队列)和输入受限的双端队列(即允许两端出队,但只允许一端进队的双端队列)。如果限定双端队列中从某端进队的元素只能从该端出队,则该双端队列就蜕变为两个栈底相邻的栈了。