4 栈与队列
4.1什么是栈?
栈( stack),是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底( bottom),不含任何数据元素的栈称为空栈。栈又称为==先进后出==(First In Last Out)的线性表.
栈的插入操作,我们称为入栈,也叫压栈。
栈的删除操作,我们称为出栈,也叫弹栈。
栈的图像。
4.2进栈出栈的变化形式
我们知道,栈是先进后出的结构,那么对于最先入栈的元素是不是一定只能最后出栈的?
不是的,栈只是规定了“先进后出”的结构,也就是对于元素的插入删除进行了限制。而对于元素什么时候入栈出栈没有限制。
举个例子,1,2,3依次进栈,会有哪些输出次序呢,或者说是出栈次序呢?
- 第一种:1、2、3进,再3、2、1出,这是最好理解也是最能想到的一种。出栈次序为3 2 1。
- 第二种:1进、1出,2进、2出,3进、3出。进一个出一个,所以出栈次序为1 2 3。
- 第三种:1进、2进、2出,1出,3进、3出。出栈次序为2 1 3。
- 第四种:1进、1出,2进、3进、3出,2出。出栈次序为1 3 2。
- 第五种:1进,2进,2出,3进,3出,1出。出栈次序为2 3 1。
有没有可能是3 1 2的出栈次序呢,当然不可能。想不明白就画图。
关于进栈出栈的变化形式,一定要搞清楚,考点。
4.3栈的顺序存储结构及相关操作
栈的结构是什么呢?
top游标和数据域
我们规定,栈为空时,top = -1.(当然你也可以规定为0);
此时,top始终指向的是栈顶元素,时刻清楚top指向的是哪个位置,就会很好的操作栈。
栈的本质是一个线性表。而线性表又分为顺序存储和链式存储,那么对于顺序存储,我们依旧用数组来实现。
4.3.1顺序栈的定义
#define maxsize 20
//顺序栈的定义
typedef struct
{
int data[maxsize];//最大元素个数
int top;//top游标
}Stack;
4.3.2顺序栈初始化
//初始化栈
void InitStack(Stack *A)
{
A->top = -1;//让游标指向-1,意为空栈
}
4.3.3入栈操作
//入栈操作
void PushStack(Stack *A,int e)
{
A->top++;//top要先进行++操作,因为top指向的是栈顶元素的位置,插入要插入到栈顶
A->data[A->top] = e;
}
4.3.4出栈操作
//出栈
int PopStack(Stack *A,int e)
{
e = A->data[A->top];//先赋值
A->top--;//top后--
return e;
}
4.3.5栈的输出操作(遍历)
//输出栈
void PrintStack(Stack *A)
{
printf("栈中元素为:\n");
//利用top游标进行遍历
for(int i = 0;i<=A->top;i++)
{
printf("%d ",A->data[i]);
}
}
4.3.6测试样例
int main()
{
Stack A;//定义一个栈
InitStack(&A);//初始化
//对栈进行赋值操作
int n;
printf("请输入元素个数:");
scanf("%d",&n);
for(int i = 0;i<n;i++)
{
printf("请输入第%d个元素",i+1);
int e;
scanf("%d",&e);
PushStack(&A,e);
}
//出栈
int num;
num = PopStack(&A,num);
printf("出栈的元素为:%d\n",num);
//输出栈的元素
PrintStack(&A);
}
4.4两栈共享空间
什么是两栈共享空间呢?
一个空间,给两个栈使用。
为什么?
提高空间的利用率。
4.4.1实现思路
用两个游标,top1表示栈1,top2表示栈2.
栈为空:top1 = -1; top2 = maxsize;
对于栈满怎么理解呢,极端情况:栈1满而栈2空,也就是top1 = maxsize -1;而top2 = maxsize;而栈1为空,栈2的top2=0时,栈满。两个top指针相差1的时候,为栈满。
栈满:top1 + 1 = top2;
4.4.2共享栈的相关操作
基本操作和顺序栈一样,只不过是多了一个top2的游标,同样,入栈出栈时,也要判断是入和出哪一个栈。
4.4.3共享栈的定义
#define maxsize 20
typedef struct
{
int data[maxsize];
int top1;//栈1的top指针
int top2;//栈2的top指针
}Stack;
4.4.4入栈
//需要入栈的数据num,入哪个栈i
void PushStack(Stack *S,int num,int i)
{
if(S->top1 + 1==S->top2)
{
return 0;
}
//往第一个栈中存放数据
if(i == 1)
{
S->top1++;
S->data[S->top1] = num;
}
//往第二个栈中存放数据
if(i == 2)
{
S->top2--;//注意是--
S->data[S->top2] = num;
}
}
4.4.5出栈
int PopStack(Stack *S,int num,int i)
{
if(i == 1)//第一个栈出数据
{
if(S->top1 == -1)
return ;
num = S->data[S->top1];
S->top1--;
return num;
}
if(i == 2)//第二个栈中出数据
{
if(S->top1 == maxsize)
return ;
num = S->data[S->top2];
S->top2++; //注意是++
return num;
}
}
事实上,使用这样的数据结构,通常都是当两个栈的空间需求有相反关系时,**也就是一个栈增长时另一个栈在缩短的情况。**就像买卖股票一样,你买入时,一定是有一个你不知道的人在做卖出操作。有人赚钱,就一定是有人赔钱。这样使用两栈共享空间存储方法才有比较大的意义。否则两个栈都在不停地增长,那很快就会因栈满而溢出了。
4.5栈的链式存储结构以及实现
链栈,和链表一样折磨人。
链表是横着的,那么把他竖起来,就是一个链栈。链栈和链表的操作唯一不同的就是,元素的插入与删除。
链栈是没有头结点的,因为没有必要,那么对于头指针呢?
==空栈:==top =NULL;
4.5.1链栈的定义
//链栈结构
typedef struct
{
int data;//数据域
struct Node *next;//指针域
}Node;
4.5.2链栈初始化
//初始化栈
void InitStack(Node *A)
{
A->next = NULL;
}
4.5.3入栈
void PushStack(Node *A,int e)
{
Node *p = (Node*)malloc(sizeof(Node));
p->data = e;
p->next = A->next;//p的数据作为第一项
A->next = p;//头指针指向p,实现类似top的移动
}
4.5.4出栈
int PopStack(Node *A,int e)
{
if(A->next == NULL)
{
return false;
}
Node *p = A->next;//保存链栈中的第一个结点
A->next = p->next;//头指针指向第二个结点,实现top的移动
int e = p->data; //赋值操作
free(p);//释放内存空间
return e;
}
4.5.5输出栈
void PrintStack(Node *A)
{
Node *p = A->next;
printf("栈中的元素为:\n");
while(p)
{
printf("%d ",p->data);
p = p->next;
}
printf("\n");
}
4.5.6测试样例
int main()
{
Node A;
InitStack(&A);
int n;
printf("请输入元素的个数:");
scanf("%d",&n);
int arr[n];
for(int i = 0;i<n;i++)
{
printf("请输入第%d个元素:",i+1);
scanf("%d",&arr[i]);
PushStack(&A,arr[i]);
}
PrintStack(&A);
int e = PopStack(&A);
printf("出栈的元素为:%d\n",e);
PrintStack(&A);
}
对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。
对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。
4.6栈的作用
有的同学可能会觉得,用数组或链表直接实现功能不就行了吗?干吗要引入栈这样的数据结构呢?这个问题问得好。
其实这和我们明明有两只脚可以走路,干吗还要乘汽车、火车、飞机一样。理论上,陆地上的任何地方,你都是可以靠双脚走到的,可那需要多少时间和精力呢?我们更关注的是到达而不是如何去的过程。
栈的引入简化了程序设计的问题,划分了不同关注层次,使得思考范围缩小,更加聚焦于我们要解决的问题核心。反之,像数组等,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。
所以现在的许多高级语言,比如 Java、C#等都有对栈结构的封装,你可以不用关注它的实现细节,就可以直接使用Stack的 push和pop方法,非常方便。
4.7栈的应用----递归
什么是递归呢?
在运行的时候自己调用自己
《大学》:“古之欲明明德于天下者,先治其国;欲治其国者,先齐其家;欲齐其家者,先修其身;欲修其身者,先正其心;欲正其心者,先诚其意;欲诚其意者,先致其知,致知在格物。物格而后知至,知至而后意诚,意诚而后心正,心正而后身修,身修而后家齐,家齐而后国治,国治而后天下平。”
在高级语言中,调用自己和其他函数并没有本质的不同。我们把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。
递归的结构:
每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。
输出斐波那契数列的前n项
对于斐波那契数列:1,1,2,3,5,8…
F
(
n
)
=
{
0
,
当
n
=
0
1
,
当
n
=
1
F
(
n
−
1
)
+
F
(
n
−
2
)
,
当
n
>
1
F(n)=\left\{\begin{array}{l} 0, \text { 当 } n=0 \\ 1, \ {当 } n=1 \\ F(n-1)+F(n-2), \text { 当 } n>1 \end{array}\right.
F(n)=⎩
⎨
⎧0, 当 n=01, 当n=1F(n−1)+F(n−2), 当 n>1
//常规算法
int a[100];
a[0] = 0;
a[1] = 1;
for(int i = 2;i<n;i++)
{
a[i] = a[i-1] + a[i-2];//动态规格的思想,DP
printf("%d ",a[i]);
}
//递归操作
int Fib(int i)
{
if(i == 1)
{
return 0;
}
if(i == 2)
{
return 1;
}
return Fib(i-1) + Fib(i-2);
}
int main()
{
for(int i = 0;i<n;i++)
{
printf("%d ",Fib(i));
}
}
递归是如何用到栈了呢?
调用过程以及最后的输出。
输出前5项时,运行的过程:
前面我们已经看到递归是如何执行它的前行和退回阶段的。==递归过程退回的顺序是它前行顺序的逆序。==在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。
就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。
4.8栈的应用----四则运算表达式求值
在我们第一次接触到编程语言的时候,我们都像这样敲过最简单和最基础的代码:
int a = 2;
int b = 3;
int c = 4;
a = b + c;
或者小时候借助计算器算题,但是有一个共同点是:没有办法实现涉及小括号的计算,或者说,涉及到运算符优先级的一个计算。例如:9+(3-1)x3+10÷2。
这里面的困难就在于乘除在加减的后面,却要先运算,而加入了括号后,就变得更加复杂。不知道该如何处理。
但仔细观察后发现,括号都是成对出现的,**有左括号就一定会有右括号,对于多重括号,最终也是完全嵌套匹配的。**这用栈结构正好合适,只有碰到左括号,就将此左括号进栈,不管表达式有多少重括号,反正遇到左括号就进栈,而后面出现右括号时,就让栈顶的左括号出栈,期间让数字运算,这样,最终有括号的表达式从左到右巡查一遍,栈应该是由空到有元素,最终再因全部匹配成功后成为空栈的结果。
但是,对于四则运算,先乘除加减的问题依然很复杂,如何有效的处理呢?伟大的科学家想到了办法。
4.8.1后缀(逆波兰)表示法定义
波兰逻辑学家,想到了一种==不需要括号的后缀表示法,所有的符号都在运算数字的后面出现,==我们称之为后缀表达式或者是逆波兰式。
与之相对应的,我们平常所写的,1 + 1 = 2这种的,==运算符都在数字之间,称为中缀表达式。==当然,出现在运算数字之前的成为前缀表达式。
例如,9+(3-1)x3+10÷2 = 9 3 1 - 3 * + 10 2 / +
4.8.2中缀表达式与后缀表达式的转换
借助树
中缀表达式构建二叉树
- 计算数作为叶子节点,运算符作为中间节点。
- 对算式按照优先级和计算顺序分割,计算数为叶子节点,运算符为中间节点,直到算式对应到二叉树中。
9+(3-1)x3+10÷2
9 + (3-1)x3+10÷2 -> (3 - 1) x 3+10÷2 -> 3 + 10÷2 -> 10 ÷ 2
如何转为后缀呢?
借助树的后序遍历。
所谓的前序、中序、后序遍历,说的都是根节点的前中后的位置,==后序遍历:左、右、根。==左子树,右子树,根节点。
所以对于一棵树,我们对其进行后序遍历,就可以得到他的后缀表达式,后序遍历的结果:9 3 1 - + 3 10 2 ÷ + *,我们可以看出来,中序和后序是不唯一的
4.8.3后缀表达式实现四则运算
1.直接进行后缀表达式的运算
9 3 1 - 3 * + 10 2 / +
**规则:**从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
①初始化一个空栈。
②后缀表达式前三个都是数字,所以都进栈。
③接下来是“-”,所以将栈中的1出栈作为减数,3出栈作为被减数,并运算3-1,得到2,再将2进栈,如左图所示。
④接着是数字3进栈,如右图所示。
⑤后面是“*”,也就意味着栈中3和⒉出栈,2与3相乘,得到6,并将6进栈,如左图所示。
⑥下面是“+”,所以栈中6和9出栈,9与6相加,得到15,将15进栈,如图右图所示。
⑦接着是10与2两数字进栈,如左图所示。
⑧接下来是符号“/”,因此,栈顶的2与10出栈,10与2相除,得到5,将5进栈,如右图所示。
⑨最后一个是符号“+”,所以15与5出栈并相加,得到20,将20进栈,如左图所示。
⑩结果是20出栈,栈变为空,如右图所示。
2.中缀转为后缀然后运算
我们之前所介绍的是利用二叉树来进行相互转换,那么在实际编程中,怎样利用栈实现中缀表达式转为后缀呢?
**规则:**从左到右遍历中缀表达式的每个数字和符号,若是数字就输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,是右括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈,一直到最终输出后缀表达式为止。
①初始化一个空栈
②第一个字符是数字9,输出9,后面是符号“+”,进栈。
③第三个字符是“(”,依然是符号,因其只是左括号,还未配对,故进栈。如左图
④第四个字符是数字3,输出,总表达式为9 3,接着是“一”,进栈。如右图
⑤接下来是数字1,输出,总表达式为9 3 1,后面是符号“)”,此时,我们需要去匹配此前的“(”,所以栈顶依次出栈,并输出,直到“(”出栈为止。此时左括号上方只有“ - ”,因此输出“ - ”。总的输出表达式为9 3 1 - 。如左图所示。
⑥接着是数字3,输出,总的表达式为9 3 1- 3。紧接着是符号“×”,因为此时的栈顶符号为“+”号,优先级低于“×”,因此不输出,“*”进栈。如右图。
⑦之后是符号“+”,此时当前栈顶元素“ * ”比这个“+”的优先级高,因此栈中元素出栈并输出(没有比“+”号更低的优先级,所以全部出栈),总输出表达式为93 1-3*+。然后将当前这个符号“+”进栈。
⑧紧接着数字10,输出,总表达式变为9 3 1-3 * +10。后是符号“÷”,所以“/”进栈。如右图所示。
⑨最后一个数字2,输出,总的表达式为9 3 1 - 3 * +10 2。如左图所示。
⑩因已经到最后,所以将栈中符号全部出栈并输出。最终输出的后缀表达式结果为9 3 1-3*+10 2/+。如右图所示。
4.8.4代码实现
1.后缀表达式的直接运算
遇到是数字就进栈,遇到是符号,就将处于栈顶两个数字出栈,进行运算,运算结果进栈,一直到最终获得结果。
#include <stdio.h>
#define maxsize 100
typedef struct
{
int data[maxsize];
int top;
}Stack;
void InitStack(Stack *S)
{
S->top = -1;
}
//入栈操作
void PushStack(Stack *A,int e)
{
A->top++;
A->data[A->top] = e;
}
//出栈
int PopStack(Stack *A)
{
int e = A->data[A->top];
A->top--;
return e;
}
int main(int argc, char const *argv[])
{
Stack S;
InitStack(&S);
//后缀表达式的计算
char str[maxsize];
printf("请输入后缀表达式:");
//scanf("%s",str);
gets(str);
for(int i = 0;str[i] != '\0';i++)
{
//处理数字
if (str[i] >= '0' && str[i] <= '9')
{
PushStack(&S,str[i] - '0');//数字进栈
}
//遇到运算符出栈运算然后再进栈
else
{
int a = PopStack(&S);
int b = PopStack(&S);
switch (str[i])
{
case '+':
PushStack(&S,b + a);
break;
case '-':
PushStack(&S,b - a);
break;
case '*':
PushStack(&S,b * a);
break;
case '/':
PushStack(&S,b / a);
break;
default:
break;
}
}
}
//遍历完后输出
printf("%d\n",PopStack(&S));
return 0;
}
2.边转边运算(中缀转后缀后运算)
若是数字则存入数字栈,若是符号则判断优先级后存入符号栈,括号或优先级低于栈顶符号(乘除优先加减)则栈顶元素依次出栈并输出,并将当前符号进栈。
**关于括号的处理:**左括号进栈,若遇到右括号,则将左括号即之前的所有元素出栈,右括号不进栈,起信号作用。
#include <stdio.h>
#include <string.h>
#include<stdlib.h>
#define MAX 100
typedef int Num;
typedef struct
{
Num data[MAX];
int top;
}StackNum;//运算数栈
typedef struct
{
char data[MAX];
int top;
}StackChar;//运算符栈
void InitNum(StackNum *p)//初始化运算数栈
{
p->top = 0;
}
void PushNum(StackNum *p,Num e)//运算数进栈
{
if(p->top ==MAX)
{
printf("运算数栈满\n");
}
else
{
p->data[p->top] = e;
p->top++;
}
}
void PopNum(StackNum *p,Num *e)//运算数出栈
{
if(p->top == 0)
{
printf("运算栈空\n");
}
else
{
p->top--;
*e = p->data[p->top];
}
}
void InitChar(StackChar *p)//初始化运算符栈
{
p->top = 0;
}
void PushChar(StackChar *p,char e)//运算符进栈
{
if (p->top == MAX)
printf("运算符栈满\n");
else
{
p->data[p->top] = e;
p->top++;
}
}
void PopChar(StackChar *p,char *e)//运算符出栈
{
if(p->top == 0)
{
printf("运算栈空\n");
}
else
{
p->top--;
*e = p->data[p->top];
}
}
Num GetNum(StackNum p)//取栈顶元素
{
return p.data[p.top - 1];
}
void Fun(StackNum *p,char e)//计算
{
Num temp1,temp2;//存放两个临时操作数
PopNum(p,&temp2);
PopNum(p,&temp1);
switch (e)
{
case '+':PushNum(p,temp1 + temp2);
break;
case '-':PushNum(p,temp1 - temp2);
break;
case '*':PushNum(p, temp1*temp2);
break;
case '/':PushNum(p, temp1 / temp2);
break;
}
}
int main()
{
int i;//循环变量
int temp;//临时转换数
char str[MAX],ch;//存放中缀表达式原值,临时运算符
StackNum n1;//初始化运算数栈
InitNum (&n1);
StackChar c1;//初始化运算符栈
InitChar (&c1);
//for(;;)
printf("请输入中缀表达式:");
gets(str);
//数值入栈
for(i = 0;str[i] != '\0';i++)//读完整字符串,字符串结束标志'\0'
{
if(str[i] >='0'&&str[i] <='9')//读入数字
{
temp = str[i]-'0'; //字符转换为数字
while(str[i+1] != '\0')//多位数字的获取
{
if(str[i+1]>='0'&&str[i+1]<='9')
{
temp = temp*10 + str[i+1]-'0';
i++;
}
else
break;//如果不是多位数字,如是符号,则跳出
}
PushNum(&n1,temp);//将数值入栈
}
else if(str[i] == '+' || str[i] == '-' || str[i] == '*' || str[i] == '/' || str[i] == '(' || str[i] == ')')
{
switch (str[i])
{
case '+':
if (c1.data[c1.top - 1] != '+'&&c1.data[c1.top - 1] != '-'&&c1.data[c1.top - 1] != '*'&&c1.data[c1.top - 1] != '/')//与上一个优先级比较
{
PushChar(&c1, '+');
}
else//如果不然,即,为小括号,则将之前的先都出栈并计算,然后再入栈
{
while (c1.top > 0 && c1.data[c1.top - 1] != '(')//将优先级高的运算符先输出计算,其中括号内的优先级最高
{
PopChar(&c1, &ch);
Fun(&n1, ch);//计算,并压运算数栈
}
PushChar(&c1,'+');
}
break;
case '-':
if (c1.data[c1.top - 1] != '+'&&c1.data[c1.top - 1] != '-'&&c1.data[c1.top - 1] != '*'&&c1.data[c1.top - 1] != '/')
{
PushChar(&c1, '-');
}
else//如果不然,则将之前的先都出栈并计算,然后再入栈
{
while (c1.top > 0 && c1.data[c1.top - 1] != '(')//将优先级高的运算符先输出计算,其中括号内的优先级最高
{
PopChar(&c1, &ch);
Fun(&n1, ch);//计算,并压运算数栈
}
PushChar(&c1, '-');
}
break;
case '*':
if (c1.data[c1.top - 1] != '*'&&c1.data[c1.top - 1] != '/')
{
PushChar(&c1, '*');
}
else//如果不然,则将之前的先都出栈并计算,然后再入栈
{
while (c1.top > 0 && c1.data[c1.top - 1] != '(')//将优先级高的运算符先输出计算,其中括号内的优先级最高
{
PopChar(&c1, &ch);
Fun(&n1, ch);//计算,并压运算数栈
}
PushChar(&c1, '*');
}
break;
case '/':
if (c1.data[c1.top - 1] != '*'&&c1.data[c1.top - 1] != '/')
{
PushChar(&c1, '/');
}
else//如果不然,则将之前的先都出栈并计算,然后再入栈
{
while (c1.top > 0 && c1.data[c1.top - 1] != '(')//将优先级高的运算符先输出计算,其中括号内的优先级最高
{
PopChar(&c1, &ch);
Fun(&n1, ch);//计算,并压运算数栈
}
PushChar(&c1, '/');
}
break;
case '(':
PushChar(&c1, '(');
break;
case ')'://并没有将'('压入栈中,只是当作一种出栈信号
while (c1.data[c1.top - 1] != '(')
{
PopChar(&c1, &ch);
Fun(&n1, ch);//计算,并压运算数栈
}
PopChar(&c1, &ch);//将'('也出栈,但并不计算
break;
}
}
}
while (c1.top > 0)//将剩余的运算符出栈并计算
{
PopChar(&c1, &ch);
Fun(&n1, ch);
}
printf("%s=%d", str, GetNum(n1));
printf("\n");
}
4.9队列
什么是队列呢?
顾名思义,就像日常生活中的队列一样。
队列(queue )是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种==先进先出(First In First Out)==的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
4.10循环队列
4.10.1队列顺序存储的不足
正常情况下,我们定义一个数组,把数据元素一次存储在数组里面。入队列时在尾部进行添加,时间复杂度为O(1),如下图。
而出队列时,==队列规定只能队头元素出队列。==也就是下标为0的元素,也就意味着,队列的所有元素都要向前移动,以保证队列的队头的下标始终为0,不为空,因此时间复杂度为O(n)。
显而易见,每次出队列都要移动大量的元素是很不方便的,因此,==如果不限制队头是下标为0的话,==性能会大大提高,换句话说,就是队头跟着队列中的第一个元素跑。即,队头始终指向队列中的第一个元素
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以引入两个指针,==front 指针指向队头元素,rear指针指向队尾元素的下一个位置,==这样当front等于rear 时,此队列不是还剩一个元素,而是空队列。注意,这里的指针就是字面意思上的指针。
==空队列:==front = rear;
假设是长度为5的数组,初始状态,空队列如左图所示,front与rear 指针均指向下标为0的位置。然后入队a1、a2、a3、a4,front 指针依然指向下标为0位置,而rear指针指向下标为4的位置,如右图所示。
图4-12-4
出队a1、a2,则front 指针指向下标为2的位置,rear不变,如左图所示,再入队as,此时front 指针不变,rear 指针移动到数组之外。嗯?数组之外,那将是哪里?如右图所示。
图4-12-5
可以看到,对于这种思想,极易产生数组下标越界的问题,但是,在这里数组越界并不意味着数组满了,0和1的位置还有空闲,因此,我们称之为**“假溢出”**。
现实当中,你上了公交车,发现前排有两个空座位,而后排所有座位都已经坐满,你会怎么做?立马下车,并对自己说,后面没座了,我等下一辆?那不可能。
4.10.2循环队列
如何解决上述的“假溢出”的问题呢?----我们让rear再指回去前面的0,1的位置,也就是指回到数组开头的位置,就可以很好的解决。
后面满了,就再从头开始,也就是头尾相接的循环。我们把队列的这种头尾相接的顺序存储结构称为循环队列。
对于之前的例子呢,我们用循环队列继续。根据循环队列的思想,rear指向了数组的第一个位置。
图 4-12-6
接着入队a6,将它放置于下标为0处,rear 指针指向下标为1处,如左图所示。若再入队a7,则rear指针就与front 指针重合,同时指向下标为2的位置,如右图所示。
图 4-12-7
- 此时问题又出来了,我们刚才说,空队列时,front等于rear,**现在当队列满时,也是 front 等于rear,**那么如何判断此时的队列究竟是空还是满呢?
- 办法一是设置一个标志变量flag,当front == rear,且 flag = 0时为队列空,当front == rear,且flag=1时为队列满。
- 办法二是当队列空时,条件就是front = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,==队列满时,数组中还有一个空闲单元。==例如下图所示,我们就认为此队列已经满了,也就是说,我们不允许图4-12-7的右图情况出现。注意:并不是隔一个空闲单元。
4.10.3循环队列队满及队长
我们对于方法二进行重点讨论。
在这种情况下,rear可能比front大,也可能比front小,如上图所示。因此,不能单单地使用rear + 1去和front比较来判断是否队满。
所以,若队列的最大长度为maxsize,则队满的条件为:(rear + 1)%maxsize == front;
对于上述的例子,maxsize = 5,上图左图中front=0,而rear=4,(4+1)%5=0,所以此时队列满。再比如上图的右图,front = 2而rear = 1。(1 +1) %5= 2,所以此时队列也是满的。而对于图4-12-6,front = 2而rear = 0,(0+1) %5=1,1≠2,所以此时队列并没有满。
另外,当rear > front时,即图4-12-4的右图和4-12-5的左图,此时队列的长度为rear-front。但当rear < front时,如图4-12-6和图4-12-7的左图,队列长度分为两段,一段是QueueSize-front,另一段是0 + rear,加在一起,队列长度为rear一front + QueueSize。因此通用的计算队列长度公式为:
(rear- front +maxsize)%maxsize
4.10.4代码实现
队列的定义
#define MAX 100
typedef struct
{
int data[MAX];
int front;
int rear;
}SqQueue;
队列的初始化
void InitQueue(SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
}
入队列
void EnQueu(SqQueue *Q,int e)
{
if((Q->rear + 1)%MAX == Q->front)
{
printf("队列满\n");
return ;
}
else
{
Q->data[Q->rear] = e;//赋值给队尾
Q->rear = (Q->rear + 1)%MAX;//rear指针移向后一个位置
}
}
出队列
void DeQueue(SqQueue *Q, int *e)
{
if (Q->front == Q->rear)
{
printf("队列空\n");
return ;
}
else
{
*e = Q->data[Q->front]; //队头元素赋值给e
Q->front = (Q->front + 1) % MAX;//front指针后移一个位置
}
}
判断队列是否为空
int IsEmpty(SqQueue *Q)
{
if (Q->front == Q->rear)
{
return 1;
}
else
{
return 0;
}
}
判断队列是否为满
int IsFull(SqQueue *Q)
{
if ((Q->rear + 1) % MAX == Q->front)
{
return 1;
}
else
{
return 0;
}
}
求队列的长度
int QueueLength(SqQueue *Q)
{
return (Q->rear - Q->front + MAX) % MAX;
}
输出队列
void PrintQueue(SqQueue *Q)
{
for (int i = Q->front; i != Q->rear; i++)
{
printf("%d ", Q->data[i]);
}
printf("\n");
}
测试样例
int main()
{
SqQueue Q;
InitQueue(&Q);
int n;
printf("请输入队列长度:");
scanf("%d", &n);
int arr[n];
for(int i = 0;i<n;i++)
{
printf("请输入第%d个元素:",i+1);
scanf("%d",&arr[i]);
EnQueue(&Q,arr[i]);
}
PrintQueue(&Q);
DeQueue(&Q,&n);
PrintQueue(&Q);
}
4.11队列的链式存储结构及实现
所谓的队列的链式存储,就是跟单链表是一样的,只不过是加入了队列的相关概念:删除只能头删,插入只能尾插,加入了front和rear的指针。
对于空队列
1.带头结点的链式队列
链式队列的定义
typedef struct
{
int data;
struct QNode *next;
}QNode;
typedef struct
{
QNode *front;//头指针
QNode *rear;//尾指针
}SqQueue;
初始化队列
//初始化队列
void InitQueue(SqQueue *Q)
{
Q->front = (QNode*)malloc(sizeof(QNode));//头指针始终指向了一块空间,这块空间就起到了投结点的作用,始终不变
Q->rear = Q->front;
Q->front->next = NULL;
}
入队列
void EnQueue(SqQueue *Q,int e)
{
QNode *p;
p = (QNode *)malloc(sizeof(QNode));
p->data = e;
p->next = NULL; //新节点初始化
Q->rear->next = p; //将新节点纳入链队列
Q->rear = p;
}
出队列
void DeQueue(SqQueue *Q,int *e)
{
if(Q->front == Q->rear)//判断为空队列
{
return ;
}
QNode *p;
p = Q->front->next;//p指向头结点的下一个元素,也就是队列中的第一个元素
*e = p->data;
Q->front->next = p->next;
if(Q->rear == p)//空队列,下图中三的情况
{
Q->rear = Q->front;
}
free(p);
}
输出队列
void PrintQueue(SqQueue *Q)
{
QNode *p;
p = Q->front->next;
while(p != NULL)
{
printf("%d ",p->data);
p = p->next;
}
printf("\n");
}
测试样例
int main()
{
SqQueue Q;
InitQueue(&Q);
int e;
EnQueue(&Q,1);
EnQueue(&Q,2);
EnQueue(&Q,3);
EnQueue(&Q,4);
EnQueue(&Q,5);
PrintQueue(&Q);
DeQueue(&Q,&e);
printf("%d\n",e);
PrintQueue(&Q);
return 0;
}
2.不带头结点的链式队列
初始化队列
void InitQueue(SqQueue *Q)
{
Q->front = NULL;
Q->rear = NULL;
}
入队列
void EnQueue(SqQueue *Q,int e)
{
QNode *p;
p = (QNode *)malloc(sizeof(QNode));
p->data = e;
p->next = NULL; //新节点初始化
if(Q->front == NULL) //没有头结点,就没有在此处,Q的front直接指向了第一个元素结点
{
Q->front = p;
Q->rear = p;
}
else
{
Q->rear->next = p;
Q->rear = p;
}
}
出队列
void DeQueue(SqQueue *Q,int *e)
{
if(Q->front == NULL)//判断空
{
return ;
}
QNode *p;
p = Q->front; //直接指向Q的front即可,因为Q的front指向的就是第一个元素结点
*e = p->data;
Q->front = p->next;
if(Q->rear == p)
{
Q->rear = NULL;
}
free(p);
}
输出队列
void PrintQueue(SqQueue *Q)
{
QNode *p = Q->front;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
对于循环队列与链队列的比较,可以从两方面来考虑,从时间上,其实它们的基本操作都是常数时间,即都为O(1)的,不过**循环队列是事先申请好空间,使用期间不释放,**而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。
4.12总结回顾
栈:先进后出,限定仅在表尾进行插入和删除操作的线性表。
注意进栈出栈的变化形式。 共享栈
**应用:**递归、后缀表达式。
队列:先进先出,是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
注意队列头尾指针的缺点,因此引入了循环队列。解决了时间消耗,插入和删除由O(n)变为了O(1).
同样注意链栈的有无头结点的思想。