C++(十一)适配器 栈和队列——stack & queue + deque

        前言:在之前的章节里面我们学了STL库里面2个非常常用的容器:vector和list。有了前面的知识可以让我们更好的理解这部分内容——适配器。

适配器也是STL库里面6大组成部分之一

一、概念 

  • 什么是适配器?适配器有什么用?

        在C++ STL中,适配器作为一种工具,可以将容器、迭代器或函数对象转换为另一种形式,从而提供新的接口或行为。STL中有三种常见的适配器:1. 容器适配器(Container Adapter)2. 迭代器适配器(Iterator Adapter)3. 函数适配器(Function Adapter)

我们先来先来学习容器适配器(Container Adapter)

容器适配器(Container Adapter)

(先看看这些名词有个印象,不理解看不懂也没关系,后面会详细学习介绍)

STL中有三种主要的容器适配器:

  • stack:将任何底层容器(如dequevector)适配为一个LIFO(后进先出)。默认使用deque作为底层容器。(deque是一个容器)
  • queue:将底层容器适配为一个FIFO(先进先出)队列,默认使用deque
  • priority_queue:将底层容器适配为一个优先级队列,元素会根据优先级顺序出队。默认使用vector

此篇章只讲述stack和queue的使用,priority_queue在下一文。

        容器适配器有什么用呢?

  •   容器适配器将已有的容器类型改装成不同的抽象类型。

将已有的容器类型改装成不同的抽象类型是什么意思呢?

举个例子:

下面有一个list存储着数据(数据我任意填入),这里把list改装成stack类型,管理这些数据只需要使用stack中接口(成员函数)如:pop,push,就可以实现数据“先进先出”。

当然在此例子中如果把里面的容器list,换成vector也可以达到改装成stack效果。

二、适配器的使用 

        学习和使用过STL的容器:vectorlist的接口函数的使用,在这里stackqueue接口函数的会非常的轻松愉快。但是这里有一些额外的知识需要进一步学习——deque的结构和使用。

在这个例子里面,代码如何实现呢?

#include <list>
#include <stack>
using namespace std;
int main()
{
	stack<int, list<int>> List_ST;//创建一个stack,存int型数据,容器是list<int>

	List_ST.push(0);
	List_ST.push(10);
	List_ST.push(20);
	List_ST.push(30);
	List_ST.push(40);

	    // 创建队列的副本
    stack<int, list<int>> copyList_ST = List_ST;//为什么要先创建副本再打印呢?
                                                //因为在适配器的使用中,我们最好不要直接跳过适配器去访问底层容器

    // 使用 for 循环遍历副本stack并打印元素(后进先出)
    cout << "List_ST:";
    while (!copyList_ST.empty()) //如果stack不为空进入循环
    {
        cout << copyList_ST.top() << " ";	//打印栈顶元素
		copyList_ST.pop();                      //打印完再中删除一个元素(头元素出)
    }
    cout << endl;

    //栈弹出一个元素
    List_ST.pop();

    //再次打印
    copyList_ST = List_ST;
    cout << "List_ST:";
    while (!copyList_ST.empty()) 
    {
        cout << copyList_ST.top() << " ";	
        copyList_ST.pop();                      
    }
    cout << endl;
	return 0;
}

输出结果为:

List_ST:40 30 20 10 0
List_ST:30 20 10 0

 

 2.1 stack

什么是stack(栈):

        stack(栈)是一种数据结构,用于存储数据并遵循后进先出(LIFO, Last In, First Out)的原则。这意味着最后插入的元素会被最先移除。栈的数据结构可以类比为一叠盘子,新盘子总是放在最上面,取盘子时也总是从最上面开始取。

结合图形理解: 

2.1.1.stack类模板和构造函数

 参考网址:cplusplus.com/reference/stack/stack/

我们对照上面示例来分析一下stack各成员函数:

1.stack类模板

        stack也是一个类,它通过类模板来实现对容器的适配。它的类模板定义大致如下:

template <class T, class Container = deque<T> >;
class stack{
 ... ...
};

