【C++】适配器模式 -- stack/queue/dqueue

news2024/11/23 11:12:50

一、适配器模式

设计模式

设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结;Java 语言非常关注设计模式,而 C++ 并没有太关注,但是一些常见的设计模式我们还是要学习。

迭代器模式

其实我们在前面学习 string、vector 和 list 时就已经接触过设计模式了 – 迭代器就是一种设计模式;迭代器模式是封装后提供统一的接口 iterator,在不暴露底层实现细节的情况下,使得上层能够以相同的方式来访问不同的容器。

适配器模式

适配器模式则是将一个类的接口转换成客户希望的另外一个接口,即根据已有的东西转换出我们想要的东西。


二、stack

1、stack 的介绍和使用

stack 的介绍

和我们以前学的容器不同,为了不破坏栈 LIFO 的特性,stack 不提供迭代器,所以 stack 不是迭代器模式,而是一种容器适配器:image-20230301210429119

如图,stack 使用 dqueue 容器作为默认的适配容器,关于 dqueue 的内容,我们放在文章最后面讲。

stack 常用接口的使用

stack 的接口较少,同时其接口的使用方法也十分简单,这里我就不再一一演示,如果对某些接口有疑问的同学,还是和以前一样,到 cplusplus.com 上面查看使用细节。

-函数说明-接口说明
stack()构造空的栈
empty()检测stack是否为空
size()返回stack中元素的个数
top()返回栈顶元素的引用
push()栈顶入栈
pop()栈顶出栈

2、stack 的模拟实现

在了解了适配器模式之后,我们就可以将适配器作为类的第二个模板参数,然后通过传递不同的适配容器来实现栈了:

//stack.h
template<class T, class Container>
class stack {
	//...
};

//test.cpp
void test_stack() {
	stack<int, std::vector<int>> st1;
	stack<int, std::list<int>> st2;
}

如上,vector 和 list 都可以作为 stack 的适配容器,我们可以通过给定不同的第二个模板参数来使用不同的容器适配 stack;

但是出于随机抽取和缓存命中的考虑,vector 显然更适合作为 stack 的适配容器,那么我们可以还可以将 vector 设置为 stack 的默认适配容器,这样,我们以后定义栈对象时就不用显式指定 vector 了:

//stack.h
template<class T, class Container = std::vector<T>>
class stack {
	//...
};

//test.cpp
void test_stack() {
	stack<int> st1;  //默认使用vector做适配容器
	stack<int, std::list<int>> st2;  //使用其他容器做适配容器需要显式指定
}

有了适配容器之后,我们就可以通过调用适配容器的接口来实现 stack 的接口了,这样,stack 的模拟实现将会变得非常简单。

stack.h:

#pragma once

#include <iostream>
#include <vector>
#include <list>

namespace thj {
	template<class T, class Container = std::vector<T>>
	class stack {
	public:
		//构造和析构不用写,默认生成的构造和析构对于自定义类型会调用它们的构造和析构函数

		bool empty() const {
			return _Con.empty();
		}

		size_t size() const {
			return _Con.size();
		}

		T& top() {
			return _Con.back();  //数组尾部作为栈的栈顶
		}

		const T& top() const {
			return _Con.back();
		}

		void push(const T& val) {
			_Con.push_back(val);  //在数组尾部插入数据
		}

		void pop() {
			_Con.pop_back();
		}

	private:
		Container _Con;
	};
}

test.h:

void test_stack() {
	//stack<int, std::vector<int>> st1;
	//stack<int, std::list<int>> st2;

	//stack<int> st1;  //默认使用vector做适配容器
	//stack<int, std::list<int>> st2;  //使用其他容器做适配容器需要显式指定

	stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	st.push(4);
	st.push(5);

	cout << st.size() << endl;

	while (!st.empty()) {
		cout << st.top() << " ";
		st.pop();
	}
	cout << endl;
}

3、stack 的经典 OJ 题

