数据结构与算法——队列

641 阅读6分钟

1.队列

队列,和栈一样,也是一种对数据的"存"和"取"有严格要求的线性存储结构。
与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出,如下图所示:

可以看出队列中数据的进出要遵循 "先进先出" 的原则。拿排队买票来说,所有的人排成一队,先到者排的就靠前,后到者只能从队尾排队等待,队中的每个人都必须等到自己前面的所有人全部买票成功并从队头出队后,才轮到自己买票。这就是典型的队列结构。

队列存储结构的实现有以下两种方式:
顺序队列:在顺序表的基础上实现的队列结构;
链队列:在链表的基础上实现的队列结构;

2.顺序队列

顺序队列,即采用顺序表模拟实现的队列结构。

2.1简单实现

由于顺序队列的底层使用的是数组,因此需预先申请一块足够大的内存空间初始化顺序队列。除此之外,为了满足顺序队列中数据从队尾进,队头出且先进先出的要求,我们还需要定义两个指针(front 和 rear)分别用于指向顺序队列中的队头元素和队尾元素. rear一般指向下一个队尾元素的存放位置。

(a)当队列为空时,rear与front位置重合。 (b)元素从队尾入队时,rear递增 (c)元素从队头出队是,fron递增 (d)c5 c6入队后,c3 c4出队时,rear已经已经不能在增长了,front签的空间也不能再利用了。 这样实现是有问题存在的, 所以我们需要换一种思路。

2.2循环队列

我们可以这样想象,把队列的收尾相连,这样出队空出的空间,还可以在队尾继续追加元素时利用。

(a)为初始时的空队列 front == rear (b)abc入队 (c)a出队 (d1)可以看到对满时 front==rear, 那么该如何判断是对满还是对空呢? (d2)牺牲掉数组中的一个存储空间,判断数组满员的条件是:尾指针的下一个位置和头指针相遇,就说明数组满了。(Q.rear + 1) % MAXSIZE == Q.front 下面我们就以循环队列的实现方式,实现一些队列的基本操作

队列定义

typedef int Status;
typedef int QElemType; /* SElemType类型根据实际情况而定,这里假设为int */

typedef struct {
    QElemType data[MAXSIZE];
    int front;//头指针
    int rear;//尾指针 若队列不空 指向队尾元素的下一个位置
} SqQueue;

2.2.1初始化

Status InitQueue(SqQueue *Q){
    Q->front = 0;
    Q->rear = 0;
    return OK;
}

2.2.2队列清空

//头尾指针都置为0
Status ClearQueue(SqQueue *Q){
    Q->front = Q->rear = 0;
    return 0;
}

2.2.3队列是否为空

Status QueueEmpty(SqQueue Q){
    //若队头指针等于队尾指针 则为空队列
    return Q.front == Q.rear;
}

2.2.4队列的当前长度

int QueueLength(SqQueue Q){
    return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}

2.2.5获取队列的头元素

//若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR;
Status GetHead(SqQueue Q,QElemType *e){
    if (QueueEmpty(Q)) {
        return ERROR;
    }
    *e = Q.data[Q.front];
    return OK;
}

2.2.6入队列

//若队列未满,则插入元素e为新队尾元素
Status EnQueue(SqQueue *Q,QElemType e){
    //队列已满
    if ((Q->rear+1)%MAXSIZE == Q->front) {
        return ERROR;
    }
    //将元素e赋值给队尾
    Q->data[Q->rear] = e;
    //rear指针向后移动一位,若到最后则转到数组头部;
    Q->rear = (Q->rear+1)%MAXSIZE;
    return OK;
}

2.2.7出队列

//若队列不空,则删除Q中队头的元素,用e返回值
Status DeQueue(SqQueue *Q,QElemType *e){
    //判断队列是否为空
    if (QueueEmpty(*Q)) {
        return ERROR;
    }
    //将队头元素赋值给e
    *e = Q->data[Q->front];
    //front 指针向后移动一位,若到最后则转到数组头部
    Q->front = (Q->front+1+MAXSIZE)%MAXSIZE;
    return OK;
}

