详解双端队列&单调队列

简介: 详解双端队列&单调队列

1. 双端队列

双端队列(Double-ended Queue),简称Deque,是一种具有特殊功能的线性数据结构。它支持从两端进行元素的插入和删除操作,因此可以在队列和栈之间灵活地切换操作。双端队列在编程中经常用于需要在队列和栈之间切换操作的场景,或者需要在任意一端高效地进行插入和删除操作的情况。

双端队列的基本概念包括以下几个方面:

  1. 插入和删除操作:双端队列允许从队列的前端和后端进行插入和删除操作。这意味着你可以在队列的头部和尾部插入元素,也可以从头部和尾部删除元素。
  2. 队列和栈操作:双端队列可以用于模拟队列和栈的操作。当你只从一端插入元素,从另一端删除元素时,它类似于队列。当你从同一端插入和删除元素时,它类似于栈。
  3. 动态调整大小:双端队列可以根据需要动态调整大小,以容纳更多或更少的元素。这可以帮助节省内存并提高性能。
  4. 实现方式:双端队列可以通过数组或链表来实现。数组实现可能需要在插入和删除元素时进行数据的移动,而链表实现则可以更轻松地在任意位置进行插入和删除。
    注:由于双端队列允许从队头和队尾插入和删除数据,因此为了提高插入和删除的效率,当用链表来实现时,应该用双向链表
  5. 常见操作:常见的双端队列操作包括:

在队列头部插入元素(push_front)

在队列尾部插入元素(push_back)

从队列头部删除元素(pop_front)

从队列尾部删除元素(pop_back)

获取队列头部元素(front)

获取队列尾部元素(back)

判断双端队列是否为空

获取双端队列中的元素数量

双端队列在编程中经常用于需要高效地在队列两端进行插入和删除操作的问题,如某些算法和数据结构的实现,以及一些具体的应用场景,如滑动窗口问题等。

下面我们用双向链表的方式来实现双端队列

1.1 常见操作

1.1.1 结构的定义

typedef struct DequeNode  //队列的节点
{
    struct DequeNode* next; //指向下一个
    struct DequeNode* prev; //指向前一个
    int data; //数据域
}DQNode;
typedef struct Deque
{
    DQNode* front;  //队头指针
    DQNode* tail; //队尾指针
}Deque;

1.1.2 初始化

void DequeInit(Deque* pq)   //双端队列初始化
{
    assert(pq);
    pq->front = pq->tail = NULL;
}

1.1.3 判空

bool DequeEmpty(Deque* pq)  //双端队列判空
{
    assert(pq);
    return pq->front == NULL;
}

1.1.3 在尾部插入元素

void DequePush_Back(Deque* pq, int val)
{
    assert(pq);
    //新建节点
    DQNode* newNode = (DQNode*)malloc(sizeof(DQNode));
    newNode->data = val;
    newNode->next = NULL;
    newNode->prev = NULL;
    if (DequeEmpty(pq)) //如果队列为空,那么队头指针和队尾指针同时指向newNode
    {
        pq->front = pq->tail = newNode;
    }
    else  //否则在队尾指针后插入
    {
        pq->tail->next = newNode;
        newNode->prev = pq->tail;
        pq->tail = newNode;
    }
}

1.1.4 在头部插入元素

void DequePush_Front(Deque* pq, int val) //队头入队
{
    assert(pq);
    //新建节点
    DQNode* newNode = (DQNode*)malloc(sizeof(DQNode));
    newNode->data = val;
    newNode->next = NULL;
    newNode->prev = NULL;
    if (DequeEmpty(pq)) //如果队列为空,那么队头指针和队尾指针同时指向newNode
    {
        pq->front = pq->tail = newNode;
    }
    else  //否则在队头指针前插入
    {
        newNode->next = pq->front;
        pq->front->prev = newNode;
        pq->front = newNode;
    }
}

1.1.5 在头部删除元素

void DequePopFront(Deque* pq)    //队头出队
{
    assert(pq);
    assert(!DequeEmpty(pq));  //队列不能为空
    DQNode* temp = pq->front; //保存队头元素
    pq->front = pq->front->next;  //队头指针指向下一个
    if (pq->front == NULL)  //如果新的队头为空,那么队尾也要置空
        pq->tail = NULL;
    else  //否则将队头指针的prev成员置空
        pq->front->prev = NULL;
    free(temp); //释放原头节点的内存
}

1.1.6 在尾部删除元素

void DequePopTail(Deque* pq) //队尾出队
{
    assert(pq);
    assert(!DequeEmpty(pq));  //队列不能为空
    DQNode* temp = pq->tail;  //保存队尾元素
    pq->tail = pq->tail->prev;  //队尾指针指向前一个
    if (pq->tail == NULL) //如果新的队尾为空,那么就将队头也置空
        pq->front = NULL;
    else  //否则将队尾指针的next成员置空
        pq->tail->next = NULL;
    free(temp);
    return ret;
}

1.1.7 返回队头/队尾元素

