顺序存储队列会出现假溢现象,因此改进成循环队列,然而循环队列也存在空间不灵活的问题(顺序结构的通病),即有时浪费存储空间(数组定义过大),而有时会空间不够(数组定义太小)。
所以出现了链式队列。
这里给出一个用单链表定义的存在头尾指针的队列。
我写的代码如下:
#ifndef CIRCLE_H
#define CIRCLE_H
typedef int ElemType;
//定义结点结构
typedef struct LinkQueueNode
{
ElemType data;
Node next;
} LinkNode,*Node;
//定义队列结构
struct LinkQueue
{
Node rear,front;
};
//队列初始化
void InitLinkQueue(LinkQueue &Q)
{
Q.rear=Q.front=(Node)malloc(sizeof(LinkNode));//建立头结点
Q.rear->next=null;
}
//判空
bool LinkQueueEmpty(LinkQueue Q)
{
if(Q.front==Q.rear)
return true;
else
return false;
}
//入队
bool EnLinkQueue(LinkQueue &Q,ElemType x)
{
Q.rear->next=(Node)malloc(sizeof(LinkNode)); //建立新结点并将地址赋值给队尾结点的next指针
if(Q.rear->next) //判断新结点成功建立
{
Q.rear=Q.rear->next; //尾指针指向新结点
Q.rear->next=null; //新结点的next指针置空
return true;
}
else //结点建立失败,输出提示信息
{
cout<<"建立结点失败!"<<endl;
return false
}
}
//出队
bool DeLinkQueue(LinkQueue &Q,ElemType &x)
{
if(LinkQueueEmpty()) //判断队列是否为空
return false;
else
{
x=Q.front->next->data; //返回队首元素的值
Node p;
p=Q.front->next; //p指向队首结点
if(p->next) //判断队列是否只有一个结点
{
Q.front->next=p->next; //若不是,则头指针指向p->next
}
else
{
Q.rear=Q.front; //若是,则队尾指针指回头结点
Q.front->next=null; //并将头结点的next指针置空
}
free(p); //释放p所指结点(原队首结点)
}
}
//取队头元素
bool GetLinkQueueHead(LinkQueue Q,ElemType &x)
{
if(LinkQueueEmpty())
return false;
else
{
x=Q.front->next->data;
return true;
}
}
#endif
链式队列没有队满情况出现,会动态申请内存,不会造成空间的浪费,但是缺点是需要额外空间来存储指针,且如果指针设置不合理会造成出入队操作时间开支剧增。
该队列还可以改为使用循环链表,或者使用双向链表等其他链表结构实现。
在常见的数据结构试题中,常考的题涉及给出某种链表结构(单双向和是否循环以及存在不同指针的各种组合),问是否适合某种数据结构,这就要结合数据结构所需要的操作可不可以通过该物理结构完成或完成的时间开支是否合理来进行判断。
如对于队列来说,若采用链式存储,则表头位置是队头位置,表尾位置是队尾位置。而队列需要进行的常见操作是出队(在队头删除元素)以及入队(在表尾添加元素)。
- 对于入队操作,链式结构下需要知道当前尾结点,将尾结点的next置为新加入的结点地址,并将rear(尾指针)置为rear->next(新结点地址),再将新结点next置空。所以最优情况是单链表存在队尾指针或双向循环队列存在头或尾指针(可以通过常数次操作得到尾结点地址)等。(若不存在尾指针,单存在头指针的单链表也可实现入队,但需要O(n)时间复杂度才可得到队尾地址。)
- 对于出队操作,链式结构下需要知道头结点地址(存在头结点的链式队列),将队头元素结点(非头结点)地址保存,然后头结点的next指向第二个元素的结点(若存在,否则置空,单链表的话同时rear指针指向头结点),再将队头元素输出,free队头元素结点。所以最优情况是单链表存在头指针(指向头结点而非头元素结点),或者单向循环链表存在尾指针,或双向链表存在指向头结点指针或指向头元素结点指针,或双向循环链表存在头/尾/首元素结点指针等。(不存在指向头结点指针的单链表无法完成出队操作。)