T:数据类型模板,在此stack中你的数据类型如:int ,char,double 

 Container 翻译:容器:你需要的底层容器 如:list<T>,vector<T>,缺省值:默认传入deque<T>容器(下文会介绍这个容器)

注意:容器显式模板实例化 和 适配器类型需要一致!!否则编译报错(也就是2个T的实例化要一样,如下面2个int)

现在就能看懂示例代码中的显示实例化了:

stack<int, list<int>> List_ST;//创建一个stack,存int型数据,容器是list<int>

list是类名 ,而list<int>是一种类型了

以上代码执行时,实际上发生了:

  • T = int:表示栈中存储的元素类型为 int
  • Container = list<int>:表示使用 std::list<int> 作为底层容器。
2.stack构造函数:

默认构造:

explicit stack(const Container& cont = Container());
  • 功能:创建一个空栈,使用默认的底层容器(默认为 std::deque<T>)进行构造。
  • 参数:可以显式提供一个容器 cont,否则使用默认构造的容器(通常为空)。
  • 示例:
stack<int> defaultStack; // 使用默认容器 std::deque<int>

拷贝构造和复制构造,和vector类一样,这里就不多说了。

这里有一种适配器特有的构造:

从容器直接构造 :

explicit stack(const Container& cont);
  • 功能:使用指定的容器 cont 构造一个栈。
  • 参数cont:可以是容器的常引用或右值引用。支持使用任何符合 stack 要求的容器,如 dequelistvector
  • 示例:
deque<int> deque = {1, 2, 3};
stack<int> Deque_ST(deque); // 使用 deque 构造 stack

2.1.2 stack的其他接口函数 

在之前vector的学习中,这些函数的用法相信大家看一看就已经知道功能和用法,用法和vector类中的接口函数是一样的:

  • Constructor: 构造 stack 对象,可以使用默认构造函数,也可以基于其他容器进行构造。

  • empty(): 检查栈是否为空。如果栈为空,返回 true;否则返回 false

  • size(): 返回栈中元素的个数。

  • top(): 访问栈顶元素,但不移除该元素。

  • push(): 向栈顶插入一个元素,将该元素复制或移动到栈中。

  • emplace(): 在栈顶直接构造并插入一个元素,避免了额外的拷贝或移动操作。

  • pop(): 移除栈顶元素。

  • swap(): 交换两个 stack 对象的内容。

需要注意的是push()和emplace(),pushemplace 在插入内置类型情况下的效果是相同的。emplace 的优势在于,当你有一个更复杂的对象(如自定义类)时,可以直接在栈中使用构造函数参数创建对象,避免了临时对象的创建。

        (简而言之:当有自定义类型再考虑使用emplace)

上面介绍的已经是stack的绝大多少使用内容了,剩下的一些冷门一点的函数,这里不作介绍,大家感兴趣可以去参考网站上查看了解详细用法。

2.2queue

什么是queue(队列):

队列(Queue) 是一种数据结构,用于存储和管理数据并遵循先进先出(FIFO, First In, First Out)的原则。这意味着最早插入的元素会被最先移除,类似于排队等候的情况。

结合图形认识队列: 

2.2.1queue类接口函数 

        queue的接口函数,和stack是一模一样的!!所以用法也是一摸一样。

        不一样的是数据管理规则,栈是后进先出,队列是先进先出

同样的类模板定义方式:

(默认传入的容器也是deque)

template <class T, class Container = std::deque<T>>
class queue {
  ... ...
};

接口函数:

  • empty(): 返回队列是否为空。
  • size(): 返回队列中的元素数量。
  • front(): 返回队列头部元素的引用。
  • back(): 返回队列尾部元素的引用。
  • push(const value_type& value): 在队列尾部插入一个元素。
  • emplace(Args&&... args): 在队列尾部原地构造元素。
  • pop(): 移除队列头部的元素。
  • swap(queue& other): 交换两个队列的内容。

