《画解数据结构》十张动图,画解双端队列

+ - [1)动画演示](#1_307)
	- [2)源码详解](#2_310)
+ [5、队尾入队](#5_317)
+ - [1)动画演示](#1_318)
	- [2)源码详解](#2_322)
+ [6、出队操作](#6_328)
+ [7、队首出队](#7_354)
+ - [1)动画演示](#1_355)
	- [2)源码详解](#2_359)
+ [8、队尾出队](#8_365)
+ - [1)动画演示](#1_366)
	- [2)源码详解](#2_370)
+ [9、清空队列](#9_376)
+ [10、只读接口](#10_386)
+ [11、双端队列的链表实现源码](#11_406)

一、概念

1、双端队列的定义

双端队列 是一种具有 队列 的性质的数据结构,是我们常说的 dequedouble-ended queue),是一种限定 插入删除 操作在表的两端进行的线性表。这两端分别被称为 队首队尾

1)模拟栈

双端队列 可以用来在一端进行 插入删除,从而实现 的功能。如图所示,代表的是队首固定,队尾循环进行 插入和删除 操作,从而模拟栈的 入栈出栈 的过程。

有关栈的更多内容,可以参考作者的另一篇文章:❤️《画解数据结构》九个动图,画解栈❤️

2)模拟队列

双端队列 也可以用限定只在一端 插入,另一删除,从而实现 队列 的功能。如图所示,代表的是队尾进行 插入,队首进行 插入 ,从而模拟 FIFO 队列 的 入队出队 的过程。

  有关 FIFO队列 的更多内容,可以参考作者的另一篇文章:❤️《画解数据结构》九张动图,画解队列❤️

3)输出受限队列

还可以实现 输出受限 的双端队列,即一个端点允许 插入和删除,另一个端点只允许 插入 的双端队列。

4)输入受限队列

也可以实现 输入受限 的双端队列,即一个端点允许 插入和删除,另一个端点只允许 删除 的双端队列。这种结构,我们一般可以它来实现 单调队列
  有关 单调队列 相关内容,下周会在 《夜深人静写算法》 专栏进行更新。

2、队首

双端队列的一端被称为 队首,如下图所示:

3、队尾

双端队列的另一端被称为 队尾,如下图所示:

二、接口

1、可写接口

1)队首入队

队列的插入操作,叫做 入队
  队首入队 就是将 数据元素队首 进行插入的过程。如图所示,表示的是在队首 插入 一个蓝色数据的过程:

2)队尾入队

队尾入队 就是将 数据元素队尾 进行插入的过程。如图所示,表示的是在队尾 插入 一个紫色数据的过程:

3)队首出队

队列的删除操作,叫做 出队
  队首出队 是将 队首 元素进行删除的过程,如图所示,表示的是在队首 删除 一个蓝色数据的过程:

4)队尾出队

队尾出队 是将 队尾 元素进行删除的过程,如图所示,表示的是在队尾 删除 一个紫色数据的过程:

5)清空队列

队列的清空操作,就是一直 出队,直到队列为空的过程,当 队首队尾 正好错开一个位置时,就代表队尾为空了,如图所示,细心的读者会发现,队尾队首 错开了一个位置:

2、只读接口

1)获取队列元素个数

队列元素个数一般用一个额外变量存储,入队 时加一,出队 时减一。这样获取队列元素的时候就不需要遍历整个队列。通过

O

(

1

)

O(1)

O(1) 的时间复杂度获取队列元素个数。

2)判空

当队列元素个数为零时,就是一个 空队空队 不允许 出队 操作。

3)获取队首元素

队首指针 指向的数据被称为 队首元素,可以通过

O

(

1

)

O(1)

O(1) 的时间复杂度来获取。

4)获取队尾元素

队尾指针 指向的数据被称为 队尾元素,可以通过

O

(

1

)

O(1)

O(1) 的时间复杂度来获取。

三、双端队列的顺序表实现

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)
};

  • (

1

)

(1)

(1) 用DataType这个宏定义来统一代表队列中数据的类型,这里将它定义为整型,根据需要可以定义成其它类型,例如浮点型、字符型、结构体 等等;

  • (

2

)

(2)

(2) maxn代表我们定义的队列的最大元素个数的一半,因为对于数组来说,不能有负数下标,所以初始情况是从数组的中心开始往两边进行插入删除,所以实际的数组长度为maxn的两倍;

  • (

3

)