力扣 155.最小栈

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 MinStack 类:

  • MinStack() 初始化堆栈对象。
  • void push(int val) 将元素val推入堆栈。
  • void pop() 删除堆栈顶部的元素。
  • int top() 获取堆栈顶部的元素
  • int getMin() 获取堆栈中的最小元素。

示例 :

输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[],[-2],[0],[-3],[],[],[],[]]

输出:
[null,null,null,null,-3,null,0,-2]

思路1

可能有的同学第一时间想到的就是使用一个 min 变量来记录最小值,然后每次插入时比较更新这个最小值,但是这样其实是不行的,比如我们先插入 [4, 2, 7, 2, 1, 6],此时 min = 1,现在我们 pop 掉两个元素,栈的数据变成 [4, 2, 7, 2],最小值为 2,而我们的 min 显然还是原来的 1。

思路2

思路2就是使用一个专门的栈 minST 来记录最小值,当插入的元素小于 minST 栈顶元素时就将该元素插入到 minST 中,如果该元素大于 或等于 minST 栈顶元素就向 minST 中插入一个与栈顶大小相同的值,此时最小值就是 minST 栈顶的元素,如下:

st: [4, 2, 7, 2, 1, 6]  minST: [4, 2, 2, 2, 1, 1] min = 1

//pop两次
st: [4, 2, 7, 2]  minST: [4, 2, 2, 2] min = 2

//再pop三次
st: [4]  minST: [4] min = 4

思路2优化

思路2虽然可行,但是其空间复杂度为O(N) – 需要一个同等大小的栈来存放最小值,我们可以对其进行优化 – 不必每次都向 minST 中插入元素,只有当插入的元素 小于或等于 minST 栈顶元素时才插入,pop 数据时也只有当 pop 的值和 minST 栈顶元素相同时才pop,如下:

//7和6插入时minST不变
st: [4, 2, 7, 2, 1, 6]  minST: [4, 2, 2, 1] min = 1 
    
//pop两次
//6与minST栈顶不相等,minST不动,1相等,pop minST栈顶元素
st: [4, 2, 7, 2]  minST: [4, 2, 2] min = 2  

//再pop三次
//2和2与minST栈顶相等,minST pop,7不相等,minST不动
st: [4]  minST: [4] min = 4

特别注意:当插入的元素与 minST 栈顶元素相等时也需要插入,这是为了避免后续插入的的元素中有等于当前 minST 栈顶的元素,这样 pop 该元素时会导致 minST 栈顶元素也被 pop 掉,这样就找不到 min 了:

//当前状态
st: [4, 2, 5, 3, 2, 7]  minST: [4, 2] min = 2

//pop两次
//由于2和minST栈顶元素相同,所以minST中2被删除
st: [4, 2, 5, 3]  minST: [4] min = 4 //错误

代码实现:

class MinStack {
public:
    MinStack() {  //可以为空,初始化列表会走自定义类型的默认构造

    }
    
    void push(int val) {  //如果插入的元素小于等于最小元素就插入_minST
        _st.push(val);
        if(_minST.empty() || val <= _minST.top()) _minST.push(val);
    }
    
    void pop() {  //如果删除的元素等于_minST.top()就删除
        int Top = _st.top();
        _st.pop();
        if(Top == _minST.top()) _minST.pop();  
    }
    
    int top() {
        return _st.top();
    }
    
    int getMin() {
        return _minST.top();
    }

private:
    stack<int> _st;
    stack<int> _minST;
};

牛客 JZ31.栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。

示例

输入:

[1,2,3,4,5],[4,5,3,2,1]

返回值:

true

思路

这道题我们模拟出栈顺序即可,将 pushV 中的元素入栈,入栈时和 popV 中的元素比较,若相同,则说明当前元素在此处出栈;当循环结束后,如果 pushV 中入栈的元素全部被 pop 或者 popV 走到了结尾,则说明出栈顺序是正确的,反之错误;需要注意的是,由于出栈可能是多个元素连续出栈,所以我们需要写成循环。

代码实现:

class Solution {
public:
    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        stack<int> st;
        int popi = 0;