2.2.8遍历队列

//从队头到队尾依次对队列的每个元素数组
Status QueueTraverse(SqQueue Q){
    int i = Q.front;
    while (i!=Q.rear) {
        printf("%d ", Q.data[i]);
        i = (i+1)%MAXSIZE;
    }
    return OK;
}

3.链式队列

链式队列,简称"链队列",即使用链表实现的队列存储结构。
链式队列的实现思想同顺序队列类似,只需创建两个指针(命名为 front 和 rear)分别指向链表中队列的队头元素和队尾元素.

链式队列结构定义如下

typedef int Status;
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */

typedef struct QNode    /* 结点结构 */
{
    QElemType data;
    struct QNode *next;
}QNode,*QueuePtr;

typedef struct            /* 队列的链表结构 */
{
    QueuePtr front,rear; /* 队头、队尾指针 */
}LinkQueue;

3.1 初始化队列

Status InitQueue(LinkQueue *Q){
    //1. 头/尾指针都指向新生成的结点
    Q->front = Q->rear = malloc(sizeof(QueueNode));
    
    //2.判断是否创建新结点成功与否
    if (!Q->front) {
        return ERROR;
    }

    //3.头结点的指针域置空
    Q->front->next = NULL;
    
    return OK;
}

3.2销毁队列Q

Status DestoryQueue(LinkQueue *Q){
    //遍历整个队列,销毁队列的每个结点
    while (Q->front) {
        QueuePtr p = Q->front->next;
        free(Q->front);
        Q->front = p;
    }
    return OK;
}

3.3将队列Q置空

Status ClearQueue(LinkQueue *Q){
    QueuePtr p, q;
    p = Q->front->next;
    Q->front->next = NULL;
    Q->rear = Q->front;
    while (p) {
        q = p->next;
        free(p);
        p = q;
    }
    return OK;
}

3.4判断队列Q是否为空

Status QueueEmpty(LinkQueue Q){
    return Q.front == Q.rear;
}

3.5 获取队列长度

int QueueLength(LinkQueue Q){
    int len = 0;
    QueuePtr p = Q.front;
    while (p != Q.rear) {
        len++;
        p = p->next;
    }
    return len;
}

3.6 插入元素e为队列Q的新元素

Status EnQueue(LinkQueue *Q,QElemType e){
    QueuePtr p = malloc(sizeof(QueueNode));
    //判断是否分配成功
    if (!p) {
         return ERROR;
    }
    p->data = e;
    p->next = NULL;
    
    //将新结点插入到队尾
    Q->rear->next = p;
    //修改队尾指针
    Q->rear = p;
    return OK;
}

3.7 出队列

Status DeQueue(LinkQueue *Q,QElemType *e){

    //判断队列是否为空;
    if (Q->front == Q->rear) {
        return ERROR;
    }
    //将要删除的队头结点暂时存储在p
    QueuePtr p = Q->front->next;
    //将要删除的队头结点的值赋值给e
    *e = p->data;
    //将原队列头结点的后继p->next 赋值给头结点后继
    Q->front->next = p->next;
    //若队头就是队尾,则删除后将rear指向头结点
    if(Q->rear == p) {
        Q->rear = Q->front;
    }
    free(p);
    
    return OK;
}

3.8 获取队头元素

Status GetHead(LinkQueue Q,QElemType *e){
    //队列非空
    if (Q.front != Q.rear) {
        //返回队头元素的值,队头指针不变
        *e =  Q.front->next->data;
        return TRUE;
    }
    
    return  FALSE;
}

3.9遍历队列

Status QueueTraverse(LinkQueue Q){
    QueuePtr p;
       p = Q.front->next;
       while (p) {
           printf("%d ",p->data);
           p = p->next;
       }
       printf("\n");
       return OK;
}

4.总结

由于顺序表的局限性,我们在顺序队列中实现数据入队和出队的基础上,又对实现代码做了改进,令其能够充分利用数组中的空间。链式队列就不需要考虑空间利用的问题,因为链式队列本身就是实时申请空间。因此,这可以算作是链式队列相比顺序队列的一个优势。