双向队列的概念
在队列中,我们仅能删除头部元素或在尾部添加元素。如图 5-7 所示,双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
对于双向队列而言,头部和尾部都可以执行入队和出队操作。换句话说,双向队列需要实现另一个对称方向的操作。为此,我们采用“双向链表”作为双向队列的底层数据结构。
如图所示,我们将双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。
代码演示
下面为大家展示链表实现双向队列的代码
首先准备好头函数:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int QDataType;
typedef struct DuListNode
{
QDataType data;
struct Node* prev;
struct Node* next;
}DuListNode;
typedef struct Deque
{
size_t size;
DuListNode* front;
DuListNode* rear;
}Deque;
void DequeInit(Deque* d);//初始化
bool DequeEmpty(Deque* d);//判断是否为空
QDataType DequeFront(Deque* d);//获取队头元素
QDataType DequeBack(Deque* d);//获取队尾元素
size_t DequeSize(Deque* d);//获取队列长度
void DequeFrontPush(Deque* d, QDataType x);//队首入队
void DequeRearPush(Deque* d, QDataType x);//队尾入队
void DequeFrontPop(Deque* d);//队首出队
void DequeRearPop(Deque* d);//队尾出队
void DequePrint(Deque* d);//打印队列元素
void DequeDestroy(Deque* d);//销毁队列
完整代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include"wknb.h"
// 初始化双端队列
void DequeInit(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
d->front = NULL; // 队头指针初始化为空
d->rear = NULL; // 队尾指针初始化为空
d->size = 0; // 队列大小初始化为0
}
// 判断队列是否为空
bool DequeEmpty(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
return (d->front == NULL) && (d->rear == NULL); // 若队头和队尾都为空,则队列为空
}
// 获取队头元素
QDataType DequeFront(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
assert(!DequeEmpty(d)); // 确保队列不为空
return d->front->data; // 返回队头元素
}
// 获取队尾元素
QDataType DequeBack(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
assert(!DequeEmpty(d)); // 确保队列不为空
return d->rear->data; // 返回队尾元素
}
// 获取队列的长度
size_t DequeSize(Deque* d)
{
return d->size; // 返回队列中元素的个数
}
// 队首插入元素
void DequeFrontPush(Deque* d, QDataType x)
{
assert(d); // 确保传入的队列指针不为空
// 创建一个新的节点
DuListNode* newnode = (DuListNode*)malloc(sizeof(DuListNode));
if (newnode == NULL)
{
perror("malloc fail"); // 如果内存分配失败,输出错误信息
return;
}
newnode->data = x; // 新节点的数据为传入的元素
newnode->next = NULL; // 初始化next为NULL
newnode->prev = NULL; // 初始化prev为NULL
// 如果队列为空,则新节点成为队头和队尾
if (d->front == NULL)
{
d->front = d->rear = newnode;
}
else
{
// 否则将新节点插入到队头
d->front->prev = newnode;
newnode->next = d->front;
d->front = newnode;
}
d->size++; // 增加队列大小
}
// 队尾插入元素
void DequeRearPush(Deque* d, QDataType x)
{
assert(d); // 确保传入的队列指针不为空
// 创建一个新的节点
DuListNode* newnode = (DuListNode*)malloc(sizeof(DuListNode));
if (newnode == NULL)
{
perror("malloc fail"); // 如果内存分配失败,输出错误信息
return;
}
newnode->data = x; // 新节点的数据为传入的元素
newnode->next = NULL; // 初始化next为NULL
newnode->prev = NULL; // 初始化prev为NULL
// 如果队列为空,则新节点成为队头和队尾
if (d->front == NULL)
{
d->front = d->rear = newnode;
}
else
{
// 否则将新节点插入到队尾
d->rear->next = newnode;
newnode->prev = d->rear;
d->rear = newnode;
}
d->size++; // 增加队列大小
}
// 队首删除元素
void DequeFrontPop(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
assert(!DequeEmpty(d)); // 确保队列不为空
// 只有一个节点的情况
if (d->front == d->rear)
{
free(d->front); // 释放队头节点
d->front = d->rear = NULL; // 队列为空
}
// 有多个节点的情况
else
{
DuListNode* next = d->front->next; // 获取队头的下一个节点
next->prev = NULL; // 更新下一个节点的prev指针
d->front->next = NULL; // 断开队头节点的next指针
free(d->front); // 释放队头节点
d->front = next; // 更新队头指针
}
d->size--; // 减少队列大小
}
// 队尾删除元素
void DequeRearPop(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
assert(!DequeEmpty(d)); // 确保队列不为空
// 只有一个节点的情况
if (d->front == d->rear)
{
free(d->rear); // 释放队尾节点
d->front = d->rear = NULL; // 队列为空
}
// 有多个节点的情况
else
{
DuListNode* prev = d->rear->prev; // 获取队尾的上一个节点
prev->next = NULL; // 更新上一个节点的next指针
d->rear->prev = NULL; // 断开队尾节点的prev指针
free(d->rear); // 释放队尾节点
d->rear = prev; // 更新队尾指针
}
d->size--; // 减少队列大小
}
// 打印队列的所有元素
void DequePrint(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
DuListNode* cur = d->front; // 从队头开始
DuListNode* tail = d->rear; // 队尾节点
printf("队头:");
// 遍历队列并打印元素
while (cur != tail->next)
{
printf("%d<=>", cur->data); // 打印节点数据
cur = cur->next; // 移动到下一个节点
}
printf("队尾\n"); // 打印队尾标识
}
// 销毁队列,释放所有节点
void DequeDestroy(Deque* d)
{
assert(d); // 确保传入的队列指针不为空
DuListNode* cur = d->front;
while (cur)
{
DuListNode* del = cur; // 获取当前节点
cur = cur->next; // 移动到下一个节点
free(del); // 释放当前节点
}
d->front = d->rear = NULL; // 队列为空
}
写一个主函数给大家演示各项操作:
// 主函数,演示队列操作
int main() {
DuListNode* Q = (DuListNode*)malloc(sizeof(DuListNode)); // 创建队列指针
DequeInit(Q); // 初始化队列
// 向队列中添加元素
DequeFrontPush(Q, 1);
DequeFrontPush(Q, 3);
DequeFrontPush(Q, 5);
DequeFrontPush(Q, 7);
DequeRearPush(Q, 2);
DequePrint(Q); // 打印队列内容
}
运行结果如图所示:
双向队列的应用
双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。
我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push
到栈中,然后通过 pop
实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 50 步)。当栈的长度超过 50 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。