        for(auto e : pushV) {
            st.push(e);
            while(!st.empty() && st.top() == popV[popi]) {  //这里需要写成循环,因为可能连续出栈
                popi++;
                st.pop();
            }
        }

        return st.empty();
    }
};

150. 逆波兰表达式求值 - 力扣(LeetCode)

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 ‘+’、‘-’、‘*’ 和 ‘/’ 。
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断 。
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

示例 :

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

思路

要解决这道题,我们首先要明白计算机对逆波兰表达式 (后缀表达式) 是计算方法 – 用栈来存储数据,从左到右遍历表达式,遇到数字直接入栈,遇到运算符先取出栈顶元素和下一个栈顶元素,分别作为右操作数和左操作数与运算符进行运算,运算结果放入栈中;然后再继续往后遍历,直到将表达式遍历完。

现在我们只需要模拟这个计算过程即可,完整代码如下:

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> st;

        for(auto& str : tokens) {
            if(str == "+" || str == "-" || str == "*" || str == "/") {  //运算符取两个栈顶元素运算后入栈
                int right = st.top();  //右操作数
                st.pop();

                int left = st.top();  //左操作数
                st.pop();

                switch(str[0]) {
                    case '+':
                        st.push(left + right);
                        break;
                    case '-':
                        st.push(left - right);
                        break;
                    case '*':
                        st.push(left * right);
                        break;
                    case '/':
                        st.push(left / right);
                        break;
                    default:
                        break;
                }
            } else {  //数字直接入栈
                st.push(stoi(str));
            }
        }
        
        return st.top();
    }
};

拓展知识 – 中缀表达式转后缀表达式

我们日常生活中见到的都是中缀表达式,这是因为我们从小接触到的就是中缀表达式,对其十分熟悉,然而对于计算机来说,计算机更容易计算后缀表达式,所以有的计算机在计算时会先将中缀表达式转为后缀表达式 (逆波兰表达式),再根据后缀表达式求值。

上面我们已经模拟了后缀表达式求值的过程,那么中缀表达式应该如何转化为后缀表达式呢?转换规则如下:

  • 遇到操作数直接输出;
  • 使用栈来存储操作符,遇到操作符如果栈为空则直接入栈;
  • 如果栈不为空,则与栈顶操作符进行比较,如果当前操作符优先级高于栈顶操作符,则入栈;如果低于或等于,则栈顶运算符输出,当前运算符入栈。

我们更好理解我们举例说明:

中缀表达式:1 + 2 * 3 / 4 - 5
    
//第一步:遇到操作数1,直接输出
st: [] 后缀表达式:1
    
//第二步:遇到操作符,且栈为空,直接入栈
st: [+] 后缀表达式:1
    
//第三步,2输出;第四步,操作符*优先级大于+,入栈
st: [+ *] 后缀表达式:1 2 
    
//第五步,3输出,第六步,操作符/优先级等于*,栈顶运算符*输出,当前运算符/入栈
st: [+ /] 后缀表达式:1 2 3 *
    
//第七步,4输出;第八步,操作符-优先级小于/,栈顶运算符/输出,当前运算符-入栈
st: [+ -] 后缀表达式:1 2 3 * 4 /

//第九步,此时栈中有两个运算符,且栈顶运算符-和运算符+优先级相等,所以+输出
st: [-] 后缀表达式:1 2 3 * 4 / +   

//第十步,5输出;第十一步,表达式遍历完成,此时栈中还剩一个-,输出
st: [] 后缀表达式:1 2 3 * 4 / + 5 -

所以,中缀表达式 [1 + 2 * 3 / 4 - 5] 转为后缀表达式为 [1 2 3 * 4 / + 5 -]

注:我们上面提到的中缀转后缀是不包含括号的情况,如果出现括号还要进行特殊处理。


232. 用栈实现队列 - 力扣(LeetCode)

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

示例 :

输入:
[“MyQueue”, “push”, “push”, “peek”, “pop”, “empty”]
[[], [1], [2], [], [], []]

