0


数据结构之循环队列

个人主页:星纭-CSDN博客****

系列文章专栏:数据结构****

踏上取经路,比抵达灵山更重要!一起努力一起进步!

一.循环队列

1.循环队列的介绍

循环队列是一种特殊的队列数据结构,它可以在固定大小的数组中实现队列的功能。

如果使用数组完成队列的话,是会存在空间浪费的问题的。比如:

在完成插入数据之后,我们在出数据的时候,为了效率,是不会将后面的数据往前移动一个位置的,而是直接让front指针+1.那么数组下标为0的位置的数据就不在队列中了,可是这样是有个问题的,这个空间浪费了,不属于这个队列了。如果我们继续插入数据。

当在存储3个数据之后,此时的数组就用完了。而且随着数据不断删除,这个队列能存储的数据将会越来越少。那么应该怎么解决这个问题呢?

前面浪费的空间也可以利用起来,如果要再插入数据就将这个数据放入下标为0的位置上去,此时的队列才是真正的满了。

如果这样实现队列,就解决了数组实现队列空间浪费的问题。

2.循环队列的实现

typedef struct {
} MyCircularQueue;

//设置队列,队列长度为k
MyCircularQueue* myCircularQueueCreate(int k);

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value);
bool myCircularQueueDeQueue(MyCircularQueue* obj);

//获取队首,队尾元素
int myCircularQueueFront(MyCircularQueue* obj);
int myCircularQueueRear(MyCircularQueue* obj);

//判空判满
bool myCircularQueueIsEmpty(MyCircularQueue* obj);
bool myCircularQueueIsFull(MyCircularQueue* obj);

//释放
void myCircularQueueFree(MyCircularQueue* obj);

这个循环队列的结构体怎么完成?

首先一个指向数组的指针肯定需要有,然后就是两个指针front,rear分别代表头指针与尾指针。

其实还需要一个k来表示这个数组的大小。(原因文章后面解释)

rear指针的实际意义

假设此时循环队列为空,将front和rear都指向0这个下标。

此时因为循环队列没有数据,rear 指向的就是队尾元素的下一个位置。

如果要使rear指针指向队尾元素,那么rear起始就应该指向-1如果插入一个元素后加1,rear就是0,指向队尾元素。

这两者有什么区别呢?

其实没什么区别。但是不论什么情况都会有一个问题。

什么时候队列为空,什么时候队列为满?

如上图所示,当队列为满的时候,这两种情况下,rear指针和front指针的位置。

不难发现,当队列为空的时候,位置也是一样的。

所以无论,rear表示什么意义,在判满的时候都会遇到这样的问题。

那么如何解决判满判空呢?

方法一:使用size记录放入数据的个数

使用这样的方式会很容易解决这个问题?此时判断满与空,就不需要看这个front与rear指针的位置了。

当size等于k的时候这个队列就满了,当size等于0的时候这个队列就是空。

缺点:多创建了一个变量。

方法二:在开辟数组的时候,可以多开辟一个空间,不用来存放数据。

使rear表示队尾元素的下一个位置,并且对开辟一个空间不用来存放数据。

队列满的情形:

队列为空的情形:

此时不难发现,空与满的时候,front指针与rear指针的位置就没有相同了。

如何让队列循环起来?

观察这种情况

如果还要继续放入数据,那么rear的值就要从5变成0.可是在这之前插入数据都是+1即可,那么如何在数组末尾的时候,让rear指针从5变成0呢?

解决这样的问题,有多种方法,最简单的就是判断以下当rear == 5的时候,再插入数据,rear就变成0即可。

常用方法就是:使用取模运算符。

rear = (rear + 1) % 6

k + 1 = 5 + 1 = 6,这里的6实际就是数组的大小

同理当front指针,在数组末尾的时候,还要进行出数据就也是f = ( + 1) % k;

这也是为什么我们要在开头的时候,为啥要在队列的结构体中存放一个数据k在表示这个数组的大小 + 1,其实因为此时浪费了一个空间,所以实际上k应该是等于5的,表示实际上队列能存储的最多数据的个数。

3.代码

1.myCircularQueueCreate()函数

