目录
注
本笔记参考:《数据结构(C语言版)》
栈和队列是两种重要且存在限定性的数据结构。之所以说存在限定性,是因为栈和队列的基本操作是线性表存在的子集,它们的操作是受到限制的。
栈和队列的定义和特点
栈的定义和特点
||| 栈——先进后出(Last In Firtst out,LIFO)
栈(stack)是限定仅在表尾进行插入或删除操作的线性表。不含元素的栈被称为空栈。
利用栈,可以按照保存数据时相反的顺序使用数据。
栈可以在类似于 数制的转换 这类问题中被使用:在输入时我们需要将每次计算得到的数据压入栈内,在计算结束后再依次弹出栈中的数据,从而得到数制转换的结果。(除此之外还有:“期待的急迫程度”,表达式求值算法等)
队列的定义和特点
||| 队列——先进先出(First In First Out,FIFO)
队列只允许在表的一端进行插入,而在另一端删除数据。
队列最经典的例子就是操作系统中的作业排队,凡是申请输出的作业都从队尾进入队列,传输完毕后,队头的作业先从队列中退出输出操作。
不论是栈还是队列,它们最基本的操作都是“入”和“出”。类似于线性表,栈和队列的存储结构也包含顺序和链式两种。
栈的表示和实现
栈的类型定义
栈的基本操作包括:
- 入栈
- 出栈
- 栈的初始化
- 栈空的判定
- 取栈顶元素等
- ……
顺序栈的表示和实现
顺序栈,顾名思义,指利用顺序存储结构实现的栈(即利用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素),顺序栈的定义如下:
#define MAXSIZE 100 //顺序栈存储空间的初始分配量
#define SElemType int
typedef struct
{
SElemType* base; //栈底指针
SElemType* top; //栈顶指针
int stacksize; //栈可用的最大容量
}SqStack;
其中:
1. 初始化
目的:为顺序栈动态分配一个预定义大小的数组空间。
Status InitStack(SqStack& S) //暂定 Status 的类型为 bool
{//构造一个空栈S
S.base = new SElemType[MAXSIZE]; //为顺序栈动态分配一个最大容量为MAXSIZE的数组空间
if (!S.base)
exit(OVERFLOW); //存储分配失败(ps:exit()的使用需要调用头文件iostream)
S.top = S.base; //top初始为base,空栈
S.stacksize = MAXSIZE; //stacksize置为最大容量MAXSIZE
return true;
}
2. 入栈
目的:在栈顶插入一个新的元素。
Status Push(SqStack& S, SElemType e)
{//插入元素e为新的栈顶元素
if (S.top - S.base == S.stacksize) //栈满
return false;
*S.top++ = e; //元素e压入栈顶,栈顶指针加1
return true;
}
3. 出栈
目的:将栈顶元素删除。(会改变栈顶指针,与下面的取出栈顶元素相区分)
Status Pop(SqStack& S, SElemType& e)
{//删除S的栈顶元素,用e返回其值
if (S.top == S.base) //栈空
return false;
e = *--S.top; //栈顶指针减1,将栈顶元素赋给e
return true;
}
【分析】
[ e = *--S.top; ]:在这条语句中,= 的优先级低于 * 和 -- ,而 * 和 -- 的优先级是一致的,此时观察结合性,发现结合性是R-L(即从右向左),也就是说,这三个操作符的执行顺序是:
- --:将栈顶指针减1,找到栈顶元素;
- * :解引用栈顶元素的地址,找到栈顶元素的数据;
- = :将该数据赋值给 e 。
4. 取栈顶元素
目的:当栈非空时,返回当前栈顶元素的值,栈顶指针保持不变。
SElemType GetTop(SqStack S) //此处没有使用 &操作符 ,因为不要求修改指针
{//返回S的栈顶元素,不修改栈顶指针
if (S.top != S.base) //栈非空
return *(S.top - 1); //返回栈顶元素的值,栈顶指针不变
}
5. 删除栈
目的:返回栈申请的空间,置空两个指针。
Status DestoryStack(SqStack& S)
{//返回空间,置空栈顶、栈底指针
delete[] S.base; //返回空间
S.base = S.top = NULL;
return true;
}
类似于顺序表,顺序栈也会受到最大空间容量的限制,尽管可以用再分配空间扩大容量,但不推荐。而弥补这一缺点的,就是接下来的链栈。
链栈的表示和实现
链栈——采用链式存储结构实现的栈,通常用单链表表示。
定义如下:
#define ElemType int //自定义SElemType的类型
typedef struct StackNode
{
ElemType data;
struct StackNode* next;
}StackNode, *LinkStack;
此处不用像一些单链表一样附加一个头结点,原因:栈的主要操作是在栈顶插入和删除,以链表的头部作为栈顶是最方便的。
1. 初始化
目的:构造一个空栈,将栈顶指针置为空(没有头结点)。
Status InitStack(LinkStack& S) //同上,将Status的类型暂定为bool类型
{//构造一个空栈S,栈顶指针置空
S = NULL;
return true;
}
2. 入栈
目的:为入栈元素动态分配一个结点空间(不需要判断栈是否满)。
Status Push(LinkStack& S, ElemType e)
{//在栈顶插入元素e
LinkStack p = new StackNode; //生成新的结点
p->data = e; //在新的数据域中存储数据e
p->next = S; //将新结点插入栈顶
S = p; //修改栈顶指针S为p
return true;
}
【分析】
3. 出栈
目的:删除栈顶元素。
不同点:
- 和顺序栈一样,需要判断栈是否为空;
- 链栈在出栈之后还要释放出栈元素的栈顶空间。
Status Pop(LinkStack& S, ElemType& e)
{//删除S的栈顶元素,用e返回其值
if (S == NULL) //栈空
return false;
e = S->data; //将栈顶元素赋给e
LinkStack p = S; //临时保存元素空间地址,以备释放
S = S->next; //修改栈顶指针
delete p; //释放原本栈顶元素的空间
return true;
}
【分析】
4. 取栈顶元素
目的:返回当前栈顶元素的值,栈顶指针S保存不变。
ElemType GetTop(LinkStack S)
{//返回S的栈顶元素,不修改栈顶指针
if (S != NULL) //栈非空
return S->data; //返回栈顶元素的值,栈顶指针不变
}
5. 删除栈
目的: 删除每一个元素所在空间,置空指针。
Status DestoryStack(LinkStack& S)
{//删除栈
if (!S) //栈空
return false;
while (S) //当栈内还有元素时
{
LinkStack p = S;
S = S->next; //寻找下一个元素
delete p; //删除空间
}
S = NULL;
return true;
}
补充结构:双栈的表示和实现
双栈,即将两个栈存放在同一个数组空间内部,其栈底分别位于该数组的两端,如图:
对于上述结构,我们这样约定:
- 当 top[0] == -1 时,认为0号栈为空;
- 当 top[1] == m 时,认为1号栈为空。
双栈数据结构的定义如下:
#define SElemType int //自定义SElemType的类型
typedef struct
{
int top[2], bot[2]; //栈顶和栈底指针
SElemType* V; //栈数组
int m; //栈最大可容纳元素个数
}DblStack;
1. 初始化
Status InitStack(DblStack& S)
{
using namespace std;
S.m = MAXSSIZE;
S.V = new SElemType[MAXSSIZE]; //为队列分配一个最大容量为MAXQSIZE的数组空间
if (!S.V) //如果存储分配失败
exit(OVERFLOW);
S.bot[0] = S.top[0] = -1;
S.bot[1] = S.top[1] = MAXSSIZE;
return true;
}
2. 入栈
使用 div 区分两个栈。
Status Push(DblStack& S, SElemType e, int div)
{
if (S.top[0] + 1 == S.top[1])
return false;
if (div) //div为1
S.top[div]--;
else //div为0
S.top[div]++;
S.V[S.top[div]] = e; //赋值操作
return true;
}
3. 出栈
Status Pop(DblStack& S, SElemType& e, int div)
{
if (div) //div是1
{
if (S.top[div] == MAXSSIZE)
return false;
e = S.V[S.top[div]]; //赋值
S.top[div]++; //移动指针
}
else //div是0
{
if (S.top[div] == -1)
return false;
e = S.V[S.top[div]];
S.top[div]--;
}
return true;
}
4. 取栈顶元素
SElemType GetHead(DblStack& S, int div)
{
if (!(S.top[div] == S.bot[div])) //栈非空
return S.V[S.top[div]];
}
5. 删除栈
Status DestoryStack(DblStack& S)
{
delete[] S.V;
S.bot[0] = S.top[0]
= S.bot[1] = S.top[1]
= S.m = 0;
return true;
}
------
在上述代码中,没有考虑变量div超出范围的情况,这就是代码健壮性。如果有需要,可以使用if语句补足这一点。
栈与递归
递归作为算法设计中常见的手段,有时会因为其的缺点而不被推荐使用。但是依旧存在递归发挥优势的方面:
- 在面对一个大型且复杂的问题时,递归的使用可以把问题的描述和求解变得简洁且清晰。
- 当问题本身或者其所涉及的数据结构是递归定义时,递归往往更加适合;
- 递归算法的正确性容易得到验证(由系统而不是用户管理递归工作栈)。
采用递归算法解决问题
||| 若一个函数、过程或者数据结构定义的内部又直接(或间接)出现定义本身的应用,则称它们是递归的。
1. 定义是递归的
例子——数学公式及其实现
对应的函数实现:
long Fact(long n)
{
if (n == 0) //递归终止的条件
return 1;
else //递归步骤
return n * Fact(n - 1);
}
【分析】
递归求解——将复杂的问题分解为几个相对简单且解法相同或类似的子问题。
主程序调用函数Fact()的示例如下:
----------------
补充例子
对应的函数实现:
long Fib(long n)
{
if (n == 1 || n == 2) //递归终止的条件
return 1;
else
return Fib(n - 1) + Fib(n - 2); //递归步骤
}
可采用上述例子中提到的 “分治法” 的条件:
- 一个问题能够转换成一个新问题;
- 转换的问题之间不同的只能是 处理的对象 ;
- 处理的对象变化有规律;
- 问题的转换能使问题简化;
- 必须有一个明确的 递归出口/递归边界 。
“分治法” 求解的一般形式:
void p(参数表)
{
if(递归结束条件成立) //递归终止的条件
可直接求解;
else
p(较小的参数); //递归步骤
}
2. 数据结构是递归的
某些数据结构本身具有递归的特性,因此它们的操作可以通过递归进行描述。譬如:
typedef struct LNode
{
ElemType data;
struct LNode* next;
}LNode, * LinkList;
上述是一个常见的链表定义,其中 指针域next 是指向LNode类型的指针,即LNode的定义用到了自身,因此链表就是一种递归的数据结构。
例子:遍历输出链表中各个结点
void TraverseList(LinkList p)
{
if (p == NULL) //递归终止
return;
else
{
cout << p->data << endl; //输出当前结点的数据域
TraverseList(p->next); //p指向下一个结点
}
}
上述算法递归结束时,只执行return操作,则算法的一般可写为:
void p(参数表) { if(递归结束条件不成立) p(较小的参数); }
因此上述算法可以简化为:
void TraverseList(LinkList p)
{
if(p)
{
cout << p->data << endl;
TraverseList(p->next);
}
}
3. 问题的解法是递归的
有这么一类问题,它们使用递归求解会更加简单。
例子:n解Hanoi(汉诺)塔问题
【问题描述】
假设有3个分别命名为A、B和C的塔座,在塔座A上插有n个直径大小各不相同,从小到大编号为1,2,……,n的圆盘。
现在要求将塔座A上的n个圆盘移至塔座C上,并仍然按照同样顺序叠排,圆盘的移动必须遵守下列规则:
- 每次只能移动一个圆盘;
- 圆盘可以插在A、B和C中的任一塔座上;
- 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上。
【要求】
描述圆盘的移动操作(先考虑每次移动的结果,再考虑其余的细节过程。或者说,只描述移动的结果,其余交给递归实现)。
【代码】
① 将搬动操作定义为一个函数:
int m = 0;
void move(char A, int n, char C)
{
using namespace std;
cout << ++m << ". "
<< n << "号盘: "
<< A << "→"
<< C << endl;
}
② 程序本体:
void Hanoi(int n, char A, char B, char C)
{//将塔座A上的n个圆盘按规则搬到C上,B作为辅助塔
if (n == 1) //将编号为1的圆盘从A移到C
move(A, 1, C);
else
{
Hanoi(n - 1, A, C, B); //C是辅助塔,将A上编号为1到n-1的圆盘移到B
move(A, n, C); //将编号为n的圆盘从A移到C
Hanoi(n - 1, B, A, C); //A是辅助塔,将B上编号为1到n-1的圆盘移到C
}
}
如果执行该函数,可得:
(执行结果格式有调整)
递归过程与递归工作栈
一个递归函数,在函数执行的过程中需要进行多次自我调用。调用函数和被调用函数之间的链接及信息交换需通过 栈 来进行。
通常,在函数调用另一个函数之前,系统需要完成3件事:
而从被调用函数返回调用函数时,系统也要完成3件事:
实际上,多函数嵌套调用的过程就类似于堆栈,系统会将所需的空间安排在一个栈内:
例如:
一个递归函数的运行过程就类似于多个函数的嵌套调用,不同的是,调用函数和被调用函数是同一个函数,这时候就涉及一个重要概念——递归的“层次”。
例子:递归工作栈和活动记录的使用
【代码】
void main()
{
long n; //调用 Fact(4) 时记录进栈
n = Fact(4);
}
long Fact(long n)
{
long temp;
if (n == 0)
return 1; //活动记录退栈
else
temp = n * Fact(n - 1); //活动记录进栈
return temp; //活动记录退栈
}
【分析】
当主函数调用函数Fact()时会同时传递返回地址,设该返回地址是 RetLoc_1。
类似的,设递归函数Fact()内部执行时,传递的返回地址是 RetLoc_2 。
注:此处暂时忽略局部变量temp的入栈和出栈情况。
接下来,主函数执行,依次启动5个函数调用:
递归的结束条件会出现在函数Fact(0)内部,执行Fact(0)会引起返回语句的执行:弹出栈顶的活动记录,返回地址返回到上一层Fact(1)的递归调用处(RetLoc2)。
递归算法的效率分析
1. 时间复杂度的分析
在算法分析中,当一个算法包含了递归调用时,其时间复杂度的分析可以转换成一个递归方程的求解(求渐进阶)。
常见的求解递归方程的方法是迭代法:迭代展开递归方程的右端,使之成为一个非递归的和式,通过估计这个和式来达到对方程右端(即方程的解)的估计。
以 Fact(n) 为例
设:
- Fact(n) 的执行时间是 T(n),则 Fact(n - 1) 的执行时间是 T(n - 1) ;
- 两数相乘 和 赋值操作 的执行时间为 O(1) 。
long Fact(long n)
{
long temp;
if (n == 0)
return 1; //执行时间是O(1)
else
temp = n * Fact(n - 1); //执行时间是O(1) + T(n - 1)
return temp;
}
由上述代码可得(其中C、D是常数):
故T(n)是关于n的一次函数,可得递归方程的解:T(n) = O(n) 。
补充
- Fibonacci数列递归算法的时间复杂度是O(2^n);
- Hanoi塔递归算法的时间复杂度也是O(2^n)。
2. 空间复杂度的分析
递归函数运行时,系统需要设立一个“递归工作栈”存储每一层递归所需信息,此工作栈是递归空间执行的辅助空间,因此,分析递归算法的空间复杂度时需要分析工作栈的大小。
故对于递归算法,空间复杂度为:
其中,f(n)为“递归工作栈”内 工作记录的个数 和 问题规模n 的函数关系。
补充
此前的 阶乘问题、Fibonacci数列问题 和 Hanoi塔问题 的递归算法的空间复杂度均为 O(n)。
利用栈将递归转换成非递归的方法
对于一般的递归过程,可以仿照递归算法,利用栈来消除递归过程:
这种写法结构的缺点:
- 不够清晰;
- 可读性差;
- 需要优化。
队列的表示和操作的实现
队列的类型定义
队列的操作与栈的操作类似,不同的是,删除是在表的头部(即队头)进行。
队列的抽象数据类型定义:
循环队列——队列的顺序表示和实现
队列也有两种存储表示,顺序表示和链式表示。
现在展示队列的顺序存储结构表示:
#define MAXQSIZE 100 //队列可能到达的最大长度
#define QElemType int //设置QElemType的类型
typedef struct
{
QElemType* base; //存储空间的基地址
int front; //头指针
int rear; //尾指针
}SqQueue;
约定:
- 初始化创建空队列时,front = rear = 0 ;
- 插入新的队列 尾元素 时,尾指针rear加1;
- 每当删除队列头元素时,头指针front加1。
注意(4)所处的状态:
为了解决这种“假溢出”现象,可以构建循环队列:
在循环队列下,头、尾指针的4种状态:
根据上图中的(3)和(4)状态可知:对于循环队列,不能通过头、尾指针的值来判断队列空间是否处于“满”的状态。
为了判断队列空间所处状态,可以使用两种不同的处理方法:
- 方法1 —— 少使用一个元素空间
接下来展示具体的实现操作:
1. 初始化
操作:动态分配一个预定义大小位MAXQSIZE的数组空间。
Status InitQueue(SqQueue& Q) //依旧暂定Status的类型是bool
{//创造一个空队列Q
using namespace std;
Q.base = new QElemType[MAXQSIZE]; //为队列分配一个最大容量为MAXQSIZE的数组空间
if (!Q.base) //如果存储分配失败
exit(OVERFLOW);
Q.front = Q.rear = 0; //头、尾指针置为 0 ,队列为空
return true;
}
2. 求队列长度(循环数列)
注意:对于循环数列,头、尾指针之间的差值可能是负数,为了处理这种情况,一般这样计算:差值 = (差值 + MAXQSIZE) % MAXQSIZE 。
int QueueLength(SqQueue Q)
{//返回Q的元素个数,即队列的长度
return(Q.rear - Q.front + MAXQSIZE) % MAXQSIZE;
}
3. 入队(循环数列)
操作:在队尾插入一个新元素。
Status EnQueue(SqQueue& Q, QElemType e)
{//插入元素e为Q的新的队尾元素
if ((Q.rear + 1) % MAXQSIZE == Q.front) //判断是否队满
return false;
Q.base[Q.rear] = e; //新元素插入队尾
Q.rear = (Q.rear + 1) % MAXQSIZE; //队尾指针加1
return true;
}
4. 出队(循环数列)
操作:删除表头元素。
Status DeQueue(SqQueue& Q, QElemType& e)
{//删除Q的队头元素,用e返回其值
if (Q.front == Q.rear) //队空
return false;
e = Q.base[Q.front]; //保存队头元素
Q.front = (Q.front + 1) % MAXQSIZE; //队头指针加1
return true;
}
5. 取队头元素
操作:当队列非空时,返回队头元素的值,队头指针不变。
QElemType GetHead(SqQueue Q)
{//返回队头元素,不修改队头指针
if (Q.front != Q.rear) //队列非空
return Q.base[Q.front];
}
6. 删除队列
操作:释放空间,队头、队尾指针归零。
Status DestoryQueue(SqQueue& Q)
{//删除队列
delete[] Q.base;
Q.front = Q.rear = 0;
return true;
}
- 方法2 —— 设 标志位 区别队列是“空”或者“满”
该方法和方法1的区别并不大,最大的改变就是判断队列状态的条件变为了判断标志的值。
1. 初始化
约定tag的值:
- true:队列未满;
- false:队列已满。
2. 求队列长度(循环数列)
该步与方法1无区别,可自行参考上文。
3. 入队(循环数列)
需要注意的是,此处进行了两次判断:
4. 出队(循环数列)
此处函数结尾没有判断,因为删除元素一定会有剩余空间存在。
5. 取队头元素
该步与方法1无区别,可自行参考上文。
6. 删除队列
链队——队列的链式表示和实现
链队,即采用链式存储结构实现的队列,通常也用单链表表示。不同的是,为了操作方便,会给链队添加一个头结点。队列的链式存储结构如下:
对应的代码如下:
#define QElemType int //定义QElemType的类型
typedef struct QNode
{
QElemType data;
struct QNode* next;
}QNode, *QueuePtr;
typedef struct
{
QueuePtr front; //队头指针
QueuePtr rear; //队尾指针
}LinkQueue;
因此,链队的操作实际上就是单链表插入和删除操作的特殊情况,只需要进一步修改头、尾指针。
1. 初始化
操作:构造一个只有头结点的空队。
Status InitQueue(LinkQueue& Q) //Status类型: bool
{//构造一个空队列Q
using namespace std;
Q.front = Q.rear = new QNode; //生成一个新节点作为 头结点 ,队头、队尾指针指向此结点
Q.front->next = NULL; //头结点的指针域置空
return true;
}
2. 入队
链队的入队操作不需要判断队列是否已满,取而代之的,需要尾入队元素分配一个结点空间。
Status EnQueue(LinkQueue& Q, QElemType e)
{//插入元素e作为Q的新的队尾元素
QueuePtr p = new QNode; //为入队元素分配结点
p->data = e; //将新节点的数据域置为e
p->next = NULL;
Q.rear->next = p; //将新解结点插入到队尾
Q.rear = p; //修改队尾指针
return true;
}
【分析】
3. 出队
注意:链队的出队操作的最后要释放出队元素所占的空间。
Status DeQueue(LinkQueue& Q, QElemType& e)
{//删除Q的队头元素,用e返回其值
if (Q.front == Q.rear) //如果队列为空的情况
return false;
QueuePtr p = Q.front->next; //p指向队头元素
e = p->data;
Q.front->next = p->next; //修改头结点的指针域
if (Q.rear == p) //被删除的是最后一个元素
Q.rear = Q.front; //在这种if情况下,让队尾指针指向头结点
delete p; //释放空间
return true;
}
【分析】
注意:
当队列当前的最后一个元素被删后,队尾指针也会丢失目标,需要重新对队尾指针进行赋值(即重新指向头节点)。
4. 取队头元素
QElemType GetHead(LinkQueue Q)
{//返回Q的队头元素,不修改队头指针
if (Q.front != Q.rear) //队列非空
return Q.front->next->data; //返回队头元素的值
}
5. 删除队列
Status DestoryQueue(LinkQueue& Q)
{
while (Q.front != Q.rear) //删除队尾以外的全部元素
{
QueuePtr p = Q.front;
Q.front = Q.front->next;
delete p;
}
delete Q.rear; //删除队尾
Q.rear = Q.front = NULL; //指针置空
return true;
}
链队(补充)—— 只有尾指针
该链队要求:
- 带有头结点,是循环链表;
- 只有一个尾指针指向队尾元素节点。
其的存储结构如下:
对应该结构队列的代码:
#define QElemType int //定义QElemType的类型
typedef struct QNode
{
QElemType data;
struct QNode* next;
}QNode, * QueuePtr;
typedef struct
{
QueuePtr rear; //队尾指针
}LinkQueue;
所以,该种队列的增删操作不仅需要改变尾指针,同时也要改变头结点的指针指向。
1. 初始化
此时头结点的next指针需要指向自身。
Status InitQueue(LinkQueue& Q)
{
Q.rear = new QNode;
Q.rear->next = Q.rear; //头结点指针指向自身
return true;
}
2. 入队
Status Push(LinkQueue& Q, QElemType e)
{//增加节点
QueuePtr p = new QNode;
QueuePtr q = Q.rear;
p->data = e; //赋值
p->next = Q.rear; //节点与其前一个节点进行链接
while (q->next != Q.rear) //寻找头结点
{
q = q->next;
}
q->next = p; //改变头结点的指针
Q.rear = p; //改变尾指针的指向
return true;
}
【分析】
3. 出队
Status Pop(LinkQueue& Q, QElemType& e)
{//出队,要求返回出队元素信息,并且删除当前出队节点
if (Q.rear == Q.rear->next) //要求队列内存在元素
return false;
QueuePtr p = Q.rear;
QueuePtr q = NULL;
if (p->next->next == Q.rear) //如果队列内只有一个元素
e = p->data; //赋值
else //如果队列内不止一个元素
{
while (p->next->next->next != Q.rear) //寻找队头元素
{
p = p->next;
}
e = p->next->data; //赋值
q = p;
p = p->next; //p指向队头元素
q->next = q->next->next; //改变队头元素的下一个元素的指针指向
}
delete p; //释放空间
return true;
}
【分析】
4. 取队头元素
QElemType GetHead(LinkQueue Q)
{
QueuePtr p = Q.rear;
while (p->next->next != Q.rear)
{
p = p->next;
}
return p->data;
}
5. 删除队列
Status DestoryQueue(LinkQueue& Q)
{
QueuePtr p = Q.rear;
while (p->next != Q.rear)
{
p = p->next;
}
while (Q.rear != p) //删除队尾以外的全部元素
{
QueuePtr q = Q.rear;
Q.rear = Q.rear->next;
delete q;
}
delete Q.rear; //删除队尾
Q.rear = p = NULL; //指针置空
return true;
}