数据结构与算法(六) -- 队列

353 阅读5分钟

一、简介

队列是我们经常会遇到的一种数据结构,它遵循着一个先进先出的规则。

二、队列的创建

队列的类型也分为顺序存储与链式存储, 要根据不同的情况去考虑是使用顺序存储还是链式存储.

2.1、顺序存储

来定义一个顺序存储的队列结构

#define OK 1
#define ERROR 0

#define OK 1
#define ERROR 0

#define QueueSIZE 20

typedef struct SqQueue {
    int front;//头
    int rear;//尾
    QueueData data[QueueSIZE];
} SqQueue;

头尾分别记录数据域的位置下标

2.1.1、创建一个顺序存储队列

//创建一个空队列
Status CreateQueue(SqQueue *sq) {
    if (!sq) return ERROR;
    sq->front = sq->rear = 0;
    return OK;
}

在一个队列中我们只要知道头与尾处在同一个位置就可以知道这个队列是一个空队列.

2.1.2、清空与判断

//清空队列
Status ClearQueue(SqQueue *sq) {
    if (!sq) return ERROR;
    sq->front = sq->rear = 0;
    return OK;
}

//队列是否为空
bool isEmptyOnQueue(SqQueue sq) {
    if (sq.front == sq.rear) {
        return YES;
    } else {
        return FALSE;
    }
}

因为这是一个顺序存储队列, 清空队列并不要释放数据域的空间.

2.1.3、队列头与个数的获取

//返回队列个数
int QueueLength(SqQueue sq) {
    return (sq.rear - sq.front + QueueSIZE) % QueueSIZE;
}
//返回对列头元素
Status GetHead(SqQueue sq, QueueData *e) {
    if (sq.front == sq.rear) return ERROR;
    *e = sq.data[sq.front];
    return OK;
}

如下图:

这是最正常的队列情况, 我们只需要通过尾减去头就可以得到它的队列长度

如下图:

经过添加删除队列, 我们并不需要去重新排列数组, 这样会消耗大量时间.如果我们通过只改变头尾标记用来代替对队列的添加删除的的话, 就会出现这种情况.

为了处理这种情况, 我们将数据域看作是一个环形, 每当尾到达了数组的最大值的时候, 再继续添加, 尾的标记就会从数组的最开始的地方填充.

所以在这里进行队列个数的计算的时候, 我们用尾减去头会出现负数的情况 例如: 头 = 7, 尾 = 3, 假设数组为10 此时队列的数据是 7 8 9 0 1 2 就需要 (3 - 7 + 10) % 10得到队列个数的长度

2.1.4、添加队列元素

队列的特性是先进先出, 那么我们添加元素就是添加到尾上去.每次添加一个元素, 尾就得+1

//添加队列元素
Status AddElQueue(SqQueue *sq, QueueData e){
    if (!sq || (sq->rear + 1) % QueueSIZE == sq->front ) return ERROR;
    
    sq->data[sq->rear] = e;
    sq->rear = ++sq->rear % QueueSIZE;
    
    return OK;
}

此时需要考虑到, 当头不为0且添加的元素在数据域的最后面的时候, 尾+1后就得来到数据域的头部位置.

2.1.5、删除队列元素

删除队列元素也就是将元素出队. 这个时候出去的就是头指向的元素.

//删除队列元素
Status DelElQueue(SqQueue *sq, QueueData *e) {
    if (!sq || sq->rear == sq->front) return ERROR;
    if (e != NULL) {
        *e = sq->data[sq->front];
    }
    sq->front = ++sq->front % QueueSIZE;
    return OK;
}

此时也仍需考虑到, 当头指向的是数据域最后一个的时候, 出队后头的指向也需要来到数据域的第一个位置

2.1.6、遍历队列

从头位置依次遍历到尾

//遍历队列
Status TraverseQueue(SqQueue sq) {
    int i = sq.front;
    while (i != sq.rear) {
        printf("%d ", sq.data[i]);
        i = (i + 1) % QueueSIZE;
    }
    printf("\n");
    return OK;
}

仍需要注意当头从数据域的最后一个跳往数据域的第一个的问题.

2.2、链式存储队列

链式存储队列就要比顺序存储容易理解一些.

typedef struct QueueNode {
    QueueData data;
    struct QueueNode *next;
} QueueNode;

typedef struct ListQueue {
    QueueNode *front;
    QueueNode *rear;
} ListQueue;

头尾直接存储一个节点的指针. 不需要考虑队列是否存在满队以及头尾位置的问题

2.2.1、创建链式队列

//创建链式队列
Status CreateListQueue(ListQueue *lq) {
    if (!lq) return ERROR;
    lq->front = lq->rear = (QueueNode *)malloc(sizeof(QueueNode));
    return OK;
}

初始化的时候头尾都指向同一片内存.

2.2.2、清空与判断

//清空链式队列
Status ClearListQueue(ListQueue *lq) {
    if (!lq) return ERROR;
    while (lq->front != lq->rear) {
        QueueNode *temp = lq->front;
        lq->front = lq->front->next;
        free(temp);
    }
    return OK;
}

//判断空链式队列
bool isEmptyOnListQueue(ListQueue lq) {
    if (lq.front != lq.rear) {
        return false;
    }
    return true;
}

需要注意的是, 在链式队列中, 我们的每一个数据存储都会开辟一个新的节点. 当清除队列就需要将每一个节点全部释放.

2.2.3、队列头与个数的获取

//链式队列长度
int ListQueueLength(ListQueue lq) {
    int len = 0;
    QueueNode *temp = lq.front;
    while (temp != lq.rear) {
        len++;
        temp = temp->next;
    }
    return len;
}

//链式队列头
Status GetListQueue(ListQueue lq, QueueData *e) {
    if (lq.front == lq.rear) {
        return ERROR;
    }
    *e = lq.front->data;
    return OK;
}

获取队列长度, 我们仅需要遍历数据域的节点个数即可, 与链表计算长度方法一致.

2.2.4、添加队列元素

//添加元素到链式队列
Status AddElListQueue(ListQueue *lq, QueueData e) {
    if (!lq) return ERROR;
    QueueNode *new = (QueueNode *)malloc(sizeof(QueueNode));
    new->data = e;
    new->next = NULL;
    lq->rear->next = new;
    lq->rear = new;

    return OK;
}

每一次添加数据, 都要创建一个节点, 用来存放数据. 同时要将尾的下一个指向新创建的这个节点

2.2.5、删除队列元素

//链式队列删除元素
Status DelElListQueue(ListQueue *lq, QueueData *e) {
    if (!lq) return ERROR;
    QueueNode *del = lq->front;
    if (e != NULL) {
        *e = del->data;
    }
    lq->front = lq->front->next;
    free(del);
    return OK;
}

删除队列我们需要将头的指向跳往下一个节点, 此时需要释放掉旧节点.

2.2.6、遍历

//遍历链式队列
Status TraverseListQueue(ListQueue lq) {
    QueueNode *tem = lq.front;
    while (tem != lq.rear) {
        printf("%d ", tem->data);
        tem = tem->next;
    }
    printf("\n");
    return OK;
}

同链表的原理一致, 一个节点跳往下一个节点