int DequeTail(Deque* pq)    //取队尾元素
{
    assert(pq);
    assert(!DequeEmpty(pq));
    return pq->tail->data;
}
int DequeFront(Deque* pq)   //取队头元素
{
    assert(pq);
    assert(!DequeEmpty(pq));
    return pq->front->data;
}

1.1.8 销毁队列

void DequeDestroy(Deque* pq)    //销毁双端队列
{
    while (!DequeEmpty(pq))
    {
        DQNode* temp = pq->front;
        pq->front = pq->front->next;
        free(temp);
    }
    free(pq);
}

1.2 单调队列

单调队列(Monotonic Queue),也称为单调栈(Monotonic Stack),是一种特殊的队列(或栈)数据结构,用于解决一些与元素的单调性相关的问题。它在处理连续元素的最值或者满足一定条件的子序列时非常有用,能够在一定程度上减少问题的时间复杂度。

单调队列的基本概念如下:

  1. 单调性:单调队列主要用于处理具有单调性质的问题,可以是单调非递增或单调非递减。也就是说,队列中的元素要么逐渐增大,要么逐渐减小。
  2. 基本操作:单调队列的主要操作是维护队列内元素的单调性。为了实现这一点,通常需要在队列的某一端插入元素,然后从队列的另一端删除不满足单调性的元素。这使得队列中的元素保持单调有序。
  3. 解决问题:单调队列通常用于解决需要快速获取滑动窗口内的最值、找到连续子数组的最值、或者处理其他与单调性相关的问题。通过单调队列,可以在O(N)的时间复杂度内解决这些问题,而暴力方法通常需要O(N^2)的时间复杂度。
  4. 应用场景:一些典型的应用场景包括:
  • 滑动窗口中的最值问题
  • 连续子数组的最值问题
  • 优先级队列的优化(如在 Dijkstra 算法中的应用)
  • 某些动态规划问题的优化
  1. 实现方式:单调队列可以使用双端队列(Deque)来实现,因为双端队列可以从队头和队尾进行插入和删除操作,适用于维护单调性。插入元素时,可以从队尾插入,并删除队列中小于当前元素的元素;删除元素时,从队头删除。

总之,单调队列是一种优雅而强大的数据结构,它可以在一些特定问题中有效地处理具有单调性质的元素,从而提高算法的效率。


先在我们通过一道具体的例题来对单调队列进行更深入的掌握:

1.2.1 队列的最大值

需要实现:

//队列结构
typedef struct {
} MaxQueue;
//初始化并返回队列指针
MaxQueue* maxQueueCreate() {
}
//获取当前队列最大值
int maxQueueMax_value(MaxQueue* obj) {
}
//队列尾入队
void maxQueuePush_back(MaxQueue* obj, int value) {
}
//队列头出队
int maxQueuePop_front(MaxQueue* obj) {
}
//销毁队列
void maxQueueFree(MaxQueue* obj) {
}
1.2.1.1 思路

如果我们采用暴力解法,那么每插入或者删除一个元素,找一次最大值的时间复杂度就是O(N),操作N次那么时间复杂度就是O(N2),显然效率不高,需要另寻他法。

这里我们就采用单调队列的方式来解决问题。那么具体该如何实现呢?

  1. 基于这个题目,我们有必要认清一点:当一个元素进入队列时,它前面所有比它小的元素都不会对答案产生影响。
    举个例子:

    由于数据是从队头出,因此只要数据1还在队列中,数据2也一定在队列中,故比2小的1便不会对答案产生影响
  2. 因此,我们就需要这样一个单调队列:每次从队尾插入数据val之前,都将小于val的数据从队列中删除,这样队列就只留下了对答案有影响的数据。这就等价于要求这个队列是单调递减的,即每个元素的前面都不会存在比他小的元素
    举个例子,如果对空队列插入89, 22, 69, 16这四个数据:

  3. 那么我们如何高效地实现单调递减的队列呢?我们知道这个单调队列从队头到队尾数据是递减的,因此队尾元素就是最小元素,所以在插入数据之val前,我们只需要通过循环不断从队尾删除小于val的数据就行。
  4. 需要注意:在维护单调队列时会删除对答案没有影响的元素,但这并不是题目要求需要删除的元素,因此我们还需要一个队列来记录所有插入的数据,从而确定front_pop正确的返回值。同时,如果pop的数据等于最大值,那么也要将保存最大值的队列的队头元素删除(单调队列的队头元素就是最大值)