(3)

(3) Queue就是我们接下来会用到的 双端队列结构体

  • (

4

)

(4)

(4) DataType data[maxn<<1]作为 队列元素 的存储方式,即 数组,其中元素个数为maxn<<1,等价于maxn*2,数据类型为DataType,可以自行定制;

  • (

5

)

(5)

(5) head队首指针tail队尾指针head - tail == 1代表空队;当队列非空时,data[head]代表了队首元素,data[tail]代表了队尾元素;

2、队首入队

1)动画演示

如图所示,蓝色元素 为新插入队首的数据,执行前,队首指针减一,然后在对应位置插入数据。具体来看下代码实现。

2)源码详解

队首入队 操作,只需要两行代码就能实现,代码实现如下:

void QueueEnqueueFront(struct Queue \*que, DataType dt) {
    --que->head;                             // (1)
    que->data[ que->head ] = dt;             // (2)
}

  • (

1

)

(1)

(1) 队首指针左移一个位置(逻辑上是减一);

  • (

2

)

(2)

(2) 将需要插入的数据放到 队首指针 对应位置上即完成了入队操作。

注意,这个接口在调用前,需要保证 队首指针 大于 零,否则就会使数组下标变负数,导致数组下标越界。

3、队尾入队

1)动画演示

如图所示,紫色元素 为新插入队尾的数据,执行前,队尾指针加一,然后在对应位置插入数据。具体来看下代码实现。

2)源码详解

队尾入队 操作,也只需要两行代码就能实现,代码实现如下:

void QueueEnqueueRear(struct Queue \*que, DataType dt) {
    ++que->tail;                             // (1)
    que->data[que->tail] = dt;               // (2)
}

  • (

1

)

(1)

(1) 队尾指针右移一个位置(逻辑上是加一);

  • (

2

)

(2)

(2) 将需要插入的数据放到 队尾指针 对应位置上即完成了入队操作。

注意,这个接口在调用前,需要保证 队尾指针 小于 maxn*2,否则就会导致数组下标越界。

4、队首出队

1)动画演示

如图所示,蓝色元素 为原先的 队首元素,执行 出队 操作以后,红色元素 成为当前的 队首元素,出队操作只是将 队首指针 加一。由于是顺序表实现,队首元素前面的那些元素已经变成无效的了,具体来看下代码实现。

2)源码详解

队首出队 操作,只需要简单的改变,将 队首指针 加一 即可,原先的 队首元素 不需要理会,代码实现如下:

void QueueDequeueFront(struct Queue\* que) {
    ++que->head;
}

5、队尾出队

1)动画演示

如图所示,紫色元素 为原先的 队尾元素,执行 出队 操作以后,绿色元素 成为当前的 队尾元素,出队操作只是将 队尾指针 减一。由于是顺序表实现,队尾元素 后面的那些元素已经变成无效的了,具体来看下代码实现。

2)源码详解

队尾出队 操作,只需要简单的改变,将 队尾指针 减一 即可,原先的 队尾元素 不需要理会,代码实现如下:

void QueueDequeueRear(struct Queue\* que) {
    --que->tail;
}

6、清空队列

对于顺序表来说,清空队列的操作只需要将 队首指针 置为 maxn,而 队尾指针 置为 队首指针 减一 即可,数据不需要清理,下次继续 入队 的时候会将之前的内存重复利用。
  这里需要注意的是,顺序表的实际最大长度为maxn的两倍,为了满足 双端队列 能够在 两端 都进行 入队 这个性质,所以才把初始位置设置在了顺序表的中点,也就是maxn的位置。
  清空队列的操作,代码实现如下:

void QueueClear(struct Queue\* que) {
    que->head = maxn;
    que->tail = que->head - 1;
}

7、只读接口

只读接口包含:获取队首元素、获取队尾元素、获取队列大小、队列的判空,实现如下:

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)
}

  • (

1

)

(1)

(1) que->head代表了 队首指针,即 队首下标,所以真正的 队首元素que->data[ que->head ]

  • (

2

)

(2)

(2) que->tail代表了 队尾指针,即 队尾下标,所以真正的 队尾元素que->data[ que->tail ]

  • (

3

)

(3)

