队列:与栈相反,数据是“先进先出”,它只允许在队列的一端(队尾)进行插入,而在另一端(队头)删除元素。
一、顺序队列
队尾:可插入数据的位置,即最后一个数据的后一个格子
队头:第一个数据的位置
在顺序表中实现队列,队列的长度是有限的,当队头的数据被删除时,后面的数据需要先前移动,以保证队尾总能空出至少一个位置来存放插入的数据。
但是,在这种“线性”设计中,数据只能从队头删除(出队),所以时间复杂度为O(1),数据同时也只能从队尾插入(入队),多了移动整组数据的操作,因而时间复杂度为O(n),那么有没有更好的设计方法能够使插入数据的时间复杂度也变为O(n)?
其实,在线性表的基础上,将整个表设计为“环形”更加方便、快捷。同时,需要两个指针分别指向队头和队尾,只是这个指针指的实际上是数据的下标,并且为了方便区分“队列空”和“队列满”,队列中单独空出一个空间来标记结尾。
也就是说,在环形队列中,随着数据的插入和删除,队头和队尾的位置可以随时变化,而整个队列仍然有序。
下面就来看看在环形队列中的基本操作吧。
声明:
//环形队列
#define SIZE 4
typedef struct Queue
{
int elem[SIZE];
int front;//队头指针,第一个数据的下标
int rear;//队尾指针,最后一个数据后的第一个格子的下标
}Queue,*PQueue;
void Initqueue(PQueue pq);
bool Push(PQueue pq, int val);
bool IsEmpty(PQueue pq);
bool GetTop(PQueue pq, int *rtval);
bool Pop(PQueue pq, int *rtval);
void Destroy(PQueue pq);
void Show(PQueue pq);
1.环形队列的结构体表示:
typedef struct Queue
{
int elem[SIZE];
int front;//队头指针,第一个数据的下标
int rear;//队尾指针,最后一个数据后的第一个格子的下标
}Queue,*PQueue;
2.初始化:
void Initqueue(PQueue pq)
{
assert(pq!=NULL);
pq->elem[SIZE] = 0;
pq->front = 0;
pq->rear = 0;
}
初始化时,队头指针和队尾指针同时指向0号下标。
3.入队
//判满
static bool IsFull(PQueue pq)
{
return (pq->rear+1)%SIZE == pq->front;
}
//入队
bool Push(PQueue pq, int val)
{
if(IsFull(pq))//队满
{
return false;
}
pq->elem[pq->rear] = val;
pq->rear++;
return true;
}
入队时,一般情况下,在队尾插入数据之后,队尾指针需要向后移动一格。同时,我们需要考虑“队列满”时,数据不能在插入的情况。
4.判空
bool IsEmpty(PQueue pq)
{
return pq->front==pq->rear;
}
环形队列为了区分“队列空”和“队列满”,专门空出一个格子来标志结尾。除去初始化之后的front == rear,队列中的数据不论怎么删减、插入,只要队列中还有数据,那么front != rear是永远成立的,即判空条件为“front == rear”。
5.得到队顶的数据,不删除
bool GetTop(PQueue pq, int *rtval)
{
if(IsEmpty(pq))
{
return false;
}
*rtval = pq->elem[pq->front];//通过输出参数传出数据
return true;
}
6.得到队顶的数据,删除
bool Pop(PQueue pq, int *rtval)
{
if(IsEmpty(pq))
{
return false;
}
*rtval = pq->elem[pq->front];//通过输出参数传出数据
pq->front++;//队头指针向后移动
return true;
}
在得到队顶的元素之后,将其删去。
7.销毁队列
void Destroy(PQueue pq)
{
pq->rear = pq->front;//依照“队列空”的判断条件:pq->rear == pq->front
}
队列中的数据是从队头开始排列的,所以销毁一个队列只需要将其队头置为队尾就可以了。
8.打印
void Show(PQueue pq)
{
int i;
for(i=pq->front; i<pq->rear; i++)
{
printf("%d ",pq->elem[i]);
}
printf("\n");
}
二、链式队列
在上述中的环形队列,队列的长度是有一个最大限度的,如果无法预估所用队列的最大长度,可以选择用单链表来实现队列——链式队列。
链式队列的结构如下:
从图中我们可以看出,链式队列和单链表的结构还是有一些差别的。
1.头节点:队列的头节点里面存放的是两个指针的内容,队头、队尾指针的指向。
2.尾指针:队列增加了一个指针——尾指针,用来标记结尾,方便在队尾插入数据。因为在环形队列中我们实现了队头插入、队尾删除的时间复杂度都为O(1),而在单链表中,每一次都需要对链表进行遍历找到尾节点再进行数据插入,正是为了简便这个过程,提高时间复杂度,我们增添了一个尾指针,那么队尾插入的时间复杂度也就是O(1)了。
下面就让我们看看在链式队列中如何实现基本操作吧。
声明:
//带尾指针的单链表
typedef struct QNode//定义数据节点
{
int data;
struct QNode *next;
}QNode;
typedef struct HNode//单独定义头结点
{
QNode *front;//队头指针
QNode *rear;//队尾指针
}HNode,*PLQueue;
void L_Initqueue(PLQueue pq);
bool L_Push(PLQueue pq, int val);
bool L_IsEmpty(PLQueue pq);
bool L_GetTop(PLQueue pq, int *rtval);
bool L_Pop(PLQueue pq, int *rtval);
void L_Destroy(PLQueue pq);
void L_Show(PLQueue pq);
1.链式队列的结构体表示:
typedef struct QNode//定义数据节点
{
int data;
struct QNode *next;
}QNode;
typedef struct HNode//单独定义头结点
{
QNode *front;//队头指针
QNode *rear;//队尾指针
}HNode,*PLQueue;
由于头节点和各个数据结点之间结构的差异,他们也是分开各自定义的。
2.链式队列的初始化
void L_Initqueue(PLQueue pq)
{
assert(pq!=NULL);
pq->front = NULL;
pq->rear = NULL;
}
3.入队
bool L_Push(PLQueue pq, int val)
{
QNode *p;
p = (QNode*)malloc(sizeof(HNode));//创建新结点
p->data = val;//放入数据
p->next = NULL;
if(pq->front==NULL && pq->rear==NULL)//第一次插入
{
pq->front = p;
pq->rear = p;
}
else
{
pq->rear->next = p;
pq->rear = p;
}
return true;
}
bool L_IsEmpty(PLQueue pq)
{
return pq->front ==NULL;//没有队头
}
bool L_GetTop(PLQueue pq, int *rtval)
{
if(pq->front==NULL)//队列为空
{
return false;
}
*rtval = pq->front->data;//取数据
return true;
}
bool L_Pop(PLQueue pq, int *rtval)
{
if(pq->front==NULL)//队列为空
{
return false;
}
*rtval = pq->front->data;//取数据
//删数据
QNode *p;
p = pq->front;
pq->front = pq->front->next;//绑气球
free(p);
return true;
}
void L_Destroy(PLQueue pq)
{
QNode *p;//从第一个数据结点开始,总是删除第一个数据结点
while(pq->front!=NULL)
{
p = pq->front;
pq->front = p->next;
free(p);
}
pq->rear = NULL;//队尾指针置空
}
8.打印
void L_Show(PLQueue pq)
{
for(QNode *p=pq->front; p!=NULL; p=p->next)
{
printf("%d ",p->data);
}
printf("\n");
}