一.队列是什么?
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
二.队列的分析
- 队列这种数据结构,与栈结构都是受限制的线性表,但是队列结构恰恰与栈结构相反。
- 栈结构是先进后出,而队列结构是,先进的先出。
- 队列结构跟栈结构实现方法相似,可以用链表实现的链队列,用顺序表实现的顺序结构等。
三.队列的实现
1.链队列
a.链队列的分析
- 链队列,顾名思义就是用链表实现的队列结构,这里我们使用单向链表。
- 其中涉及的简单操作有,入队, 出队,打印队列元素,获取队头元素,获取当前队列的元素个数,清楚队列,摧毁队列等。
- 在这些操作中,我们需要特别注重的是入队和出队,只要这两个关键操作做好,其他的都是易于实现的。
- 要实现链队列,我们首先需要创建一个链表,包含数据域跟指针域。那数据域还不确定要存储的是什么类型的,所以我们就将数据类型使用#define定义出来,以后如果想要存储的数据类型发生改变,这样修改代码比较容易。
- 其次我们还需要一个结构体,这个结构体包含了两个指向链表节点的指针,一个为队首指针,永远指向队伍的头部,另一个为队尾指针,永远指向队的尾部。
- 最后我们创建这个结构体变量,包括这两个指针,将结构体变量的地址付给指针Q,全局变量方便函数内使用。
#include<stdio.h> #include<stdlib.h> #define DATE_TYPE int//使用定义宏,使得以后修改代码方便 struct QUEUE_node//队列的节点 { DATE_TYPE date;//数据域 struct QUEUE_node* next;//指针域 }; struct LINK_QUEUE//链队列 { struct QUEUE_node* front;//队首指针(永远指向队伍的头结点) struct QUEUE_node* end;//队尾指针(永远指向队伍的最后一个节点) }; struct LINK_QUEUE QE;//创建一个队列 struct LINK_QUEUE* Q = &QE;//使用指针更加方便函数内使用
b.具体实现
- 在上面我们已经做好了所有的准备工作,接下来我们要实现各种函数并且在主函数中调用,用来检测各种函数的功能是否正确。
- 首先就是初始化。我们需要一个头节点,开始时头节点的指针域为空,这个时候表示空表,我们需要让两个指针都指向头节点。
void INITIAL_LINK_QUEUE()//初始化函数 { struct QUEUE_node* head = (struct QUEUE_node*)malloc(sizeof(struct QUEUE_node));//创建头节点并且申请空间 Q->front = Q->end = head;//开始时队列为空,首指针和尾指针都指向头节点 Q->end->next = NULL;//让头节点的指针域为空 }
- 初始化完成之后我们就要开始进行入队的操作了,入队其实就是在链表的尾部进行尾插法,而出队就是在头部删除节点罢了。这其中涉及的其实就是关于单链表的操作。入队之后队首指针不变,队尾指针要随之移动。但是要注意下,如果要出队的话就要先判断队伍是否为空,如果为空就无法出队。而我们判断队列是否为空的标志就是两个指针是否相等,如果相等,说明为空。
void ENTER_QUEUE(DATE_TYPE n) { struct QUEUE_node* new = (struct QUEUE_node*)malloc(sizeof(struct QUEUE_node));//创建新节点 new->date = n;//将数据入队 new->next = NULL;//让新节点的指针域为空 Q->end->next = new;//连接 Q->end = new;//更新队尾指针 }
- 打印当前队列的元素。这里我们需要一个遍历的操作。我们知道队首指针指向的是头节点,而头节点的数据域并不存放数据,所以我们将第一个真正存放数据的节点的首地址赋给变量pc,进行遍历打印。这个操作的本质就是单链表的遍历。
void SHOW_QUEUE() { printf("当前队列的数据为:"); struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第一个存储数据的节点地址,方便接下来的遍历操作 while (pc != NULL) { printf("%d ", pc->date);//打印当前节点的数据 pc = pc->next;//移动到下一个节点 } printf("\n"); }
- 出队。出队其实就是在队列头部删除元素,本质就是在单链表头部删除节点。要注意的地方是头结点是不动的,我们删除的不是头结点,而是第一个存储数据的首节点,但是我们知道,头节点的指针域指向的是第一个存储数据的节点,现在这个节点被删除了,所以我们要将头节点的指针域指向原本未删除链表的第二个存储数据的节点。
void GET_OFF_QUEUE()//出队函数 { if (Q->front == Q->end)//如果这两者相等,说明队列为空,直接返回 { printf("空队列!!!\n"); return; } //因为对列的删除是在头部进行,所以,我们要删除第一存放数据的节点,就要将头节点跟第二个 //存放数据的节点链接起来,然后将原来的第一个存放数据的节点删除掉 struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第二个存放数据的节点的地址 Q->front->next = pc->next;//连接头节点跟第二个存储数据的节点 free(pc);//直接释放这个节点 if (pc == Q->end)//如果删除到最后一个节点,队列就为空了,要将队尾指针指向头结点 Q->end = Q->front; pc = NULL; }
- 获取队头元素。我们知道队首节点永远指向头结点,而头节点的指针域指向的是存放队头元素的节点。所以我们直接打印就ok。
void GET_HEAD_OF_QUEUE()//打印队头数据 { printf("当前队列头部元素为:%d\n", Q->front->next->date); }
- 求得队列中的元素个数。元素个数其实就是遍历的时候加上一个计数器,最后打印计数器的值就好了。
void NUM_OF_QUEUE()//统计当前队列的元素个数 { if (Q->end == Q->front)//判断队列是否为空 { printf("当前队列为空!!!"); return; } int cent = 0; struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第一个存储数据的节点地址,方便接下来的遍历操作 while (pc != NULL) { cent++; pc = pc->next; } printf("当前队列的元素个数为:%d\n", cent); }
- 清除队列。如果要清除队列的话,我们可以清除队列所有的元素,然后将队尾指针重新指向头结点。
void CLEAR_QUEUE()//清除队列所有元素 { if (Q->end == Q->front)//判断是否为空队 { printf("当前队列为空!!!"); return; } struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第一个存储数据的节点地址 while (pc != NULL) { Q->front->next = pc->next; free(pc);//释放节点空间 pc = Q->front->next;//移动到下一个节点 } Q->end = Q->front;//清除掉最后一个节点时要将队尾指针指向头结点 printf("\n"); }
- 摧毁队列,我们在摧毁之前先清除队列中的所有数据,然后释放头节点的空间,将两个指针赋空。
void DISTORY_QUEUE()//摧毁队列 { CLEAR_QUEUE();//摧毁前先清除所有数据 free(Q->front);//释放头结点空间 Q->front = Q->end = NULL;//将指针赋空 printf("已摧毁!!!\n"); }
- 完整代码
#include<stdio.h> #include<stdlib.h> #define DATE_TYPE int//使用定义宏,使得以后修改代码方便 struct QUEUE_node//队列的节点 { DATE_TYPE date;//数据域 struct QUEUE_node* next;//指针域 }; struct LINK_QUEUE//链队列 { struct QUEUE_node* front;//队首指针(永远指向队伍的头结点) struct QUEUE_node* end;//队尾指针(永远指向队伍的最后一个节点) }; struct LINK_QUEUE QE;//创建一个队列 struct LINK_QUEUE* Q = &QE;//使用指针更加方便函数内使用 void INITIAL_LINK_QUEUE()//初始化函数 { struct QUEUE_node* head = (struct QUEUE_node*)malloc(sizeof(struct QUEUE_node));//创建头节点并且申请空间 Q->front = Q->end = head;//开始时队列为空,首指针和尾指针都指向头节点 Q->end->next = NULL;//让头节点的指针域为空 } void ENTER_QUEUE(DATE_TYPE n) { struct QUEUE_node* new = (struct QUEUE_node*)malloc(sizeof(struct QUEUE_node));//创建新节点 new->date = n;//将数据入队 new->next = NULL;//让新节点的指针域为空 Q->end->next = new;//连接 Q->end = new;//更新队尾指针 } void SHOW_QUEUE() { printf("当前队列的数据为:"); struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第一个存储数据的节点地址,方便接下来的遍历操作 while (pc != NULL) { printf("%d ", pc->date);//打印当前节点的数据 pc = pc->next;//移动到下一个节点 } printf("\n"); } void GET_OFF_QUEUE()//出队函数 { if (Q->front == Q->end)//如果这两者相等,说明队列为空,直接返回 { printf("空队列!!!\n"); return; } //因为对列的删除是在头部进行,所以,我们要删除第一存放数据的节点,就要将头节点跟第二个 //存放数据的节点链接起来,然后将原来的第一个存放数据的节点删除掉 struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第二个存放数据的节点的地址 Q->front->next = pc->next;//连接头节点跟第二个存储数据的节点 free(pc);//直接释放这个节点 if (pc == Q->end)//如果删除到最后一个节点,队列就为空了,要将队尾指针指向头结点 Q->end = Q->front; pc = NULL; } void NUM_OF_QUEUE()//统计当前队列的元素个数 { if (Q->end == Q->front)//判断队列是否为空 { printf("当前队列为空!!!"); return; } int cent = 0; struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第一个存储数据的节点地址,方便接下来的遍历操作 while (pc != NULL) { cent++; pc = pc->next; } printf("当前队列的元素个数为:%d\n", cent); } void GET_HEAD_OF_QUEUE()//打印队头数据 { printf("当前队列头部元素为:%d\n", Q->front->next->date); } void CLEAR_QUEUE()//清除队列所有元素 { if (Q->end == Q->front)//判断是否为空队 { printf("当前队列为空!!!"); return; } struct QUEUE_node* pc = Q->front->next;//创建变量pc存放第一个存储数据的节点地址 while (pc != NULL) { Q->front->next = pc->next; free(pc);//释放节点空间 pc = Q->front->next;//移动到下一个节点 } Q->end = Q->front;//清除掉最后一个节点时要将队尾指针指向头结点 printf("\n"); } void DISTORY_QUEUE()//摧毁队列 { CLEAR_QUEUE();//摧毁前先清除所有数据 free(Q->front);//释放头结点空间 Q->front = Q->end = NULL;//将指针赋空 printf("已摧毁!!!\n"); } int main() { INITIAL_LINK_QUEUE();//调用初始化函数,对队列进行初始化 //下面是入队的简单示例(这里以整形数据为例,将1到n进行入队) int n; printf("请输入你想要入队的数据个数:"); scanf("%d", &n); for (int i = 1; i <= n; i++) ENTER_QUEUE(i);//调用入队函数 SHOW_QUEUE();//打印当前队中的数据 //下面是出队的简单示例(同样以整形数据为例) GET_OFF_QUEUE();//调用出队函数 SHOW_QUEUE(); //下面是统计当前队列中的数据个数的简单示例 NUM_OF_QUEUE(); //下面是获取队头数据的简单示例 GET_HEAD_OF_QUEUE(); //下面是清除队列的简单示例 CLEAR_QUEUE(); //下面是摧毁队列的简单示例 DISTORY_QUEUE(); return 0; }
2.顺序队列
- 接下来我们进行顺序队列的分析,其实顺序表的实现比较简单,跟栈结构的实现差不多,只是有些许改变。
- 下面我就不详细描述,附上完整代码和和运行结果。
#include<stdio.h> #include<stdlib.h> #define DATE_TYPE int//定义数据类型方便以后修改 #define INITIAL_QUEUE_SIZE 10 struct QUEUE { DATE_TYPE* base;//基地址 int head;//队首指针(永远指向第一个元素) int end;//队尾指针(永远指向最后一个元素) }; struct QUEUE QE;//创建队列 struct QUEUE* Q = &QE;//将队列的地址赋给Q方便函数使用 void INITIAL_QUEUE()//初始化函数,对队列进行初始化 { Q->base = (DATE_TYPE*)malloc(INITIAL_QUEUE_SIZE * sizeof(DATE_TYPE)); Q->end = Q->head = 0;//初始队列为空,所以让他们都为0 } void ENTER_QUEUE(int n)//入队函数 { if (Q->end == INITIAL_QUEUE)//判断队是否已经满了如果满了就直接返回 { printf("队已满!!!无法入队\n"); return; } Q->base[Q->end++] = n;//将元素入队并且移动队尾 } void SHOW_QUEUE()//打印队内所有元素 { if (Q->end == Q->head) { printf("空队!!!\n"); return; } printf("当前队内的元素为:"); for (int i = Q->head; i < Q->end; i++) printf("%d ", Q->base[i]); printf("\n"); } void GET_OFF_QUEUE()//出队函数 { if (Q->end == Q->head)//判断是否空队 { printf("空队!!!\n"); return; } Q->head++; } void GET_HEAD_OF_QUEUE()//获取队头元素 { printf("当前队头元素为:%d\n", Q->base[Q->head]); } void NUM_OF_QUEUE() { printf("当前队内的元素个数为:%d\n", Q->end - Q->head); } void CLEAR_QUEUE() { Q->end = Q->head = 0; } void DISTORY() { free(Q->base); Q->end = Q->head = 0; Q->base = NULL; } int main() { INITIAL_QUEUE();//调用初始化函数 //入队 int n; printf("请输入你想要入队的元素个数:"); scanf("%d", &n); for (int i = 1; i <= n; i++) ENTER_QUEUE(i); SHOW_QUEUE();//打印队内元素 //出队 GET_OFF_QUEUE(); SHOW_QUEUE(); //获取队头元素 GET_HEAD_OF_QUEUE(); //统计当前队内元素个数 NUM_OF_QUEUE(); //清除队列 CLEAR_QUEUE(); SHOW_QUEUE(); //摧毁队列 DISTORY(); return 0; }
- 大家如果去试验的话,并且仔细想想的话,这个代码其实是存在问题的。为什么这样说呢?因为如果你就算将元素存满了,但是出队是在前面,整个队伍只会在后面存放元素,所以出队后,队列未满但也无法入队新的元素,这样就造成了空间的浪费。这就是顺序队列的虚假满状态。
- 那么我们有没有什么好的办法呢?这时候后就该请出循环队列了,循环队列并非真正的循环。而是说将尾指针指向队尾时,如果队伍已满,前面有出队的元素,还可以让尾指针移动到出队的地方存放新元素,这样就能减少空间的浪费。
3.循环队列
a.图示
b.分析
- 如上图所示,这就是循环队列的简单图示。其实就是稍加改动的顺序队列。
- 内部小圈代表数组下标。外部大圈代表存储数据的空间。我们从下标0开始,依次将1~7存放在这个顺序队列中。我们需要空出来一个位置不要存放数据,这将会作为我们判断队列是否已满的标志
- 我们现在这样想,如果将1出队。head就要++,那么就相当于空出来一个位置。那我们如果要入队的话,就要对end++,但是end++的结果可能大于最大存储的队列元素个数。所以我们要对end取模,这样就相当于end转了一圈又回到了老位置。所以循环队列的循环并不是指真的循环,而是通过取模的操作是得尾指针可以指向之前走过的位置,所以成为循环队列。
- 如果要判断队列是否已满的话,就对end加一之后取模看看是否跟head相等,如果相等就是队列已满。这里是因为我们提前空出来一个位置,如果满了。那么头尾指针也就是差了这一个位置,所以加一在取模
- 如果要判断队列是否已空,判断end是否跟head相等,如果相等就说明队列为空。这里是因为我们提前空出来一个位置,如果满了。那么头尾指针也就是差了这一个位置,所以加一在取模
c.代码实现及运行结果
#include<stdio.h>
#include<stdlib.h>
#define DATE_TYPE int//定义数据类型方便以后修改
#define INITIAL_QUEUE_SIZE 10
struct QUEUE
{
DATE_TYPE* base;//基地址
int head;//队首指针(永远指向第一个元素)
int end;//队尾指针(永远指向最后一个元素)
};
struct QUEUE QE;//创建队列
struct QUEUE* Q = &QE;//将队列的地址赋给Q方便函数使用
void INITIAL_QUEUE()//初始化函数,对队列进行初始化
{
Q->base = (DATE_TYPE*)malloc(INITIAL_QUEUE_SIZE * sizeof(DATE_TYPE));
Q->end = Q->head = 0;//初始队列为空,所以让他们都为0
}
void ENTER_QUEUE(int n)//入队函数
{
if ((Q->end+1)%INITIAL_QUEUE_SIZE==Q->head)//判断队是否已经满了如果满了就直接返回
{
printf("队已满!!!%d无法入队\n",n);
return;
}
Q->base[Q->end] = n;//将元素入队并且移动队尾
Q->end = (Q->end+1) % INITIAL_QUEUE_SIZE;
}
void SHOW_QUEUE()//打印队内所有元素
{
if (Q->end == Q->head)
{
printf("空队!!!\n");
return;
}
printf("当前队内的元素为:");
for (int i = Q->head; i != Q->end;)
{
printf("%d ", Q->base[i]);
i = (i + 1) % INITIAL_QUEUE_SIZE;
}
printf("\n");
}
void GET_OFF_QUEUE()//出队函数
{
if (Q->end == Q->head)//判断是否空队
{
printf("空队!!!\n");
return;
}
Q->head = (Q->head+1) % INITIAL_QUEUE_SIZE;
}
void GET_HEAD_OF_QUEUE()//获取队头元素
{
printf("当前队头元素为:%d\n", Q->base[Q->head]);
}
void NUM_OF_QUEUE()
{
printf("当前队内的元素个数为:%d\n", Q->end - Q->head);
}
void CLEAR_QUEUE()
{
Q->end = Q->head = 0;
}
void DISTORY()
{
free(Q->base);
Q->end = Q->head = 0;
Q->base = NULL;
}
int main()
{
INITIAL_QUEUE();//调用初始化函数
for (int i = 1; i <= 10; i++)
ENTER_QUEUE(i);
SHOW_QUEUE();
GET_OFF_QUEUE();
SHOW_QUEUE();
ENTER_QUEUE(0);
SHOW_QUEUE();
return 0;
}
四.总结
我们发现,在上面的运行及结果,本来在普通的顺序队列中出现虚假满状态存放不下数据的情况,在循环队列中得到了解决。本来已经存满,我们将1出队之后又将0存放了进去。
如有问题,还请指正!!!