输出:
[null, null, null, 1, 1, false]

思路

由于栈只能在头部插入和删除数据,而队列是在尾部插入数据,在头部删除数据,所以我们可以使用两个栈,一个专门用来入数据,一个专门用来出数据:

pushST:专门用来入数据,当有元素入队列时直接插入 pushST 中;

popST:专门用来出数据,当数据出队列时,检查 popST 是否为空,如果为空,将 pushST 中的数据全部取出插入到 popST 中 – 因为栈是后入先出的,所以当我们将 pushST 中的数据取出插入到 popST 后,原本位于栈底的数据就会位于栈顶,此时 popST 的出栈顺序就和队列的出队顺序相同了,如图所示:image-20230302193609231

代码实现如下:

class MyQueue {
public:
    MyQueue() {  //此处不用写,因为成员变量会自动走初始化列表,自定义类型会在此处调用默认构造

    }

    void move() {  //检查popST是否为空,是就将pushST中的元素全部转移到其中
        if(_popST.empty()) {
            while(!_pushST.empty()) {
                _popST.push(_pushST.top());
                _pushST.pop();
            }
        }
    }
    
    void push(int x) {  //直接插入到pushST中
        _pushST.push(x);
    }
    
    int pop() {
        move();  //pop之前先检查是否需要移动元素到popST

        int Top = _popST.top();
        _popST.pop();
        return Top;
    }
    
    int peek() {
        move();

        return _popST.top();
    }
    
    bool empty() {
        move();

        return _popST.empty();
    }

private:
    stack<int> _pushST;
    stack<int> _popST;
};

这里有几个需要注意的地方:

1、在类和对象部分我们学习了类的六大默认成员函数以及初始化列表,知道了如果我们不写构造函数编译器会自动生成一个无参的默认构造函数,它对于自定义类型会去调用自定义类型的默认构造;

2、同时,类成员变量真正的初始化工作是在初始化列表完成的,所以不管我们是否显示的在初始化列表处对成员变量进行初始化工作,成员变量都会在初始化列表进行初始化工作,而自定义类型会调用自身的默认构造完成初始化;

3、所以,题目给出的代码中 MyQueue() 即默认构造接口我们可以直接删除,此时 MyQueue 会根据自动生成的默认构造函数完成对 pushST 和 popST 的初始化;我们也可以不管它,因为即使我们不在初始化列表显式的对 pushST 和 popST 进行初始化,它们也会在初始化列表处调用自身的默认构造完成初始化工作。

4、最后,由于 pop、peek、empty 接口都需要在最开始判断 popST 是否为空,如果为空就要将 pushST 中的数据转移到 popST 中来,为了避免代码冗余,我们可以将这一部分功能单独写成一个函数供其他函数调用。


三、queue

1、queue 的介绍和使用

queue 的介绍

和 stack 一样,queue 也是一种容器适配器,也不提供迭代器:image-20230301235931514

可以看到,queue 也是使用 deque 作为默认适配容器,和之前一样,deque 我们放在最后面讲。

queue 常用接口的使用

queue 的接口较少,同时其接口的使用方法也十分简单,这里我就不再一一演示,如果对某些接口有疑问的同学,还是和以前一样,到 cplusplus.com 上面查看使用细节。

-函数声明-接口说明
queue()构造空的队列
empty()检测队列是否为空
size()返回队列中有效元素的个数
front()返回队头元素的引用
back()返回队尾元素的引用
push()在队尾将元素val入队列
pop()将队头元素出队列

2、queue 的模拟实现

和 stack 一样,vector 和 list 都可以作为 queue 的适配容器,但是由于 queue 需要大量在头部删除数据,所以 list 更适合作为 queue 的默认适配容器,那么 queue 模拟实现的代码如下:

queue.h:

#pragma once

#include <iostream>
#include <vector>
#include <list>

namespace thj {
	template<class T, class Container = std::list<T>>
	class queue {
	public:
		//构造和析构不用写,默认生成的构造和析构对于自定义类型会调用它们的构造和析构函数