函数命名和stack类中大部分一样的,不一样的是:

        在队列中查看第一个元素:front() 

       中 查看栈顶元素:top()


但是,使用queue时,有一点需要注意:(非常重要)

        queue 适配器不支持 vector 作为底层容器!!

  queue 适配器不支持 vector 作为底层容器 !!

  queue 适配器不支持 vector 作为底层容器 !!

为什么 queue 适配器不支持 vector 作为底层容器 呢?

   vector 不支持高效的头部删除操作,因为 vector 中的元素在内存中是连续存储的。删除头部元素意味着需要移动整个数组中的元素,所有后面的数据都要向前挪。

        开发C++STL的前辈们可能觉得效率太低,所以直接不允许了(强行用会报错)。

2.2.2使用示例

#include <iostream>
#include <queue>  // 包含 std::queue

int main() {
    // 创建一个空的整数队列(使用默认容器deque)
    std::queue<int> myQueue;

    // 向队列中添加元素
    myQueue.push(10);
    myQueue.push(20);
    myQueue.push(30);

    // 打印队列的大小
    std::cout << "size: " << myQueue.size() << std::endl;

    // 访问队列头部的元素
    std::cout << "myQueue.front(): " << myQueue.front() << std::endl;

    // 访问队列尾部的元素
    std::cout << "myQueue.back(): " << myQueue.back() << std::endl;

    // 从队列中移除头部元素
    myQueue.pop();

    // 再次访问队列头部的元素
    std::cout << "myQueue.front(): " << myQueue.front() << std::endl;

    // 打印剩下的所有元素并清空队列
    std::cout << "myQueue: ";
    while (!myQueue.empty()) 
    {
        std::cout << myQueue.front() << " ";
        myQueue.pop();
    }
    std::cout << std::endl;

    return 0;
}

运行结果:

size: 3
myQueue.front(): 10
myQueue.back(): 30
myQueue.front(): 20
myQueue: 20 30

三、容器:deque

前文一直提到的deque容器,到底是个什么呢?

为什么stack和queue的默认容器都使用她?

详细的参考网站:cplusplus.com/reference/deque/deque/ 

我们来揭开她神秘的面纱:

  deque(双端队列,double-ended queue)是一种序列容器,它允许在容器的两端进行高效的插入和删除操作。

deque可以看作是vector和list的结合体为什么呢?用图形具体表示:

如图形所呈现,

depue的特点

  • deque 的内存分配是分段的,不像 vector 那样需要一个连续的大块内存空间。它由多个固定大小的小块(通常称为缓冲区或块)组成,这些块通过中控数组来管理。
  • deque 使用中控数组管理这些块。这个指针数组通常被称为 mapmap 中的每个元素都是一个指向块的指针。通过这个 map,可以快速定位并访问任意块中的元素。
  • 当在 deque 的头部或尾部插入元素时,如果当前块已满,deque 会分配一个新的块,并将其指针添加到 map 中。如果 map 本身的容量不足,则会重新分配一个更大的 map 并迁移现有的块指针。
  • 块的大小通常是固定的,但具体大小取决于实现和平台
  • 两端操作

  为什么stack和queue的默认容器都使用她呢?

因为depue的两端操作:

  1. 当在头部插入元素时,如果前面的块有空间,直接插入即可;如果前面的块已满,则会分配一个新的块并将其插入 map 的前端。
  2. 在尾部插入元素时,情况类似。如果当前尾部块有空间,则直接插入;如果没有空间,则会分配新的块。

这种设计允许它在两端进行高效的插入和删除操作,同时仍然随机访问能力。 

缺点:

  • deque 支持常数时间的随机访问,但由于需要通过 map 和块的组合来访问具体元素,随机访问的效率不如 vector。在 deque 中,随机访问涉及多个指针跳转,这在某些情况下会导致比 vector 更高的开销。
  • deque 使用分段数组来存储元素,因此它的内存开销比vector 更大
  • deque 使用分段数组来存储元素,当它修改/插入/删除块中数据时,需要很复杂的挪动数据,效率很低

