队列(Queue)
队列:是只允许在一端进行插入操作,而在另一端进行删除操作。(可以是表头,也可以是表尾)
我们可以看到,队列其实也是线性表的一种,队列的意思其实就和他的名字一样,他的存储特点就像一个一堆数据排队一样,先排到队里面的数据就先出来(First In First Out),简称FIFO。允许插入的一段为表尾,允许删除的一端称为表头。
operation //常用操作
InitQueue(*Q) //初始化操作,建立一个空队列Q。
DestroyQueue(*Q) //若队列Q存在,则销毁它。
ClearQueue(*Q) //将队列Q清空。
QueueEmpty(Q) //若队列Q为空,返回true,否则返回false。
GetHead(Q,*e) //若队列Q存在且非空,用e返回队列Q的队头元素。
EnQueue(*Q,e) //若队列Q存在,插入新元素e到队列Q中并成为队尾元素。
DeQueue(*Q,*e) //删除队列Q中队头元素,并用e返回其值。
QueueLength(Q) //返回队列Q的元素个数。
队列的顺序存储结构
因为队列也是线性表的一种,所以如果队列要存储 n 个元素,就要创建一个长度为 n+1 的数组。
我们的入队列操作就是在队尾添加一个元素很明显,其时间复杂度为O(1);但是出队列就不同了,在这里可以想到两种方法。方法一:执行出队列操作时,删除队头元素,并且把后面的元素向前移动,他的时间复杂度为O(n),我们可以发现,他并不是一个好方法,他的时间复杂度太高了,所以我们有了方法二:只删除队头操作,不进行移动,诶~这时间复杂度就好看多了O(1)。
但是我们又遇到了一个新的问题,那就是,我们的元素总要进进出出吧,当我们的队尾位置是数组的最后一个位置时,就无法进行进队列的操作了,但是,我们的队头不一定就是数组的起始位置啊,可能排在老后面呢,这样的话,就存在空间的浪费了。
解决这个问题的办法其实很简单,就是吧这个数组从一条线,看成一个圆,里面的下标是循环的,假设有一个大小为5的数列,那么我们可以把它看成:0,1,2,3,4,0,1,2,3,4,0······这样,就很完美的解决了空间浪费的问题。
还有一个小问题,如果是循环队列,要怎样确定它存满了呢?假设我们把队头取名为(front),队尾的下一个位置为(rear),则当 front == rear 时,表示队列为空。这里又两种方法:
①创建一个 flag 变量,有元素时为1,没有元素时为0,当 front == rear 时,如果 flag 为1,则队列满;否则队列空。
②在队尾留一个空位置,意思是存满时其实只存了数组大小减1个元素,这时,存满的标识即为 (rear+1)%arrayLength == front (这里的arrayLength指数组的大小)
我们着重介绍方法二,队列的结构为:
#define OK 1
#define ERROR 0
#define TURE 1
#define FALSE 0
typedef int Status;
/*Status是函数的类型,其值是函数结果状态代码,如OK等*/
typedef int QelemType;
/* 这是循环队列的顺序存储结构 */
typedef struct{
QElemType data[MAXSIZE];
int front;
int rear;
}SqQueue;
初始化
Status InitQueue(SqQueue *Q){
Q->front = 0;
Q->rear = 0;
return OK;
}
求长度
int QueueLength(SqQueue Q){ // 返回队列的长度
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE;
}
入队列
Status EnQueue(SqQueue *Q,QElemType e){
if((Q->rear + 1) % MAXSIZE == Q->front) // 先判断队列是否已满
return ERROR;
Q->data[Q->rear] = e;
Q->rear - (Q->rear+1) % MAXSIZE; // rear指针后移
return OK;
}
出队列
Status DeQueue(SqQueue *Q,QElemType *e){
if(Q->front == Q->rear)
return ERROR
*e = Q->data[Q->front];
Q->front = (Q-front + 1) % MAXSIZE;
return OK;
}
他们的时间复杂度都为O(1),同样顺序存储结构的缺点就是他们的长度在一开始就已经确定了,如果数据过多,也无法在后期增加长度了。所以,它的好兄弟链式存储结构,他来了。
队列的链式存储结构
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出,我们把它简称为链队列。在链队列中, front 指向队列的头节点, rear 指向终端结点,当队列为空时, front 和 rear 都指向头节点。(注意头节点与头指针之间的异同)
#define OK 1
#define ERROR 0
#define TURE 1
#define FALSE 0
typedef int Status;
typedef int QElemType;
typedef struct QNode{
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
typedef struct {
QueuePtr front,rear; /* 定义头尾指针 */
}LinkQueue;
入队列操作
Status EnQueue(LinkQueue *Q,QElemtype e){
QueuePtr s=(QueuePtr)malloc(sizeof(QNode));
if(!S)
exit(OVERFLOW);
s->data = e;
s->next = NULL;
Q->rear->next = s;
Q->rear = s;
return OK;
}
出队列操作
Status DeQueue(LinkQueue *Q,QElemType *e){
QueuePtr p;
if(Q->front == Q->rear)
return ERROR;
p = Q->front->next;
*e = p->data;
Q->front->next = p->next;
if(Q->rear == p)
Q->raer = Q->front;
free(p);
return OK;
}
可以看出,它们的时间复杂度都为O(1)。
总结
首先,队列的性质我们要清楚,就是一端进,另一端出。
其次,对比它的两种存储结构。我们可以发现,这两种存储结构的基本操作的时间复杂度都为O(1),他们的主要区别就是存储空间的不同,对于顺序存储结构,他在一开始就固定了存储空间大小的,所以它的最多存放的数据是固定多的,但是链式存储结构则在理论上可以存储无限个数据,但因为每个结点需要存放下一个结点的地址,所以存储相同数据使用的大小要比顺序存储空间多。我们应视情况而考虑用哪一种存储结构。