第三章 栈和队列
大家好,我叫亓官劼(qí guān jié ),在CSDN中记录学习的点滴历程,时光荏苒,未来可期,加油~博客地址为:亓官劼的博客
本文原创为亓官劼,请大家支持原创,部分平台一直在盗取博主的文章!!!
博主目前仅在CSDN中写博客,唯一博客更新的地址为:亓官劼的博客
3.2 队列
3.2.1 队列的基本概念
队列(Queue),是一种操作受限的线性表,只允许在表的一端进行插入,另一端进行删除。是一种先进先出的线性表(FIFO)。
入队:向队列中插入元素
出队:删除元素
队头(front):允许删除的一端。(也叫队首,head)
队尾(rear):运行插入的一端。(也叫tail)
队列常见的基本操作有:
- InitQueue(&Q); 初始化队列,构造一个空队列Q。
- QueueEmpty(Q); 判空操作
- EnQueue(&Q,x); 入队操作
- DeQueue(&Q,&x); 出队操作
- GetHead(Q,&x); 取队头
3.2.2 队列的顺序存储结构
队列的顺序存储实现是指分配一块连续的存储单元存放队列中的元素,并附设两个指针front和rear分别指向对头和队尾。设队头指针指向队头元素,队尾指针指向队尾元素的下一个位置。(这种设置方法,一个元素入队出队之后,之前的位置就会被浪费。
)
队列的顺序存储类型描述为:
# define MaxSize 50
typedef struct {
ElemType data[MaxSize]; // 存放数据
int front,rear;
}SqQueue;
初始状态(判空条件):Q.front == Q.rear == 0
(队头指针指向队头元素,队尾指针指向队尾元素的下一个元素。)
入队操作:队不满时,先送值到队尾元素,然后队尾指针+1
出队操作:队不空时,先取对头元素值,然后队头元素指针+1
这种存储方法,当一直进队出队时,队头指针和队尾指针会一直后移,造成假溢出。
3.2.3 循环队列
顺序队列的缺点即容易产生溢出,因此这里我们引入了循环队列。我们将顺序队列想象为一个环形结构,把存储队列元素的一维存储空间在逻辑上视为一个环。当队首指针Q.front = MaxSize-1
时,再次后移到达0
的位置,这里使用%取模
操作进行实现。
初始化:Q.front = Q.rear = 0;
队头元素+1: Q.front = (Q.front + 1)%MaxSize;
队尾元素+1:Q.rear = (Q.rear + 1)%MaxSize;
队列长度:(Q.rear - Q.front + MaxSize) % MaxSize
此时入队和出队时,队头队尾指针顺时针进1。
此时循环队列的判空条件为:Q.front == Q.rear
。但是如果队满时,我们的队尾指针会再次追上队头指针,为了区分队空和队满的情况,有3中处理方式。
(1)牺牲一个单元来区分队空和队满,入队时少用一个单元,队满条件为(Q.rear + 1) % MaxSize == Q.front
,这种方式是较为普遍的做法。此时约定“队头指针在队尾指针的下一个位置时为队满”
队空条件:Q.front == Q.rear
队满条件:(Q.rear + 1) % MaxSize == Q.front
队长:(Q.rear - Q.front + MaxSize) % MaxSize
(2)存储类型中增设表示元素个数的数据项。
队空条件:Q.size == 0
队满条件:Q.size == MaxSize
队长:Q.size
(3)存储类型中增设tag数据项,区分栈满还是栈空。tag为0时表示上一个操作为出队,tag为1表示上一个操作为入队。
队空条件:tag == 0 && Q.front == Q.rear
队满条件:tag == 1 && Q.front == Q.rear
队长:(Q.rear - Q.front + MaxSize) % MaxSize
3.2.4 循环队列的基本操作
这里采用牺牲一个单元的方式来区分队空和队满。
(1)初始化
// 初始化
void InitQueue(SqQueue &Q) {
Q.rear = Q.front = 0;
}
(2)判空
// 判空
bool emptyQueue(SqQueue Q) {
if (Q.rear == Q.front)
return true;
else
return false;
}
(3)入队
// 入队
bool EnQueue(SqQueue& Q, ElemType x) {
// 入队操作,先判断是否栈满。
// 若满,则返回false
// 若未满,则先为队尾指针的位置赋值,然后队尾指针进1
if ((Q.rear + 1) % MaxSize == Q.front)
return false;
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;
return true;
}
(4)出队
// 出队
bool DeQueue(SqQueue& Q, ElemType &x) {
// 出队操作,先判断是否栈空
// 若空,则返回false
// 非空,则先为x进行赋值,然后队头元素进1
if (Q.rear == Q.front)
return false;
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}
3.2.5 队列的链式存储结构
队列的链式存储称为链队列,它实际上是一个同时有队头指针和队尾指针的单链表。头指针指向队头结点,尾指针指向队尾结点。(这里和顺序队列不同,顺序队列的尾指针指向队尾元素的下一个位置
)
队列的链式存储类型描述为:
typedef struct LinkNode{
ElemType data; // 存放数据
struct LinkNode* next;
}LinkNode;
typedef struct {
LinkNode* front, * rear;
}LinkQueue;
判空条件:Q.front == NULL && Q.rear == NULL
出队:先判断是否栈空,非空时,先取出队头元素,然后从链表中删除该结点,Q.front指向下一个结点。(当该删除的结点为最后一个结点时,Q.front = Q.rear = NULL)
入队:(链队不存在队满的情况)建立一个新结点,将新结点插入到链表尾部,队尾指针移动。
这里若使用不带头结点的链式队列在操作上会比较麻烦,因此通常将链式队列设置为一个带头结点的单链表,这样的好处是的插入和删除操作可以统一。
链式队列的基本操作
(1)初始化
// 初始化
void InitQueue(LinkQueue &Q) {
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;
}
(2)判空
// 判空
bool emptyQueue(LinkQueue Q) {
if (Q.rear == Q.rear)
return true;
else
return false;
}
(3)入队
// 入队
bool EnQueue(LinkQueue& Q, ElemType x) {
// 入队操作,链队不会队满。
// 建立一个新结点,将新结点插入到链表尾部,队尾指针移动。
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x;
s->next = NULL;
Q.rear->next = s;
Q.rear = s;
return true;
}
(4)出队
这里需要注意下原队列中仅有一个元素时的情况
// 出队
bool DeQueue(LinkQueue& Q, ElemType &x) {
// 出队操作,先判断是否栈空
// 若空,则返回false
// 非空,则先取出队头元素,然后从链表中删除该结点,Q.front指向下一个结点。
if (Q.rear == Q.front)
return false;
LinkNode* p = Q.front;
x = p->data;
Q.front->next = p->next;
if (Q.rear == p)// 原队列中仅有一个元素,删除后为空
Q.rear = Q.front;
free(p);
return true;
}
3.2.6 双端队列
双端队列是指允许两端都可以进行入队和出队操作的队列。其元素逻辑结构仍是线性结构,将队列的两端分别成为前端和后端,两端都可以进行出队和入队。
- 在双端队列进队时,前端进的元素在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。
- 在双端队列出队时,其阿无论前端还是后端出队,先出的元素排列在后出的元素的前面。
其他的几种双端队列有:
(1)输出受限的双端队列
允许在一端进行插入和删除,另一端只允许插入的双端队列称为输出受限的双端队列
(2)输入受限的双端队列
允许在一端进行插入和删除,另一端只允许输出的双端队列称为输人受限的双端队列