总结:头插头删尾插尾删非常灵活高效,但中间数据部分管理效率很低,因此非常适合做stack和queue的默认容器,日常用vector和list效率更高,更好用。

deque常用接口

1.构造函数


explicit deque (const allocator_type& alloc = allocator_type());

explicit deque (size_type n, const value_type& val = value_type(),
                const allocator_type& alloc = allocator_type());
	
template <class InputIterator>
  deque (InputIterator first, InputIterator last,
         const allocator_type& alloc = allocator_type());
	
deque (const deque& x);
  • std::deque<T> dq;:默认构造函数,创建一个空的 deque
  • std::deque<T> dq(n);:创建一个包含 n 个元素的 deque
  • std::deque<T> dq(n, value);:创建一个包含 n 个元素的 deque,每个元素的值为 value
  • std::deque<T> dq({value1, value2, ...});:使用初始化列表构造 deque
  • std::deque<T> dq(first,last);:使用迭代器区间初始化。

2.元素访问

  • dq[i]:访问第 i 个元素(支持随机访问)。
  • dq.at(i):访问第 i 个元素,带边界检查。
  • dq.front():访问 deque 的第一个元素。
  • dq.back():访问 deque 的最后一个元素。

3.修改器元素访问

void push_back(const T& value);
void push_back(T&& value); // 针对右值引用的重载
void push_front(const T& value);
void push_front(T&& value); // 针对右值引用的重载
void pop_back();
void pop_front();
iterator insert(iterator pos, const T& value);
iterator insert(iterator pos, T&& value); // 针对右值引用的重载
iterator erase(iterator pos);
void clear();
  • dq.push_back(value):在 deque 的尾部插入元素。
  • dq.push_front(value):在 deque 的头部插入元素。
  • dq.pop_back():移除 deque 尾部的元素。
  • dq.pop_front():移除 deque 头部的元素。
  • dq.insert(pos, value):在 deque 的指定位置 pos 插入元素。
  • dq.erase(pos):移除 deque 指定位置 pos 的元素。
  • dq.clear():清空 deque 中的所有元素。

4.容量

  • dq.size():返回 deque 中元素的数量。
  • dq.empty():判断 deque 是否为空。

5.其他操作

  • dq.swap(dq2):交换两个 deque 的内容。
  • dq.assign(n, value):将 deque 赋值为 nvalue

使用示例

#include <iostream>
#include <deque>

int main() {
    // 创建一个空的 deque
    std::deque<int> dq;

    // 在 deque 的尾部插入元素
    dq.push_back(10);
    dq.push_back(20);
    dq.push_back(30);

    // 在 deque 的头部插入元素
    dq.push_front(5);
    dq.push_front(2);

    // 访问元素
    std::cout << "第一个元素: " << dq.front() << std::endl;  //  2
    std::cout << "最后一个元素: " << dq.back() << std::endl; // 30

    // 使用下标访问元素
    std::cout << "第二个元素: " << dq[1] << std::endl;       //  5

    // 在指定位置插入元素
    dq.insert(dq.begin() + 2, 7);

    // 打印所有元素
    std::cout << "dq : ";
    for (int value : dq) 
    {
        std::cout << value << " ";
    }
    std::cout << std::endl; // 输出: 2 5 7 10 20 30

    // 移除头部和尾部的元素
    dq.pop_front();
    dq.pop_back();

    // 再次打印
    std::cout << "dq : ";
    for (int value : dq) {
        std::cout << value << " ";
    }
    std::cout << std::endl; // 输出: 5 7 10 20

    // 清空 deque
    dq.clear();

    // 检查 deque 是否为空
    if (dq.empty())
    {
        std::cout << "deque 已清空。" << std::endl; // 输出: deque 已清空。
    }

    return 0;
}

运行结果:

第一个元素: 2
最后一个元素: 30
第二个元素: 5
dq : 2 5 7 10 20 30
dq : 5 7 10 20
deque 已清空。

使用方法也非常的简单,和vector的使用很相像。 

  • 30
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值