1.2.1.2 实现代码
typedef struct DequeNode    //双向队列节点
{
    struct DequeNode* next;
    struct DequeNode* prev;
    int val;
}DQNode;
typedef struct Deque    //队头指针,队尾指针
{
    DQNode* front;
    DQNode* tail;
}Deque;
typedef struct {    //Q1用来记录最大值,Q2用来记录所有数据
    Deque* DQ1;
    Deque* DQ2;
} MaxQueue;
MaxQueue* maxQueueCreate() {
    MaxQueue* max = (MaxQueue*)malloc(sizeof(MaxQueue));
    max->DQ1 = (Deque*)malloc(sizeof(Deque));
    max->DQ2 = (Deque*)malloc(sizeof(Deque));
    max->DQ1->tail = max->DQ1->front = NULL;
    max->DQ2->tail = max->DQ2->front = NULL;
    return max;
}
int maxQueueMax_value(MaxQueue* obj) {
    if (obj->DQ1->front == NULL)
        return -1;
    return obj->DQ1->front->val;
}
void maxQueuePush_back(MaxQueue* obj, int value) {
    //先将数据插入存储所有数据的队列
    DQNode* newNode1 = (DQNode*)malloc(sizeof(DQNode));
    newNode1->next = NULL;
    newNode1->prev = NULL;
    newNode1->val = value;
    if (obj->DQ2->front == NULL)
    {
        obj->DQ2->front = obj->DQ2->tail = newNode1;
    }
    else
    {
        obj->DQ2->tail->next = newNode1;
        newNode1->prev = obj->DQ2->tail;
        obj->DQ2->tail = newNode1;
    }
    //如果最大值队列不为空并且最大值队列的队尾元素小于插入的数据
    //通过循环确保队列单调递减
    while (obj->DQ1->front != NULL && obj->DQ1->tail->val < value)
    {
        //那么就将队尾元素出队
        DQNode* temp = obj->DQ1->tail;
        obj->DQ1->tail = obj->DQ1->tail->prev;
        if (obj->DQ1->tail == NULL)
            obj->DQ1->front = NULL;
        else
            obj->DQ1->tail->next = NULL;
        free(temp);
    }
    //再将数据存入保存最大值的队列
    DQNode* newNode2 = (DQNode*)malloc(sizeof(DQNode));
    newNode2->next = NULL;
    newNode2->prev = NULL;
    newNode2->val = value;
    if (obj->DQ1->front == NULL)
        obj->DQ1->front = obj->DQ1->tail = newNode2;
    else
    {
        obj->DQ1->tail->next = newNode2;
        newNode2->prev = obj->DQ1->tail;
        obj->DQ1->tail = newNode2;
    }
}
int maxQueuePop_front(MaxQueue* obj) {
    if (obj->DQ2->front == NULL)
        return -1;
    DQNode* temp1 = obj->DQ2->front;  //记录待删除的队头
    int ret = temp1->val;   //记录删除数据
    obj->DQ2->front = obj->DQ2->front->next;
    if (obj->DQ2->front == NULL)
        obj->DQ2->tail = NULL;
    else
        obj->DQ2->front->prev = NULL;
    free(temp1);
    //如果删除的数据等于最大值,就将最大值出队
    if (ret == obj->DQ1->front->val)
    {
        DQNode* temp2 = obj->DQ1->front;
        obj->DQ1->front = obj->DQ1->front->next;
        if (obj->DQ1->front == NULL)
            obj->DQ1->tail = NULL;
        else
            obj->DQ1->front->prev = NULL;
        free(temp2);
    }
    return ret; //返回删除的值
}
void maxQueueFree(MaxQueue* obj) {
    //通过循环将两个队列的节点释放
    while (obj->DQ1->front)
    {
        DQNode* temp = obj->DQ1->front;
        obj->DQ1->front = obj->DQ1->front->next;
        free(temp);
    }
    free(obj->DQ1);
    while (obj->DQ2->front)
    {
        DQNode* temp = obj->DQ2->front;
        obj->DQ2->front = obj->DQ2->front->next;
        free(temp);
    }
    free(obj->DQ2);
    free(obj);
}
相关文章
|
6月前
|
NoSQL 容器 消息中间件
单调栈、单调队列
单调栈、单调队列
|
1月前
|
算法 C++
单调队列(C/C++)
单调队列(C/C++)
|
5月前
|
存储 缓存 算法
|
6月前
|
设计模式 算法 调度
【C++】开始使用优先队列
优先队列的使用非常灵活,它适合于任何需要动态调整元素优先级和快速访问最高(或最低)优先级元素的场景。在使用时,需要注意其插入和删除操作的时间复杂度,以及如何根据实际需求选择合适的仿函数。
54 4
|
6月前
|
存储
用队列和栈分别实现栈和队列
用队列和栈分别实现栈和队列
41 1
|
6月前
leetcode:用栈实现队列(先进先出)
leetcode:用栈实现队列(先进先出)
|
存储 C++ 容器
深入了解C++优先队列
在计算机科学中,优先队列是一种抽象数据类型,它与队列相似,但是每个元素都有一个相关的优先级。C++中的优先队列是一个容器适配器(container adapter),它提供了一种在元素之间维护优先级的方法。
186 0
关于队列和栈我所知道的
关于队列和栈我所知道的
38 0
|
存储 算法
双端队列广搜
复习acwing算法提高课的内容,本篇为讲解算法:双端队列广搜,关于时间复杂度:目前博主不太会计算,先鸽了,日后一定补上。
101 0
双端队列广搜