		bool empty() const {
			return _Con.empty();
		}

		size_t size() const {
			return _Con.size();
		}

		T& front() {
			return _Con.front();  //第一个节点为队头
		}

		const T& front() const {
			return _Con.front();
		}

		T& back() {
			return _Con.back();  //最后一个节点为队尾
		}

		const T& back() const {
			return _Con.back();  //最后一个节点为队尾
		}

		void push(const T& val) {
			_Con.push_back(val);  //在链表尾部插入节点
		}

		void pop() {
			_Con.pop_front();  //删除第一个节点
		}

	private:
		Container _Con;
	};
}

test.cpp:

void test_queue() {
	queue<int> q; //默认使用vector做适配容器
	q.push(1);
	q.push(2);
	q.push(3);
	q.push(4);
	q.push(5);

	cout << q.size() << endl;

	while (!q.empty()) {
		cout << q.front() << " ";
		q.pop();
	}
	cout << endl;
}

需要注意的是,当我们使用 vector 作为 queue 的适配容器时,编译会报错,这是由于 vector 头插头删效率太低,所以并没有专门提供头插和头删的函数,而是通过 insert 和 erase 来完成,而我们实现的 queue 中使用了 pop_front 函数:image-20230302001846695

不过这种情况出现并不意味着我们实现的 queue 不行,其实报错反而提醒了 queue 的使用者 – vector 不适合作为 queue 的适配容器,应该更换一个更合适的。

3、queue 经典 OJ 题

225. 用队列实现栈 - 力扣(LeetCode)

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

示例:

输入:
[“MyStack”, “push”, “push”, “top”, “pop”, “empty”]
[[], [1], [2], [], [], []]

输出:
[null, null, null, 2, 2, false]

思路

这道题比上面 用栈实现队列 要复杂一些,用栈实现队列思路很简单,两个栈,一个专门用来入,一个专门用来出,用来出的出完了就把用来入的里面的数据转移过来即可;这个虽然也是两个队列,但它们之间的关系不仅仅是一个出、一个入那么简单:

1、入数据:定义两个队列 q1 和 q2,最开始入栈时随便将数据插入哪个队列,第二次及以后插入时把数据插入到非空队列中;

2、出数据:出栈就比较复杂了,由于队列是先入先出而栈是后入先出的,所以队尾的数据就是栈顶的数据;而出栈时我们需要将队尾前面的元素全部挪到另一个为空的队列中,然后删除队列中剩余的那个数据,即队尾数据,这样才完成了出栈 – 删除后此队列就为空,而另一个队列就不为空了,那么下次再出栈时就可以将另一个不为空队列中除队尾外所有数据挪动到此空队列中,再删除那个队列中的数据;

3、注意:将 q1 中的数据挪动 q2 中的数据时数据的前后顺序不会改变,即入 q1 的数据顺序为 1 2 3 4 5,那么将数据取出来插入到 q2 后,q2 中数据的顺序仍是 1 2 3 4 5,这是因为队列的入队顺序和出队顺序是一致的;

4、总结:不进行出栈操作时,始终有一个队列是空的,入栈直接插入到非空队列中,出栈则需要先将非空队列中除队尾以外的数据全部挪动的空队列中,再删除非空队列中剩余的那个数据。

代码实现

class MyStack {
public:
    MyStack() {  //自动走初始化列表,调用其默认构造

    }

    void push(int x) {  //向非空队列中push
        if(!_q1.empty()) _q1.push(x);
        else _q2.push(x);
    }
    
    int pop() {  //相当于pop非空队列队尾的元素
    int Top = 0;  //用于记录空队列队尾的元素
        if(_q1.empty()) {
            while(_q2.size() != 1) {  //注意这里是元素个数不为1,而不是队头元素不等于队尾元素
                int Top = _q2.front();
                _q1.push(Top);
                _q2.pop();
            }

            Top = _q2.back();  //其实此时q2中就只有一个元素了
            _q2.pop();
        } 
        else if(_q2.empty()) {
            while(_q1.size() != 1) {  
                int Top = _q1.front();
                _q2.push(Top);
                _q1.pop();
            }

            Top = _q1.back();
            _q1.pop();
        }
    
        return Top;
    }