(3) 当队列为空时,que->tail == que->head - 1。直观的感受下,入队 会把 队首指针队尾指针 的距离拉开,出队 会把 队首指针队尾指针 的距离拉近;所以,队列的元素个数就是两者差值加一。

  • (

4

)

(4)

(4) 当 队列元素 个数为 零 时,队列为空。

8、双端队列的顺序表实现源码

双端队列的顺序表实现的源码如下:

/\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* 顺序表 实现双端队列 \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*/
#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、数据结构定义

对于链表,在进行 双端队列的定义 之前,我们需要考虑以下几个点:
  1)队列数据的存储方式,以及队列数据的数据类型;
  2)队列的大小;
  3)队首指针;
  4)队尾指针;

  • 我们可以定义一个 双端队列结构体,C语言实现如下所示:
#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)
};

  • (

1

)

(1)

(1) 队列结点元素的 数据域,这里定义为整型;

  • (

2

)

(2)

(2) struct QueueNode;是对 链表结点 的声明;

  • (

3

)

(3)

(3) 定义链表结点,其中DataType data代表 数据域struct QueueNode *prev代表 前驱指针域struct QueueNode *next代表 后继指针域;注意,双端队列 需要用 双向链表 实现,单向链表 无法满足需求;

  • (

4

)

(4)

(4) head作为 队首指针tail作为 队尾指针

  • (

5

)

(5)

(5) 由于 求链表长度 的算法时间复杂度是

O

(

n

)

O(n)

O(n) 的, 所以我们需要记录一个size来代表现在队列中有多少元素。每次 入队size自增,出队size自减。这样在询问 队列 的大小的时候,就可以通过

O

(

1

)

O(1)

O(1) 的时间复杂度。

2、创建链表结点

在进行 入队 操作的时候,需要将数据转换成双向链表的结点,所以需要通过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;
}

  • (

1

)

(1)

(1) 将传入参数作为 数据域

  • (

2

)

(2)

(2) 将 前驱指针后继指针 都置为空,代表创建完毕后,这是一个孤立的双向链表结点;

3、入队操作

双端队列 的入队操作分为 队首入队队尾入队,我们将两种实现通过一个统一的内部接口_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)
}

  • (

1

)

(1)

(1) 创建一个 数据域dt的结点vtx

  • (

2

)

(2)

(2) 如果目前是一个空的双端队列,则将 队首指针队尾指针 都指向vtx

  • (

3

)

(3)

(3) 如果非空,则需要考虑是 队首入队 还是 队尾入队。用传参isFrontOrRear进行判断。

  • (

4

)

(

5

)

(4) - (5)

(4)−(5) 如果是 队首入队,则将vtx后继 指向 队首,并且将vtx作为新的 队首

  • (

6

)

(

7

)

(6) - (7)

(6)−(7) 如果是 队尾入队,则将vtx前驱 指向 队尾,并且将vtx作为新的 队尾

  • (

8

)

(8)

(8) 最后,无论是 队首入队 还是 队尾入队,队列的长度都增加一;

4、队首入队

1)动画演示

如图所示,head队首元素tail队尾元素vtx 为当前需要 入队 的元素,即图中的 橙色结点入队 操作完成后,队首元素 变为 vtx,即图中 绿色结点

2)源码详解
  • 队首入队 上文已经做了解释,代码实现如下:
void QueueEnqueueFront(struct Queue \*que, DataType dt) {
    \_QueueEnqueue(que, dt, 1);
}

5、队尾入队

1)动画演示

如图所示,head队首元素tail队尾元素vtx 为当前需要 入队 的元素,即图中的 橙色结点入队 操作完成后,队尾元素 变为 vtx,即图中 绿色结点

2)源码详解
void QueueEnqueueRear(struct Queue \*que, DataType dt) {
    \_QueueEnqueue(que, dt, 0);
}

6、出队操作

双端队列的出队操作分为 队首出队队尾出队,我们将两种实现通过一个统一的内部接口_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)
}

  • (

1

)

(1)

(1) 当只剩一个队列数据时,直接将 队首指针队尾指针 都置为空;

  • (

2

)

(2)

(2) 考虑是 队首出队 还是 队尾出队。用传参isFrontOrRear进行判断;

  • (

3

)

(3)

(3) 如果是 队首出队,则将 队首指针 置为原先队首的后继;

  • (

4

)

(4)

(4) 如果是 队尾出队,则将 队尾指针 置为原先队尾的前驱;

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值