直接跳到末尾 获取粉丝专属福利。
「 数据结构 」 和 「 算法 」 是密不可分的,两者往往是「 相辅相成 」的存在,所以,在学习 「 数据结构 」 的过程中,不免会遇到各种「 算法 」。
到底是先学 数据结构 ,还是先学 算法,我认为不必纠结这个问题,一定是一起学的。
数据结构 常用的操作一般为:「 增 」「 删 」「 改 」「 查 」。基本上所有的数据结构都是围绕这几个操作进行展开的。
那么这篇文章,作者将用 「 十张动图 」 来阐述一种 「 两端插入 」「 两端弹出 」 的数据结构
「 双端队列 」
让天下没有难学的算法
C语言免费动漫教程,和我一起打卡! 《光天化日学C语言》
入门级C语言真题汇总 《C语言入门100例》
几张动图学会一种数据结构 《画解数据结构》
组团学习,抱团生长 《算法入门指引》
竞赛选手金典图文教程 《夜深人静写算法》
双端队列可以用 顺序表 实现,也可以用 双向链表 实现,浓缩为以下两张图:
看不懂没有关系,我会把它拆开来一个一个讲,首先来看一下今天要学习的内容目录。
双端队列 是一种具有 队列 和 栈 的性质的数据结构,是我们常说的 deque(double-ended queue),是一种限定 插入 和 删除 操作在表的两端进行的线性表。这两端分别被称为 队首 和 队尾。
双端队列 可以用来在一端进行 插入 和 删除,从而实现 栈 的功能。如图所示,代表的是队首固定,队尾循环进行 插入和删除 操作,从而模拟栈的 入栈 和 出栈 的过程。
有关栈的更多内容,可以参考作者的另一篇文章:❤️《画解数据结构》九个动图,画解栈❤️。
双端队列 也可以用限定只在一端 插入,另一删除,从而实现 队列 的功能。如图所示,代表的是队尾进行 插入,队首进行 插入 ,从而模拟 FIFO 队列 的 入队 和 出队 的过程。
有关 FIFO队列 的更多内容,可以参考作者的另一篇文章:❤️《画解数据结构》九张动图,画解队列❤️。
还可以实现 输出受限 的双端队列,即一个端点允许 插入和删除,另一个端点只允许 插入 的双端队列。
也可以实现 输入受限 的双端队列,即一个端点允许 插入和删除,另一个端点只允许 删除 的双端队列。这种结构,我们一般可以它来实现 单调队列。
有关 单调队列 相关内容,下周会在 《夜深人静写算法》 专栏进行更新。
队列的插入操作,叫做 入队。
队首入队 就是将 数据元素 从 队首 进行插入的过程。如图所示,表示的是在队首 插入 一个蓝色数据的过程:
队尾入队 就是将 数据元素 从 队尾 进行插入的过程。如图所示,表示的是在队尾 插入 一个紫色数据的过程:
队列的删除操作,叫做 出队。
队首出队 是将 队首 元素进行删除的过程,如图所示,表示的是在队首 删除 一个蓝色数据的过程:
队尾出队 是将 队尾 元素进行删除的过程,如图所示,表示的是在队尾 删除 一个紫色数据的过程:
队列的清空操作,就是一直 出队,直到队列为空的过程,当 队首 和 队尾 正好错开一个位置时,就代表队尾为空了,如图所示,细心的读者会发现,队尾 和 队首 错开了一个位置:
队列元素个数一般用一个额外变量存储,入队 时加一,出队 时减一。这样获取队列元素的时候就不需要遍历整个队列。通过 O ( 1 ) O(1) O(1) 的时间复杂度获取队列元素个数。
当队列元素个数为零时,就是一个 空队,空队 不允许 出队 操作。
队首指针 指向的数据被称为 队首元素,可以通过 O ( 1 ) O(1) O(1) 的时间复杂度来获取。
队尾指针 指向的数据被称为 队尾元素,可以通过 O ( 1 ) O(1) O(1) 的时间复杂度来获取。
对于顺序表,在 C语言中 表现为 数组,在进行 双端队列的定义 之前,我们需要考虑以下几点:
1)队列数据的存储方式,以及队列数据的数据类型;
2)队列的大小;
3)队首指针;
4)队尾指针;
我们可以定义一个 双端队列 的 结构体,C语言实现如下所示:
#define DataType int // (1)
#define maxn 100005 // (2)
struct Queue {
// (3)
DataType data[maxn<<1]; // (4)
int head, tail; // (5)
};
DataType
这个宏定义来统一代表队列中数据的类型,这里将它定义为整型,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等;maxn
代表我们定义的队列的最大元素个数的一半,因为对于数组来说,不能有负数下标,所以初始情况是从数组的中心开始往两边进行插入删除,所以实际的数组长度为maxn
的两倍;Queue
就是我们接下来会用到的 双端队列 的 结构体;DataType data[maxn<<1]
作为 队列元素 的存储方式,即 数组,其中元素个数为maxn<<1
,等价于maxn*2
,数据类型为DataType
,可以自行定制;head
即 队首指针,tail
即 队尾指针,head - tail == 1
代表空队;当队列非空时,data[head]
代表了队首元素,data[tail]
代表了队尾元素;如图所示,蓝色元素 为新插入队首的数据,执行前,队首指针减一,然后在对应位置插入数据。具体来看下代码实现。
队首入队 操作,只需要两行代码就能实现,代码实现如下:
void QueueEnqueueFront(struct Queue *que, DataType dt) {
--que->head; // (1)
que->data[ que->head ] = dt; // (2)
}
注意,这个接口在调用前,需要保证 队首指针 大于 零,否则就会使数组下标变负数,导致数组下标越界。
如图所示,紫色元素 为新插入队尾的数据,执行前,队尾指针加一,然后在对应位置插入数据。具体来看下代码实现。
队尾入队 操作,也只需要两行代码就能实现,代码实现如下:
void QueueEnqueueRear(struct Queue *que, DataType dt) {
++que->tail; // (1)
que->data[que->tail] = dt; // (2)
}
注意,这个接口在调用前,需要保证 队尾指针 小于 maxn*2
,否则就会导致数组下标越界。
如图所示,蓝色元素 为原先的 队首元素,执行 出队 操作以后,红色元素 成为当前的 队首元素,出队操作只是将 队首指针 加一。由于是顺序表实现,队首元素前面的那些元素已经变成无效的了,具体来看下代码实现。
队首出队 操作,只需要简单的改变,将 队首指针 加一 即可,原先的 队首元素 不需要理会,代码实现如下:
void QueueDequeueFront(struct Queue* que) {
++que->head;
}
如图所示,紫色元素 为原先的 队尾元素,执行 出队 操作以后,绿色元素 成为当前的 队尾元素,出队操作只是将 队尾指针 减一。由于是顺序表实现,队尾元素 后面的那些元素已经变成无效的了,具体来看下代码实现。
队尾出队 操作,只需要简单的改变,将 队尾指针 减一 即可,原先的 队尾元素 不需要理会,代码实现如下:
void QueueDequeueRear(struct Queue* que) {
--que->tail;
}
对于顺序表来说,清空队列的操作只需要将 队首指针 置为 maxn
,而 队尾指针 置为 队首指针 减一 即可,数据不需要清理,下次继续 入队 的时候会将之前的内存重复利用。
这里需要注意的是,顺序表的实际最大长度为maxn
的两倍,为了满足 双端队列 能够在 两端 都进行 入队 这个性质,所以才把初始位置设置在了顺序表的中点,也就是maxn
的位置。
清空队列的操作,代码实现如下:
void QueueClear(struct Queue* que) {
que->head = maxn;
que->tail = que->head - 1;
}
只读接口包含:获取队首元素、获取队尾元素、获取队列大小、队列的判空,实现如下:
DataType QueueGetFront(struct Queue* que) {
return que->data[ que->head ]; // (1)
}
DataType QueueGetRear(struct Queue* que) {
return que->data[ que->tail ]; // (2)
}
int QueueGetSize(struct Queue* que) {
return que->tail - que->head + 1; // (3)
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que); // (4)
}
que->head
代表了 队首指针,即 队首下标,所以真正的 队首元素 是 que->data[ que->head ]
;que->tail
代表了 队尾指针,即 队尾下标,所以真正的 队尾元素 是 que->data[ que->tail ]
;que->tail == que->head - 1
。直观的感受下,入队 会把 队首指针 和 队尾指针 的距离拉开,出队 会把 队首指针 和 队尾指针 的距离拉近;所以,队列的元素个数就是两者差值加一。双端队列的顺序表实现的源码如下:
/**************************** 顺序表 实现双端队列 ****************************/
#define DataType int
#define maxn 100005
struct Queue {
DataType data[maxn<<1];
int head, tail;
};
void QueueClear(struct Queue* que) {
que->head = maxn;
que->tail = que->head - 1;
}
void QueueEnqueueFront(struct Queue *que, DataType dt) {
que->data[ --que->head ] = dt;
}
void QueueEnqueueRear(struct Queue *que, DataType dt) {
que->data[ ++que->tail ] = dt;
}
void QueueDequeueFront(struct Queue* que) {
++que->head;
}
void QueueDequeueRear(struct Queue* que) {
--que->tail;
}
DataType QueueGetFront(struct Queue* que) {
return que->data[ que->head ];
}
DataType QueueGetRear(struct Queue* que) {
return que->data[ que->tail ];
}
int QueueGetSize(struct Queue* que) {
return que->tail - que->head + 1;
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que);
}
/**************************** 顺序表 实现双端队列 ****************************/
对于链表,在进行 双端队列的定义 之前,我们需要考虑以下几个点:
1)队列数据的存储方式,以及队列数据的数据类型;
2)队列的大小;
3)队首指针;
4)队尾指针;
#define DataType int // (1)
struct QueueNode; // (2)
struct QueueNode {
// (3)
DataType data;
struct QueueNode *prev;
struct QueueNode *next;
};
struct Queue {
struct QueueNode *head, *tail;// (4)
int size; // (5)
};
struct QueueNode;
是对 链表结点 的声明;DataType data
代表 数据域;struct QueueNode *prev
代表 前驱指针域;struct QueueNode *next
代表 后继指针域;注意,双端队列 需要用 双向链表 实现,单向链表 无法满足需求;head
作为 队首指针,tail
作为 队尾指针;size
来代表现在队列中有多少元素。每次 入队时size
自增,出队时size
自减。这样在询问 队列 的大小的时候,就可以通过 O ( 1 ) O(1) O(1) 的时间复杂度。 在进行 入队 操作的时候,需要将数据转换成双向链表的结点,所以需要通过malloc
分配结点的内存,实现如下:
struct QueueNode *QueueCreateNode(DataType dt) {
struct QueueNode *vtx = (struct QueueNode *) malloc( sizeof(struct QueueNode));
vtx->data = dt; // (1)
vtx->next = vtx->prev = NULL; // (2)
return vtx;
}
双端队列 的入队操作分为 队首入队 和 队尾入队,我们将两种实现通过一个统一的内部接口_QueueEnqueue
来实现,并且用一个标记isFrontOrRear
来表示是从 队首 进行入队的,还是从 队尾 进行入队的,C语言实现如下:
void _QueueEnqueue(struct Queue *que, DataType dt, int isFrontOrRear) {
struct QueueNode *vtx = QueueCreateNode(dt); // (1)
if(que->size == 0) {
que->head = que->tail = vtx; // (2)
}else {
if(isFrontOrRear) {
// (3)
vtx->next = que->head; // (4)
que->head->prev = vtx;
que->head = vtx; // (5)
}else {
que->tail->next = vtx; // (6)
vtx->prev = que->tail;
que->tail = vtx; // (7)
}
}
++que->size; // (8)
}
dt
的结点vtx
;vtx
;isFrontOrRear
进行判断。vtx
的 后继 指向 队首,并且将vtx
作为新的 队首;vtx
的 前驱 指向 队尾,并且将vtx
作为新的 队尾;如图所示,head 为 队首元素,tail 为 队尾元素,vtx 为当前需要 入队 的元素,即图中的 橙色结点。入队 操作完成后,队首元素 变为 vtx,即图中 绿色结点。
void QueueEnqueueFront(struct Queue *que, DataType dt) {
_QueueEnqueue(que, dt, 1);
}
如图所示,head 为 队首元素,tail 为 队尾元素,vtx 为当前需要 入队 的元素,即图中的 橙色结点。入队 操作完成后,队尾元素 变为 vtx,即图中 绿色结点。
void QueueEnqueueRear(struct Queue *que, DataType dt) {
_QueueEnqueue(que, dt, 0);
}
双端队列的出队操作分为 队首出队 和 队尾出队,我们将两种实现通过一个统一的内部接口_QueueDequeue
来实现,并且用一个标记isFrontOrRear
来表示是从 队首 进行出队的,还是从 队尾 进行出队的,C语言实现如下:
void _QueueDequeue(struct Queue *que, struct QueueNode *temp, int isFrontOrRear) {
if(que->size == 1) {
que->head = que->tail = NULL; // (1)
}else {
if(isFrontOrRear) {
// (2)
que->head = temp->next; // (3)
que->head->prev = NULL;
}else {
que->tail = temp->prev; // (4)
que->tail->next = NULL;
}
}
free(temp); // (5)
--que->size; // (6)
}
isFrontOrRear
进行判断;free
将出队的结点内存释放掉;如图所示,head 为 队首元素,tail 为 队尾元素,temp 为当前需要 出队 的元素,即图中的 橙色结点。出队 操作完成后,队首元素 变为之前 head 的 后继结点,即图中 绿色结点。
void QueueDequeueFront(struct Queue* que) {
_QueueDequeue(que, que->head, 1);
}
如图所示,head 为 队首元素,tail 为 队尾元素,temp 为当前需要 出队 的元素,即图中的 橙色结点。出队 操作完成后,队尾元素 变为之前 tail 的 前驱结点,即图中 绿色结点。
void QueueDequeueRear(struct Queue* que) {
_QueueDequeue(que, que->tail, 0);
}
对于链表而言,清空队列 的操作需要删除每个链表结点,代码实现如下:
void QueueClear(struct Queue* que) {
while(!QueueIsEmpty(que)) {
// (1)
QueueDequeueFront(que); // (2)
}
}
只读接口包含:获取队首元素、获取队尾元素、获取队列大小、队列的判空,实现如下:
DataType QueueGetFront(struct Queue* que) {
return que->head->data; // (1)
}
DataType QueueGetRear(struct Queue* que) {
return que->tail->data; // (2)
}
int QueueGetSize(struct Queue* que) {
return que->size; // (3)
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que);
}
que->head
作为 队首指针,它的 数据域 data
就是 队首元素的值,返回即可;que->tail
作为 队尾指针,它的 数据域 data
就是 队尾元素的值,返回即可;size
记录的是 队列元素 的个数;#include
/**************************** 链表 实现双端队列 ****************************/
#define DataType int
struct QueueNode;
struct QueueNode {
DataType data;
struct QueueNode *prev;
struct QueueNode *next;
};
struct Queue {
struct QueueNode *head, *tail;
int size;
};
struct QueueNode *QueueCreateNode(DataType dt) {
struct QueueNode *vtx = (struct QueueNode *) malloc( sizeof(struct QueueNode));
vtx->data = dt;
vtx->next = vtx->prev = NULL;
return vtx;
}
void _QueueEnqueue(struct Queue *que, DataType dt, int isFrontOrRear) {
struct QueueNode *vtx = QueueCreateNode(dt);
if(que->size == 0) {
que->head = que->tail = vtx;
}else {
if(isFrontOrRear) {
vtx->next = que->head;
que->head->prev = vtx;
que->head = vtx;
}else {
que->tail->next = vtx;
vtx->prev = que->tail;
que->tail = vtx;
}
}
++que->size;
}
void _QueueDequeue(struct Queue *que, struct QueueNode *temp, int isFrontOrRear) {
if(que->size == 1) {
que->head = que->tail = NULL;
}else {
if(isFrontOrRear) {
que->head = temp->next;
que->head->prev = NULL;
}else {
que->tail = temp->prev;
que->tail->next = NULL;
}
}
free(temp);
--que->size;
}
void QueueClear(struct Queue* que) {
que->head = que->tail = NULL;
que->size = 0;
}
void QueueEnqueueFront(struct Queue *que, DataType dt) {
_QueueEnqueue(que, dt, 1);
}
void QueueEnqueueRear(struct Queue *que, DataType dt) {
_QueueEnqueue(que, dt, 0);
}
void QueueDequeueFront(struct Queue* que) {
_QueueDequeue(que, que->head, 1);
}
void QueueDequeueRear(struct Queue* que) {
_QueueDequeue(que, que->tail, 0);
}
DataType QueueGetFront(struct Queue* que) {
return que->head->data;
}
DataType QueueGetRear(struct Queue* que) {
return que->tail->data;
}
int QueueGetSize(struct Queue* que) {
return que->size;
}
int QueueIsEmpty(struct Queue* que) {
return !QueueGetSize(que);
}
/**************************** 链表 实现双端队列 ****************************/
关于 「 双端队列 」 的内容到这里就结束了。
如果还有不懂的问题,可以通过 「 电脑版主页 」找到作者的「 联系方式 」 ,线上沟通交流。
有关《画解数据结构》 的源码均开源,链接如下:《画解数据结构》
相信看我文章的大多数都是「 大学生 」,能上大学的都是「 精英 」,那么我们自然要「 精益求精 」,如果你还是「 大一 」,那么太好了,你拥有大把时间,当然你可以选择「 刷剧 」,然而,「 学好算法 」,三年后的你自然「 不能同日而语 」。
那么这里,我整理了「 几十个基础算法 」 的分类,点击开启:
为了让这件事情变得有趣,以及「 照顾初学者 」,目前题目只开放最简单的算法 「 枚举系列 」 (包括:线性枚举、双指针、前缀和、二分枚举、三分枚举),当有 一半成员刷完 「 枚举系列 」 的所有题以后,会开放下个章节,等这套题全部刷完,你还在群里,那么你就会成为「 夜深人静写算法 」专家团 的一员。
不要小看这个专家团,三年之后,你将会是别人 望尘莫及 的存在。如果要加入,可以联系我,考虑到大家都是学生, 没有「 主要经济来源 」,在你成为神的路上,「 不会索取任何 」。
让天下没有难学的算法
C语言免费动漫教程,和我一起打卡! 《光天化日学C语言》
入门级C语言真题汇总 《C语言入门100例》
几张动图学会一种数据结构 《画解数据结构》
组团学习,抱团生长 《算法入门指引》
竞赛选手金典图文教程 《夜深人静写算法》
语言入门:《光天化日学C语言》(示例代码)
语言训练:《C语言入门100例》试用版
数据结构:《画解数据结构》源码
算法入门:《算法入门》指引
算法进阶:《夜深人静写算法》算法模板
验证码 可通过搜索下方 公众号 获取。