【C语言数据结构】之队列
一、基本概念
队列是一种特殊的线性表,它被限制只能在头部和尾部进行操作。并且只能在头部(队首)删除,在尾部(队尾)插入。
这就是队列的特点:先进先出(FIST IN FIST OUT)``(FIFO)
结构。
常用的两种实现队列的方式:一是使用顺序结构来实现,二是使用链式结构来实现。
二、顺序队列
顺序队列就是使用顺序结构来实现的队列,分配一段连续的存储空间,有头指针和尾指针分别指向头部元素和尾部元素的下一个位置。(在顺序队列中,头指针尾指针叫做头下标尾下标更合理,但是大家都习惯叫做指针了,也就这样吧)。
插入元素的时候现在尾指针所指向的空间插入,然后尾指针向后挪移一位。(这一点对下面的图示理解比较重要)
诶,这时候问题就出来了,这时候数组还有这么多空间没有存放数据,我如果想加入一个元素,可是我这时候队尾指针都指向了一片没有开辟的内存,这该怎么办啊。
这时候我们就得采用一种循环的方法来避免这种情况的发生,当队尾指针超出最大范围,我们就让他重新指向下标为0的位置。
这时候这个顺序队列就可以用这样的图来理解。
上图是空队列的图示。
上图是满队列的图示。
诶,这时候又有人想问了,为什么有6个空间,只有5个空间存放了元素呢?
原因是这样的:当头指针和尾指针重合的时候,我们就说他是空队列,但是,循环了一圈之后,尾指针追上了头指针,但是这时候是满队列,所以我们无法以这种方式区别空队列和满队列。
为了解决这种问题,我们只能浪费一个空间不去存放元素,我叫它空空间,它不是在一个下标固定不变的,这个空空间的下一个空间就是头指针所指向的空间。当尾指针到达了空空间的时候,这时候队头指针就在队尾指针的下一个位置,这时候我们称之为满队列。
因为我们的指针要循环,所以不能单纯的++,这样会超出,所以我们需要用到取模运算的方法。
队满:(p->back+1)%Size == p->head;
1 宏定义和结构体
# define True 1
# define False 0
# define Size 10
typedef struct Queue
{
int data[Size];
int head;
int back;
}Queue;
2 初始化一个队列
Queue* CreateQueue()
{
Queue* p = (Queue*)malloc(sizeof(Queue));
assert(p);
memset(p, 0, sizeof(Queue));//头指针尾指针都在下标为0处
return p;
}
3 判断队空队满
//判断队列为空
int IsEmpty(Queue* p)
{
assert(p);
if (p->head == p->back)
{
return True;
}
return False;
}
//判断队列满
int IsFull(Queue* p)
{
assert(p);
if (p->head == ((p->back + 1) % Size))
{
return True;
}
return False;
}
4 入队
入队,先让元素插入到队尾指针所指向的下标的位置,然后队尾指针向后挪移一个,注意不能是单纯的++
,因为是循环的,所以我们要取模运算。
比如说我目前尾下标是4
,总共有6
个空间大小。插入了元素之后尾下标挪移到了下标为5
的位置。
这时候我们要想加入一个元素之后,下标就会再向后挪移一位,这时候不能挪移到6
了,因为下标最大就是5
。
按道理我们要挪移到下标为1
的位置,但是怎么去呢,我们采用取模运算的方法(5 + 1) % 6 = 0
,这样指针就到了下标为0
的位置。
在这里,6
是最大空间,5
是当前队尾指针所指下标,所以可以这样表示(p->back + 1) % Size
void InsertQueue(Queue* p, int num)
{
if (IsFull(p) == True)
{
printf("队满无法入队\n");
return;
}
p->data[p->back] = num;
p->back = (p->back + 1) % Size;
}
5 出队
int PopQueue(Queue* p)
{
if (IsEmpty(p) == True)
{
printf("队伍为空无法出队\n");
return -1;
}
int x = p->data[p->head];
p->head = (p->head + 1) % Size;
return x;
}
6 获取队列内元素数量
(p->back - p->head + Size) % Size
就能获取队列元素数量
int LenQueue(Queue* p)
{
return (p->back - p->head + Size) % Size;
}
7 查看队首元素
void ShowHead(Queue* p)
{
if (IsEmpty(p) == True)
{
printf("队列为空\n");
return;
}
printf("队首元素是 %d \n", p->data[p->head]);
}
三、链式队列
链式队列就是单链表,只不过限制了单链表只能头删尾插操作。剩下的操作和单链表相差不大。
1.结构体和宏定义
# define True 1
# define False 0
typedef struct Queue
{
int data;
struct Queue* next;
struct Queue* back;//尾部指针
}Queue;
2. 初始化
Queue* CreateQueue()
{
Queue* p = (Queue*)malloc(sizeof(Queue));
assert(p);
memset(p, 0, sizeof(Queue));
return p;
}
3. 判断队空
int IsEmpty(Queue* p)
{
assert(p);
if (p->next == NULL)
{
return True;
}
return False;
}
4. 入队
入队后记得更新队尾指针即可。
void InsertQueue(Queue* p, int num)
{
assert(p);
Queue* node = (Queue*)malloc(sizeof(Queue));
memset(node, 0, sizeof(Queue));
node->data = num;
if (IsEmpty(p) == True)
{
p->next = node;
p->back = node;
}
p->back->next = node;
p->back = node;
}
5. 出队
如果出队后没有元素了,记得更新队尾指针指向空即可。
int PopQueue(Queue* p)
{
if (IsEmpty(p) == True)
{
printf("空队列无法出队\n");
return -1;
}
Queue* tmp = p->next;
p->next = tmp->next;
int x = tmp->data;
free(tmp);
tmp = NULL;
if (IsEmpty(p) == True)
{
p->back = NULL;
}
return x;
}
6. 打印队首元素
void ShowHead(Queue* p)
{
if (IsEmpty(p) == True)
{
printf("空队列没有首元素\n");
return;
}
printf("%d\n", p->next->data);
}
四、总结
顺序队列用的最多的,最方便的就是循环顺序队列,循环顺序队列我们只需要确保循环时候的下标不要出错,合理运用取模运算就好。
链式队列就是限制了输入输出的单链表,我个人习惯在结构体中多加了一个尾指针,所以在进行入队出队的时候要确保尾指针指向正确,尤其是最后一个元素出队后,队列中没有元素了,尾指针要指向空。并且入队时,如果队列是空的,不能直接使用尾指针来入队。