    int top() {  //非空队列的队尾元素为栈顶元素
        int Top = 0;
        if(!_q1.empty()) Top = _q1.back();
        else if(!_q2.empty()) Top = _q2.back();

        return Top;
    }
    
    bool empty() {
        return _q1.empty() && _q2.empty();
    }

private:
    queue<int> _q1;
    queue<int> _q2;
};

注意事项:

和用栈实现队列一样,构造函数不提供编译器自动生成,成员变量不显式进行初始化也会走初始化列表进行初始化。


四、deque

1、deque 的原理介绍

deque (双端队列):是一种双开口的 “连续” 空间的数据结构,双开口的含义是 deque 可以在头尾两端进行插入和删除操作,且时间复杂度为O(1);与vector比较,头插效率高,不需要搬移元素,与 list 比较,空间利用率比较高,“随机访问” 效率较高。image-20230302203408173

2、deque 的底层结构

deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个动态的二维数组,其结构示意图如下:image-20230302205058954

由上图我们可以知道 deque 的底层结构:

  • deque 具有多个 buffer 数组,每个buffer数组可以存储 n 个数据 (n 一般为10),还具有一个用于管理 buffer 数组的中控指针数组,数组中的每个元素都指向一个 buffer 数组;
  • 中控指针数组的使用:让数组最中间的元素指向第一个 buffer,当第一个buffer 数组满开辟第二个 buffer 数组时,让指针数组的后一个位置或者前一个位置指向新开辟的 buffer 数组 – 因为头插导致新 buffer 数组开辟就让前一个位置指向新 buffer 数组,尾插导致就让后一个位置指向;
  • deque 的扩容:当中控指针数组满后扩容,让中控指针数组的容量变为原来的二倍,然后将原中控数组里面的数据 (即各个buffer数组的地址) 拷贝到新的中控指针数组中;

3、deque 的迭代器设计

为了维护 deque “整体连续” 以及随机访问的假象,deque 的迭代器设计非常复杂,如图:image-20230302203929819

image-20230302203945225

4、deque 的优缺点

根据 deque 的设计结构,我们可以知道其优点:

  • 具有 vector 的优点 – 支持随机访问、缓存命中率较高、尾部插入删除数据效率高;
  • 同时具有 list 的优点 – 空间浪费少、头部插入插入数据效率高;

但同时 deque 也存在一些缺点:

  • deque 的随机访问效率较低 – 需要先通过中控数据找到对应的buffer数组,再找到具体的位置 (假设偏移量为 i,需先 i/10 得到位于第几个buffer数组,再 i%10 得到 buffer 数组中的具体位置),即 deque 随机访问时一次跳过一个buffer数组,需要跳多次才能准确定位,其效率比 list 高了不少,但比 vector 也低了不少;
  • deque 在中部插入删除数据的效率是比较低的 – 需要挪动数据,但不一定后续 buffe 数组中的数据全部挪动,可以控制只挪一部分,即中间插入删除数据的效率高于 vector,但是低于 list。

基于 deque 的优缺点我们可以发现,虽然 deque 综合了 vector 和 list 的优缺点,看似很完美,但是它单方面的性能是不如 vector 或者 list 的,也就是说,deque 有点东西,但不多,因此 deque 在实际应用中使用的非常少

不过不可否认的是 deque 确实很适合作为 stack 和 list 的默认适配容器,毕竟它对于 stack 和 list 的通用的,这也是 STL 中选择 deque 作为 stack 和 queue 默认适配容器的原因

deque 特别适合需要大量进行头插和尾部数据的插入删除、偶尔随机访问、偶尔中部插入删除的场景;不太适合需要大量进行随机访问与中部数据插入删除的场景,特别是排序。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/384346.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

call、apply、bind的区别以及源码实现

