双端队列
双端队列是一种特殊的队列,可以在队列的两头进行插入删除操作.依旧和队列一样遵循先进先出的逻辑.
顺序储存结构
顺序储存依旧是基于线性表实现,我们把队列放在一块连续的内存上.为了减少内存的浪费同时又要满足双端操作,我们必须把队列循环起来,用的是循环队列中的数学方法.
#include<stdio.h>
#include<stdlib.h>
#define maxx 5
//l指向元素真正的位置,r指向元素的后一个位置
typedef struct {
int* data;
int l, r;
int sum;
}Deque;
Deque InitDeque() {
Deque q;
q.data = (int*)malloc(sizeof(int) * maxx);
if (q.data == NULL) {
printf_s("内存申请失败\n");
return q;
}
q.l = q.r = 0;
q.sum = 0;
return q;
}
void LInsert(Deque* q, int k) {
if (q->sum == maxx) {
printf_s("队列已满无法入队\n");
return;
}
q->l = (q->l - 1 + maxx) % maxx;
q->data[q->l] = k;
q->sum++;
return;
}
void RInsert(Deque* q, int k) {
if (q->sum == maxx) {
printf_s("队列已满无法入队\n");
return;
}
q->data[q->r] = k;
q->r = (q->r + 1) % maxx;
q->sum++;
return;
}
void LDelete(Deque* q) {
if (q->sum == 0) {
printf_s("队列空无法删除\n");
return;
}
q->l = (q->l + 1) % maxx;
q->sum--;
return;
}
void RDelete(Deque* q) {
if (q->sum == 0) {
printf_s("队列空无法删除\n");
return;
}
q->r = (q->r - 1 + maxx) % maxx;
q->sum--;
return;
}
int main()
{
Deque q = InitDeque();
LInsert(&q, 1);
LInsert(&q, 2);
LInsert(&q, 3);
RInsert(&q, 6);
RInsert(&q, 7);
RDelete(&q);
RDelete(&q);
RDelete(&q);
LDelete(&q);
LDelete(&q);
LDelete(&q);
return 0;
}
结构体来封装这个双端队列,其中包含一块连续的内存来储存数据,还有两个"指针",左右指针本质上并不是指针而是int类型的下标.为了后续的判满操作,我们再封装一个sum计数器进去.
1.初始化双端队列,首先需要用malloc给它申请内存然后进行判空.最开始要把左右指针全部归零,同时计数器也得初始为零.
2.入队操作分为左端入队还有右端入队.这里我们先讨论一下左右指针的含义,左右指针都可以有两个含义:1.指向队首/队尾元素,2.指向队首元素的前一个位置/队尾元素的后一个位置.这两种情况两两搭配就有了四种可能.但是我们进行模拟的话如果都取用含义1,左右端分别入队一个元素后队列中间的内存就空了没有储存数据.如果都取用含义2,会导致后入队的元素覆盖第一次入队的元素.所以这里我们左右指针分别选用不同的含义.l指向元素真正的位置,r指向元素的后一个位置.那么左端入队我们应该先判满然后让l指针先移动再更新数据域和计数器,右端入队判满后先更新数据域和计数器再移动指针.再来讨论移动指针的方法,为了让队列循环起来我们还是选用取模这个数学方法.左端入队l指针应该减小,但是为了避免负数取模的情况我们加上一个maxx就可以解决.右端入队r指针会增大,直接加一对maxx取模即可.
3.出队操作也分为左端出队和右端出队,我们只把左右指针之间的元素视为这个队列,所以出队的内存我们可以不用释放,等下一次入队就可以自动覆盖,所以我们只需要对指针进行移动就可以了.左端出队l指针会增大直接加一取模即可,而右端出队r会减小也得加上maxx后再进行取模.
双端队列的易错点在于左右指针的含义,必须得弄清楚为什么要这样定义他的含义.
链式储存结构
#include<stdio.h>
#include<stdlib.h>
//l指向元素真正的位置,r指向元素的后一个位置
typedef struct Node{
int data;
struct Node* next;
struct Node* pre;
}DNode;
typedef struct {
DNode* l;
DNode* r;
}Deque;
Deque InitDeque() {
Deque q;
DNode* head = (DNode*)malloc(sizeof(DNode));
if (head == NULL) {
printf_s("内存申请失败\n");
return q;
}
head->next = NULL;
head->pre = NULL;
q.l = head;
q.r = head;
return q;
}
void LInsert(Deque* q, int k) {
DNode* d = (DNode*)malloc(sizeof(DNode));
if (d == NULL) {
printf_s("内存申请失败\n");
return;
}
q->l->pre = d;
d->next = q->l;
q->l = d;
d->pre = NULL;
d->data = k;
return;
}
void RInsert(Deque* q, int k) {
DNode* d = (DNode*)malloc(sizeof(DNode));
if (d == NULL) {
printf_s("内存申请失败\n");
return;
}
q->r->data = k;
q->r->next = d;
d->pre = q->r;
d->next = NULL;
q->r = d;
return;
}
void LDelete(Deque* q) {
if (q->l == q->r) {
printf_s("队列空无法删除\n");
return;
}
DNode* t = q->l;
q->l = t->next;
q->l->pre = NULL;
free(t);
t = NULL;
return;
}
void RDelete(Deque* q) {
if (q->l->next == NULL) {
printf_s("队列空无法删除\n");
return;
}
DNode* d = q->r;
q->r = d->pre;
q->r->next = NULL;
free(d);
d = NULL;
return;
}
int main()
{
Deque q = InitDeque();
LInsert(&q, 1);
LInsert(&q, 2);
LInsert(&q, 3);
RInsert(&q, 6);
RInsert(&q, 7);
RDelete(&q);
RDelete(&q);
RDelete(&q);
LDelete(&q);
LDelete(&q);
LDelete(&q);
return 0;
}
链式的储存结构我们还是基于链表来操作.先用一个结构体来封装队列中的节点,其中包含数据域以及前驱地址和后继地址,因为我们是双端操作所以得利用前驱指针来对节点进行删除或者入队.而双端队列的结构体只需用包含两个指针,在链式结构中这两个指针本质确实是指针.
1.初始化需要声明一个头节点,判空后把头节点的前驱和后继都要置空,并把左右指针都指向头节点.
2.入队操作同样也需要考虑这两个指针的含义,如果两个指针都是指向队尾或队首的元素那么几次入队操作后头节点处会储存一个脏数据,如果都是指向下一个数据那么头节点的数据会被覆盖一次.所以我们同样选择不同的含义,这样可以保证队列的完整和逻辑正确.这里我们依然选择l指向元素真正的位置,r指向元素的后一个位置.左端入队我们先malloc一个新的节点d,判空后把l指针的前驱指向d,d的后继指向l指针,再将d的前驱置空,更新l指针到节点d处,最后更新d的数据域即可.右端入队malloc节点d后我们应该先更新数据域再对节点进行操作,让r的后继指向d,让d的前驱指向r指针,再把d的后继置空,最后更新r指针到d位置处.
3.删除操作和顺序结构有所不同我们需要对删除的节点进行释放内存的操作.判空的方法很简单只用判断左右指针是否相同即可.左端出队我们用节点t记录要删除的节点,先移动左指针,再把左指针的前驱置空,最后释放t节点的内存置空即可.右端出队,同样用节点t记录,然后先移动右指针再把右指针的后继置空,最后释放t节点内存置空即可.
链式储存如果已经牢牢掌握了链表的操作那么会很好理解.