typedef int CQueueDatatype;
typedef struct {
    CQueueDatatype* a;//数组
    int front;//头指针,指向第一个元素
    int rear;//尾指针,指向队尾元素的下一个位置
    int k;//队列满时的数据个数
} MyCircularQueue;

MyCircularQueue* myCircularQueueCreate(int k) {
    MyCircularQueue* CQ = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    CQ->a = (CQueueDatatype*)malloc(sizeof(CQueueDatatype) * (k + 1));
    CQ->front = CQ->rear = 0;
    CQ->k = k;
    return CQ;
}

根据题目要求得知,函数参数中的k表示的队列的长度,因为要多开辟一个空间,所以在实际上这个数组的长度是k + 1.

2.判满判空函数

根据前面的分析,知道队列满的时候是有两种情况的。

第一种就是rear + 1 = front

第二种就是rear - 5 = front.

需要分类讨论吗?

其实是不需要的,根据数学运算不难发现 当 front = (rear + 1) % (k + 1).的时候此时就满了,结合了两种情况,读者可以自行带入数据进行尝试,这里其实与之前讲的如何循环起来是一样的。

判断空就很简单了,只有一种情况就是front和rear两个指针相同的时候,此时就相同了。

bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
    return obj->front == obj->rear;
}

bool myCircularQueueIsFull(MyCircularQueue* obj) {
    return obj->front == (obj->rear + 1) % (obj->k + 1);
}

3.插入删除函数

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
    if (myCircularQueueIsFull(obj)) {
        return false;
    }
    obj->a[obj->rear] = value;
    obj->rear = (obj->rear + 1) % (obj->k + 1);
    return true;
}

删除函数

bool myCircularQueueDeQueue(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj)) {
        return false;
    }
    obj->front = (obj->front + 1) % (obj->k + 1);
    return true;
}

4.查看队首队尾元素

int myCircularQueueFront(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj)) {
        return -1;
    }
    return obj->a[obj->front];
}
int myCircularQueueRear(MyCircularQueue* obj) {
    if (myCircularQueueIsEmpty(obj)) {
        return -1;
    }
    return obj->a[(obj->rear - 1 + obj->k + 1) % (obj->k + 1)]
}

在查看队尾元素的时候,是有一个问题的,当rear指针指向数组下标为0的位置的时候,此时的队尾元素在数组的最后一个位置上。所以如何要让rear从0变成k.

此时可以区分当rear为0和不为0 两个情况,也可以向以上代码中写的那样,只不过与之前讲的让队列循环起来采用了反着的方式,本质上没有区别,读者可以带入数字自行尝试。

5.释放函数

void myCircularQueueFree(MyCircularQueue* obj) {
    free(obj->a);
    free(obj);
}

向释放数组,再释放结构体。

循环队列是一种特殊的队列数据结构,它可以在固定大小的数组中实现队列的功能。循环队列的实现方式是通过使用一个数组和两个指针来管理数据的循环插入和删除。

循环队列的两个指针分别称为"front"和"rear"指针。front指针指向队列的第一个元素,rear指针指向最后一个元素的下一个位置。当队列为空时,front和rear指针指向同一个位置。

循环队列的插入操作是在rear指针的位置插入一个新元素,并将rear指针向后移动一位。如果rear指针到达数组的末尾,插入操作会循环到数组的开头。当rear指针指向front指针时,表示队列已满,无法插入新的元素。

循环队列的删除操作是将front指针指向的元素删除,并将front指针向后移动一位。如果front指针到达数组的末尾,删除操作会循环到数组的开头。当front指针等于rear指针时,表示队列为空。

循环队列的优点是可以充分利用数组空间,避免了元素的搬迁操作。相比于普通队列,循环队列的插入和删除操作的时间复杂度都是O(1)。

然而,循环队列的缺点是无法区分队列是满还是空。为了解决这个问题,可以设置一个额外的标志位来记录队列的状态。


本文转载自: https://blog.csdn.net/CNDS_lili/article/details/138845281
版权归原作者 星纭 所有, 如有侵权,请联系我们删除。

“数据结构之循环队列”的评论:

还没有评论