首先&#xff0c;需要明确&#xff0c;call()、apply()、bind()这三个方法的作用都是 改变this指向它们之间的不同点在于&#xff1a;call和apply是直接调用的&#xff0c;而bind不会立即调用&#xff0c;需要手动调用&#xff08;原因在于bind返回的是一个改变了this指向的函数…

请你喝一杯 Corretto?谈一谈 JDK 的新选择

前言如果以20年的时间为限&#xff0c;让我们选出一个影响力最大的程序语言&#xff0c;我的答案应该只有一个 – 那就是 Java。这个1995年诞生的全新的计算机语言&#xff0c;以“一次编写&#xff0c;到处运行”的跨平台特性以及面向对象、泛型编程等现代语言的特性迅速成为了…

Allegro172版本如何通过规则设置检查器件的热平衡问题

Allegro172版本如何通过规则设置检查器件的热平衡问题 在做PCB设计的时候,器件的热平衡问题是必须考虑到的一个设计要点,尤其小封装的阻容器件,热平衡问题没有考虑好,直接影响到装配的可靠性 如下图 小封装器件,一边线宽粗并且铺铜,另外一端是一根细线 Allegro172及以上…

c语言指针怎么理解 第一部分

不理解指针&#xff0c;是因为有人教错了你。 有人告诉你&#xff0c;指针是“指向”某某某的&#xff0c;那就是误导你&#xff0c;给你挖了个坑。初学者小心不要误读这“指向”二字。 第一&#xff0c;“指针”通常用于保存一个地址&#xff0c;这个地址的数据类型在定义指…

ASGCN之依存句法图的构建

文章目录前言1.理论部分1.1 依存句法理论1.2 依存句法分析1.3 依存句法的应用2. 代码实践2.1 数据集2.2 代码实现2.3 效果查看总结前言 本文首先介绍依存句法理论&#xff0c;之后通过代码实现ASGCN中的依存句法图数据的构建。 1.理论部分 1.1 依存句法理论 词与词之间存在主…

Vue3电商项目实战-购物车模块2【04-头部购物车-商品列表-本地、05-头部购物车-删除操作-本地、06-购物车页面-基础布局】

文章目录04-头部购物车-商品列表-本地05-头部购物车-删除操作-本地06-购物车页面-基础布局04-头部购物车-商品列表-本地 目的&#xff1a;根据本地存储的商品获取最新的库存价格和有效状态。 大致步骤&#xff1a; 定义获取最新信息的API定义修改购物车商品信息的mutations定义…

Mybatis工作原理及流程

1、MyBatis介绍 MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的XML或注解来配置和映射原始类型、接口和JavaPOJO(PlainOldJavaObjects,普通老式Java对象)为…

OSSFs挂载工具简介

OSSFs挂载工具 OSSFs挂载工具简介 ​ ossfs允许您在Linux系统中将对象存储OSS的存储空间&#xff08;Bucket&#xff09;挂载到本地文件系统。挂载完成后&#xff0c;您能够像操作本地文件一样操作OSS的对象&#xff08;Object&#xff09;&#xff0c;从而实现数据共享。 ​…

RT_Thread Nano 简介, 移植

官网介绍 RT_Thread Nano 1.简介 RT-Thread Nano 是一个极简版的硬实时内核&#xff0c;它是由 C 语言开发&#xff0c;采用面向对象的编程思维&#xff0c;具有良好的代码风格&#xff0c;是一款可裁剪的、抢占式实时多任务的 RTOS。其内存资源占用极小&#xff0c;功能包括…

打怪升级之MFC变量小实验

按惯例&#xff0c;这一篇文章主要还是作者读《深入浅出MFC》整理的一些笔记。不过本次还加上了一些作者自己的理解。 实验的前期准备 做实验前&#xff0c;你最好了解一下MFC的执行流程&#xff0c;从winapp到各类控件的实际变化过程&#xff0c;可以参考博主之前的笔记。 …

SOTA!目标检测开源框架YOLOv6 3.0版本来啦

近日&#xff0c;美团视觉智能部发布了 YOLOv6 3.0 版本&#xff0c;再一次将目标检测的综合性能推向新高。YOLOv6-L6 检测精度和速度超越 YOLOv7-E6E&#xff0c;取得当前实时目标检测榜单 SOTA。本文主要介绍了 YOLOv6 3.0 版本中引入的技术创新和优化&#xff0c;希望能为从…

64. 最小路径和

64. 最小路径和 给定一个包含非负整数的 m∗nm * nm∗n 网格 grid &#xff0c;请找出一条从左上角到右下角的路径&#xff0c;使得路径上的数字总和为最小。 说明&#xff1a;每次只能向下或者向右移动一步。 示例 1&#xff1a; 输入&#xff1a;grid [[1,3,1],[1,5,1],[…

Hudi的7种索引

1、Bloom Index Bloom Index (default) 使用根据记录键构建的bloom过滤器&#xff0c;也可以使用记录键范围修剪候选文件.原理为计算RecordKey的hash值然后将其存储到bitmap中&#xff0c;为避免hash冲突一般选择计算3次 HoodieKey 主键信息&#xff1a;主要包含recordKey 和p…

[ IFRS 17 ] 新准则下如何确认保险合同

本系列文章&#xff1a;[ IFRS 17 ] 针对保险新准则 IFRS 17 进行一些列文章更新。如若文中如有所疑问或者不同见解&#xff0c;欢迎留言互动交流。 注&#xff1a;本系列文章受众群体较小众&#xff0c;如若对你感到不适&#xff0c;请立刻点击右上角的 【】 本系列文章适用…

RDD持久化原理和共享变量

&#xff08;一&#xff09; RDD持久化原理 Spark中有一个非常重要的功能就是可以对RDD进行持久化。当对RDD执行持久化操作时&#xff0c;每个节点都会将自己操作的RDD的partition数据持久化到内存中&#xff0c;并且在之后对该RDD的反复使用中&#xff0c;直接使用内存中缓存…

互联网工程师 1480 道 Java 面试题及答案整理 ( 2023 年 整理版)

最近很多粉丝朋友私信我说&#xff1a;熬过了去年的寒冬却没熬过现在的内卷&#xff1b;打开 Boss 直拒一排已读不回&#xff0c;回的基本都是外包&#xff0c;薪资还给的不高&#xff0c;对技术水平要求也远超从前&#xff1b;感觉 Java 一个初中级岗位有上千人同时竞争&#…

安卓逆向_4 --- 定位关键Smali、monitor使用、log插桩、栈追踪、methodprofiling(方法分析)

1、快速定位关键 smali 代码 1.分析流程 搜索特征字符串 搜索关键api 通过方法名来判断方法的功能 2.快速定位关键代码 反编译 APK 程序&#xff0c;AndroidManifest.xml > 包名/系统版本/组件 程序的主 activity(程序入口界面) 每个…

Allegro如何画半圆形的线操作指导

Allegro如何画半圆形的线操作指导 在用Allegro设计PCB的时候,在某些应用场合会需要画半圆形,如下图 如何画半圆形,具体操作如下 点击Add点击Arc w/Radius

WebRTC QoS方法之Pacer实现

本文将解读WebRTC中Pacer算法的实现。WebRTC有两套Pacer算法&#xff1a;TaskQueuePacedSender、PacedSender。本文仅介绍PacedSender的实现。&#xff08;文章中引用的WebRTC代码基于master&#xff0c;commit&#xff1a;3f412945f05ce1ac372a7dad77d85498d23deaae源码分析&a…

算法练习(八)区域搜索

一、腐烂的橘子 1、题目描述&#xff1a; 在给定的 m x n 网格 grid 中&#xff0c;每个单元格可以有以下三个值之一&#xff1a; 值 0 代表空单元格&#xff1b; 值 1 代表新鲜橘子&#xff1b; 值 2 代表腐烂的橘子。 每分钟&#xff0c;腐烂的橘子 周围 4 个方向上相邻 的…