【C++】stack | queue | priority_queue | deque

一、stack栈

介绍

1.栈是一种特殊的线性表,其元素遵循“后进先出”的原则,即仅允许在在表的一端进行插入、删除操作,这一模式被称为“后进先出”或LIFO(last in fisrt out)。

2.从底层实现来看,stack是作为容器适配器被实现的,什么是容器适配器?我们来解释一下先。

先来看看我们身边的适配器。比方说,你有注意到笔记本电脑的充电器吗?

其实,笔记本的充电器就是一个适配器。适配器要做的就是电压的转换。一般来说,电脑的电池电压为14V左右,而标准电压为220V,要想给电脑充电,这就需要对标准电压进行转换,这就是适配器发挥的作用,适配就是转换的意思。

适配器是一种设计模式,该模式将一个类的接口转换为用户希望的另一个类的接口,你可以认为适配器是一种转换器。

容器适配器,能让程序员选择一种合适的底层数据结构。如stack,之前我们写c语言时,要用到栈的数据结构时,是不是得还先模拟一个栈出来?

那现在就不用这么麻烦了,要用到栈和队列的地方,直接用就行

3.stack的底层容器可以是 任何容器类模板 or 其他特定的容器类。

这些容器类应该支持以下操作:

empty:判空操作

back:获取尾部元素操作

push_back:尾部插入元素操作

pop_back:尾部删除元素操作

栈的底层实现(简易版):

可以看到,栈的这些方法都是直接复用容器的方法,体现了代码复用的思想。

这里的Container就是容器类,它可以是vector、deque、list,也可以省略不传。若省略,那默认情况下是deque类。

使用

使用时要包<stack>头文件。

栈提供的方法有:判空、取大小、取栈顶数据、压栈(插入)、出栈(删除)、交换 还有emplace安放。

这里对其中几点方法做个说明:

1.构造函数:

stack<Type, Container> (<数据类型,容器类型>) stackName;

数据类型一定要有,容器类型可以是vector、deque、list。(容器类型可省略,默认是deque)

2.在用top()前,得判断下栈是否为空。只有不为空的时候,才能调用top(),不然会发生段错误。

这是因为,在栈为空时,stack 的top函数返回的是超尾-1,而不是NULL。

3.和上面的top一样,在调用pop函数前,也要确保栈中至少有一个元素。

如果栈为空,调用pop会抛出EmptyStackException异常。

4.关于emplace,意为“安放”,就是说,构造一个新的元素放入栈顶。emplace和push很像,这里做一下区分。

push的话,得是你先构造好对象,然后push插进栈里;emplace则是主动帮你调用构造函数,构造出对象,然后插进栈里。

示例:

#include<iostream>
#include<stack>
using namespace std;
int main() {
    stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
​
    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    cout << endl;
    return 0;
}

从底层来看,Container是一个模板,你传数组or链表都可以。

来看看它是怎么适配的:

二、queue队列

介绍

1.队列是一种特殊的线性表,它只能在队尾插入数据,队头出数据,这一模式被称为“先进先出”或FIFO(first in first out)。

2.从底层实现来看,queue也是作为容器适配器被实现的。

展示下queue简易版的底层实现:

底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。

该底层容器应至少支持以下操作:

empty:检测队列是否为空

size:返回队列中有效元素的个数

front:返回队头元素的引用

back:返回队尾元素的引用

push_back:在队列尾部入队列

pop_front:在队列头部出队列

标准容器类deque和list满足了这些要求。也就是说,Container的位置可以传deque/ list过去,也可以省略不传。当省略不传时,那默认Container是deque

(为什么vector不满足呢?因为vector没有头删。之前我们说过,vector嫌头删效率低就没实现)

使用

使用时要包头文件<queue>。

根据队列的特性,实现的方法有:判空、取大小、取队头、取队尾、(队尾)插入、(队头)删除、交换 和emplace安放。

几点说明:

1.构造函数:

queue<Type, Container> (<数据类型,容器类型>) queueName;

在初始化时必须要有数据类型,容器可省略,省略时默认为deque 类型。

所以构造一个queue可以是这样两种情况:

queue<int> q;
​
queue<int,list<int>> q;

示例:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
int main() {
    queue<int> q;
    q.push(1);
    q.push(2);
    q.push(3);
​
    while (!q.empty()) {
        cout << q.front()<<" ";
        q.pop();
    }
    cout << endl;
    return 0;
}

这张图说明了,为什么deque是容器适配器:

三、priority_queue优先级队列

介绍

1.优先级队列是一种特殊的队列结构,它队列中的顺序并非插入的顺序,而是按权重来排序。

它给每个元素加上了优先级,每次出队的元素是队列中优先级最高的那个元素。

注:不是越大的元素,优先级就越高。

那优先级是如何评定的呢?其实,优先级队列在创建伊始,它的优先级就已经定好了,默认为降序。

那要如何排升序呢?一会会在仿函数那说到。

2.优先级队列 底层被实现为容器适配器。

底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。

容器应该可以通过随机访问迭代器访问,并支持以下操作:

empty():检测容器是否为空

size():返回容器中有效元素个数

front():返回容器中第一个元素的引用

push_back():在容器尾部插入元素

pop_back():删除容器尾部元素

容器类vector和deque满足这些需求。如果容器类被省略不写,那默认使用vector。

3.需要支持随机访问迭代器,以便始终在内部保持堆结构。

priority_queue是用堆实现的,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。

注:默认情况下priority_queue是大堆。

使用

使用时要包头文件<queue>。

注:取顶端数据不是用front了!是用top。

示例:

#include<iostream>
#include<queue>
using namespace std;
int main() {
    priority_queue<int> q;
    q.push(4);
    q.push(12);
    q.push(3);
​
    while (!q.empty()) {
        cout << q.top()<<" ";
        q.pop();
    }
    cout << endl;
    return 0;
}

模拟实现

priority_queue具有队列的所有特性,只是在此基础上又在内部加入了排序。它的底层原理是用堆实现的。

现在我们来模拟实现一下它的简易版,这样能更好地理解它的底层原理。

➡️复用代码

先把top、empty这种,通过复用代码而实现的 函数给写了:

#pragma once
#include<iostream>
#include<vector>
using namespace std;
namespace jzy
{
    template<class T,class Container = vector<T>>  //T是优先级队列中的 数据存储类型
    class priority_queue                            //Container是优先级队列中的数据结构
    {
    public:
        void push(const T& val) {}
        void pop() {}
        T top() {
            return _con.front();   //直接复用容器的方法
        }
        bool empty() {
            return _con.empty();
        }
        size_t size() {
            return _con.size();
        }
​
    private:
        T _val;
        Container _con;
    };
}

➡️push

push的实现:(大根堆)先尾插再向上调整。

void AdjustUp(Container& _con) {
    int child = _con.size()-1;
    int parent = (child - 1) / 2;
​
    while (child > 0){
        if (_con[child] > _con[parent]) {
            swap(_con[child], _con[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else {
            break;
        }
    }
}
​
void push(const T& val) {
    _con.push_back(val);
    AdjustUp(_con);
}

➡️pop

pop的实现:先把首尾交换,让优先级最高的元素来到队尾,以便尾删。再向下调整。

void AdjustDown(Container& _con) {
    int parent = 0;
    int child = 2 * parent + 1;
​
    while (child < _con.size()) {
        if (child + 1 < _con.size() && _con[child + 1] > _con[child]) {
            child++;
        }
​
        if(_con[parent] <_con[child]) {
            swap(_con[parent], _con[child]);
            parent = child;
            child = 2 * parent + 1;
        }
        else {
            break;
        }
    }
}
​
void pop() {
    swap(_con.front(), _con[_con.size()-1]);
    _con.pop_back();
    AdjustDown(_con);
}

➡️如何排降序

目前排的是升序,那要想排降序,要怎么办呢?

我倒是有个很朴素的办法:每次插入元素,向上调整时,我把小的往上调,不就行了吗?

其实就只要改个符号:

void AdjustUp(Container& _con) {
    int child = _con.size()-1;
    int parent = (child - 1) / 2;
​
    while (child > 0){
        if (_con[child] < _con[parent]) {    //这里原本是>,现在给改成<。越小越往上调
            swap(_con[child], _con[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }
        else {
            break;
        }
    }
}

但是,这种方法并不好。我想排升序,要把符号改成>;想排降序,又要改回<,麻烦。

能不能用泛型解决呢?

其实在STL库里,就是用泛型解决的,我们来学习一下。

STL里,priority_queue有3个模板参数:

template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type>>

第三个模板参数Compare,定义了比较的方式。就是说,我们通过定义Compare类,来制定比较的规则。

不过,要想理解参数Compare,我们得先学习一个知识点:仿函数。

仿函数

仿函数(functor)不是函数,它描述的是对象,只不过这个对象重载了operator(),所以它使用起来像函数一样。

比方说,你定义了一个A类,用A实例化出对象a。此时a是一个普通的对象。

但当你在A里面重载了operator()(即函数调用运算符),那a就是仿函数,就可以这样用:

class A
{
  ……
  int operator() (int x,int y){
    return x+y;
  }
};
int mian(){
    A a;
    cout<<a(1,2);   //A实例化出的对象a,就可以像函数一样使用,a就叫仿函数
    return 0;
}

关于():

我一开始大为不解,怎么圆括号()也能重载?

没错!()叫做”函数调用运算符“,我们在调用函数时,往往要传参,此时就用到这个运算符。

所以说,要想让对象成为仿函数,只需要在它的类里面重载operator()。

用仿函数实现升降序

先实现两个类:Greater、Less,分别用于定义升序、降序(这俩类实现为模板)。

然后在priority_queue类中增加一个模板参数compare,此参数用于接收 比较方式 的类型(是升序,还是降序)。

compare默认为降序。当我们想要升序,那就在传参时,实例化出Greator类型的对象,传过去即可。

//先实现Less和Greater两个类,来定义比大小的规则
template<class T>
struct Less    //less意为越来越小,即降序
{
    bool operator() (const T& x, const T& y) {
        return x > y;
    }
};
​
template<class T>
struct Greater     //升序
{
    bool operator() (const T& x, const T& y) {
        return x < y;
    }
};
​
template<class T,class Container = vector<T>,class Compare=Less<T>>  //增加模板参数,默认为降序
class priority_queue
{
    Compare com;
public:
    void AdjustUp(Container& _con) {
        int child = _con.size()-1;
        int parent = (child - 1) / 2;
​
        while (child > 0){
            if (com(_con[child],_con[parent])) {
                swap(_con[child], _con[parent]);
                child = parent;
                parent = (child - 1) / 2;
            }
            else {
                break;
            }
        }
    }
    void AdjustDown(Container& _con) {
        Compare com;
        int parent = 0;
        int child = 2 * parent + 1;
​
        while (child < _con.size()) {
            if (child + 1 < _con.size() && com(_con[child+1],_con[child])) {   
                child++;
            }
​
            if(com(_con[child], _con[parent])) {
                swap(_con[parent], _con[child]);
                parent = child;
                child = 2 * parent + 1;
            }
            else {
                break;
            }
         }
    }
    void push(const T& val) {
        _con.push_back(val);
        AdjustUp(_con);
    }
    void pop() {
        swap(_con.front(), _con[_con.size()-1]);
        _con.pop_back();
        AdjustDown(_con);
    }
    ……
};

注:因为我们展开了STL库,所以在命名Less/Greater时,注意不要写成less/greater,不然就和库里的重名了,编译器就就晕了 不知道用哪个。

测试:排升序

#include"priority_queue.h"
using namespace jzy;
int main() {
    priority_queue<int,vector<int>,Greater<int>> pq;
    pq.push(7);
    pq.push(3);
    pq.push(8);
    pq.push(2);
    pq.push(12);
    pq.push(6);
    pq.push(3);
​
    while (!pq.empty()) {
        cout << pq.top() << " ";
        pq.pop();
    }
    return 0;
}

模拟实现的总代码

priority_queue.h:

#pragma once
#include<iostream>
#include<vector>
using namespace std;
namespace jzy
{
    template<class T>
    struct Less    //less意为越来越小,即降序
    {
        bool operator() (const T& x, const T& y) {
            return x > y;
        }
    };
​
    template<class T>
    struct Greater     //升序
    {
        bool operator() (const T& x, const T& y) {
            return x < y;
        }
    };
​
    template<class T,class Container = vector<T>,class Compare=Less<T>>
    class priority_queue
    {
        Compare com;
    public:
        void AdjustUp(Container& _con) {
            int child = _con.size()-1;
            int parent = (child - 1) / 2;
​
            while (child > 0){
                if (com(_con[child],_con[parent])) {
                    swap(_con[child], _con[parent]);
                    child = parent;
                    parent = (child - 1) / 2;
                }
                else {
                    break;
                }
            }
        }
        void AdjustDown(Container& _con) {
            Compare com;
            int parent = 0;
            int child = 2 * parent + 1;
​
            while (child < _con.size()) {
                if (child + 1 < _con.size() && com(_con[child+1],_con[child])) {   
                    child++;
                }
​
                if(com(_con[child], _con[parent])) {
                    swap(_con[parent], _con[child]);
                    parent = child;
                    child = 2 * parent + 1;
                }
                else {
                    break;
                }
            }
        }
        void push(const T& val) {
            _con.push_back(val);
            AdjustUp(_con);
        }
        void pop() {
            swap(_con.front(), _con[_con.size()-1]);
            _con.pop_back();
            AdjustDown(_con);
        }
        T top() {
            return _con.front();
        }
        bool empty() {
            return _con.empty();
        }
        size_t size() {
            return _con.size();
        }
​
    private:
        T _val;
        Container _con;
    };
}

四、deque双端队列

(这个容器了解即可,这个容器很少用)

介绍

1.deque是“double-ended queue”的缩写。

虽然叫双端队列,但它和队列没啥关系。deque是一种双开口的"连续"空间的数据结构。双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1)。

deque支持随机访问,也支持任意位置的插入、删除。似乎,deque结合了vector和list的优点。

但,deque真的是完美的吗?

并不是!deque随机访问的效率比较低。至于为什么,那先让我们了解一下它的底层实现。

底层实现

和vector不同,deque的底层并不是 真正意义上的连续的空间,而是由一段段连续的小空间拼接而成的,这些小空间不一定是连续的,可能是位于内存的不同区域。如图:

这里的map,并不是STL里的map容器。而是数组,数组的每个元素都是 指向另一块连续空间(缓冲区buffer)的 指针。我们插入的元素,实际上是存进buffer里的。

可见,deque对空间利用率很高,几乎没什么空间的浪费。

➡️如何进行扩容呢?

当一个buffer存满了,要尾插,此时不会给当前buffer扩容(因为它是大小固定的),而是再开一个buffer空间,尾插的内容存进新的buffer里。指针数组的下一个元素指向新的buffer。头插同理。下图可以帮助我们理解:

可见,头插、尾插不需要挪动数据,效率自然高。

如果map满了,那就再开一个更大的map,如何把数据拷到新的map里。

➡️关于deque的迭代器,它的原理很复杂,由四个指针组成。node指向中控区的指针数组,first和node分别指向一段空间的开始和结束,cur指向访问到的位置。如果访问的元素不在当前空间,cur就等于下一个空间的first,再继续访问。

➡️当需要随机访问时,deque得先完整复制到一个vector上,如何vector排序后,再复制deque。这就导致了deque的随机访问效率较vector更低。

关于deque的小结

优势:1.相比vector,deque的头插、头删不需要挪动数据,而是直接开新buffer插入,效率很高;

在扩容时,也无需开新空间、拷数据,而是直接开个新buffer,效率高。

2.相比list,deque的底层是连续的存指针的map数组,空间利用率比较高,不需要存储额外字段。况且,deque还支持随机访问,虽然效率不高。

劣势:1.不适合遍历。

因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下。

而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。

2.中间插入、删除的效率并不高。(因为下标需要经过计算,并且要挪动元素)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

adb and 软件架构笔记

Native Service&#xff0c;这是Android系统里的一种特色&#xff0c;就是通过C或是C代码写出来的&#xff0c;供Java进行远程调用的Remote Service&#xff0c;因为C/C代码生成的是Native代码&#xff08;机器代码&#xff09;&#xff0c;于是叫Native Service。 native服务…

真正解决jellyfin硬解码转码

前段时间入手一个DS423集成显卡UHD600&#xff0c;搭了一个jellyfin&#xff0c;发现网上关于硬解码的教程基本都存在问题&#xff0c;没有真正解决我的硬解码问题。经过一系列分析修改&#xff0c;最终实现硬解码。先贴效果图&#xff1a; 下载安装jellyfin这里就不叙述&#…

php加密解密的用法(对称加密,非对称加密)

加密和摘要的区别 ***摘要&#xff1a;是从已知的数据中&#xff0c;通过摘要计算出一个值&#xff0c;一个数据对应一个或多个摘要的值 *** 比如&#xff1a;md5 和 sha1 sha256 hash 就是得到一个特定的值 &#xff0c;同一个数据得到的md5 是一样的&#xff0c;不会改变的 比…

python自动化测试(十一):写入、读取、修改Excel表格的数据

目录 一、写入 1.1 安装 xlwt 1.2 增加sheet页 1.2.1 新建sheet页 1.2.2 sheet页写入数据 1.2.3 excel保存 1.2.4 完整代码 1.2.5 同一坐标&#xff0c;重复写入 二、读取 2.1 安装读取模块 2.2 读取sheet页 2.2.1 序号读取shee页 2.2.2 通过sheet页的名称读取she…

实用知识(工作中常用)

mybatis-plus联表查询 pom.xml坐标 <!-- mybatis-plus-join --> <dependency><groupId>com.github.yulichang</groupId><artifactId>mybatis-plus-join</artifactId><version>1.2.4</version> </dependency>使用步骤&…

【可视化Java GUI程序设计教程】第5章 Swing容器的使用

Swing采用自顶向下的方式构建GUI&#xff0c;即先创建容器&#xff0c;再向容器中添加组件。 “组件”面板中的Swing容器 5.1 面板容器&#xff08;JPanel&#xff09; 5.5.1 使用方法 创建面板有以下两种方法 &#xff08;1&#xff09;创建一个窗体&#xff08;JFrame&…

SAP BASIS SET_PARAMETER_ID_TOO_LONG

ji 原因 DATA:curvbelnid(40) TYPE c,"问题在这里curposnrid(40) TYPE c. "问题在这里curvbelnid sy-uname && VN.curposnrid sy-uname && PR.SET PARAMETER ID curvbelnid FIELD i_vbeln . SET PARAMETER ID curposnrid FIELD i_posnr . 改成 D…

ci-cd的流程

1、项目在gitlab上&#xff0c;从gitlab上使用git插件获取源码&#xff0c;构建成war包&#xff0c;所以使用tomcat作为运行环境 发布 &#xff1a;使用maven插件发布&#xff0c;使用ssh连接。

【Android】画面卡顿优化列表流畅度一

卡顿渲染耗时如图&#xff1a; 卡顿表现有如下几个方面&#xff1a; 网络图片渲染耗时大上下滑动反应慢&#xff0c;甚至画面不动新增一页数据加载渲染时耗时比较大&#xff0c;上下滑动几乎没有反应&#xff0c;画面停止没有交互响应 背景 实际上这套数据加载逻辑已经运行…

支持向量机 (SVM):初学者指南

照片由 Unsplash上的 vackground.com提供 一、说明 SVM&#xff08;支持向量机&#xff09;简单而优雅用于分类和回归的监督机器学习方法。该算法试图找到一个超平面&#xff0c;将数据分为不同的类&#xff0c;并具有尽可能最大的边距。本篇我们将介绍如果最大边距不存在的时候…

Failed to connect to github.com port 443:connection timed out

解决办法&#xff1a; 步骤1&#xff1a; 在这里插入图片描述 步骤2&#xff1a; -步骤3 &#xff1a;在git终端中执行如下命令&#xff1a; git config --global http.proxy http:ip:port git config --global https.proxy http:ip:port git config --global http.proxy htt…

重磅发布 OpenAI 推出用户自定义版 ChatGPT

文章目录 重磅发布 OpenAI 推出用户自定义版 ChatGPT个人简介 重磅发布 OpenAI 推出用户自定义版 ChatGPT OpenAI 首届开发者大会 (OpenAI DevDay) 于北京时间 11 月 7 日凌晨 02:00 开始&#xff0c;大会上宣布了一系列平台更新。其中一个重要更新是用户可以创建他们自己的自定…

智慧农业:农林牧数据可视化监控平台

数字农业是一种现代农业方式&#xff0c;它将信息作为农业生产的重要元素&#xff0c;并利用现代信息技术进行农业生产过程的实时可视化、数字化设计和信息化管理。能将信息技术与农业生产的各个环节有机融合&#xff0c;对于改造传统农业和改变农业生产方式具有重要意义。 图扑…

SysML理论知识

概述 由来 长期以来系统工程师使用的建模语言、工具和技术种类很多&#xff0c;如行为图、IDEF0、N2图等&#xff0c;这些建模方法使用的符号和语义不同&#xff0c;彼此之间不能互操作和重用。系统工程正是由于缺乏一种强壮的标准的建模语言&#xff0c;从而限制系统工程师和…

Makefile 介绍

目录 一、Makefile 的规则 二、一个示例 三、make 是如何工作的 四、makefile 中使用变量 五、让 make 自动推导 六、另类风格的 makefile 七、清空目标文件的规则 make 命令执行时&#xff0c;需要一个 Makefile 文件&#xff0c;以告诉 make 命令需要怎么样的去编译和…

用Python实现朴素贝叶斯垃圾邮箱分类

一、实验目的 通过本实验&#xff0c;旨在使用朴素贝叶斯算法实现垃圾邮箱分类&#xff0c;并能够理解并掌握以下内容&#xff1a; 了解朴素贝叶斯算法的基本原理和应用场景。 学习如何对文本数据进行预处理&#xff0c;包括去除标点符号、转换为小写字母、分词等操作。 理解特…

git 生成公钥

1、通过命令 ssh-keygen 生成 SSH Key&#xff1a; ssh-keygen -t ed25519 -C "Gitee SSH Key" 三次回车 2、查看生成的 SSH 公钥和私钥&#xff1a; ls ~/.ssh/ 3、把公钥设置到git id_ed25519.pub 4、测试 ssh -T gitgitee.com 成功&#xff01;&#xff01;&…

Hive3 on Spark3配置

1、软件环境 1.1 大数据组件环境 大数据组件版本Hive3.1.2Sparkspark-3.0.0-bin-hadoop3.2 1.2 操作系统环境 OS版本MacOSMonterey 12.1Linux - CentOS7.6 2、大数据组件搭建 2.1 Hive环境搭建 1&#xff09;Hive on Spark说明 Hive引擎包括&#xff1a;默认 mr、spark、…

从vue源码中看diff算法

一、v-for必须要指定key&#xff0c;其作用是什么&#xff1f; 在源码中有一个函数为&#xff0c;其中就是通过判断两个vnode的type和key进行判断&#xff0c;如果这两个属性相同&#xff0c;那么这两个vnode就是相同&#xff0c;所以在设置key的时候也不可以设置为object等无…

Fortigate SSL VPN路径遍历漏洞(CVE-2018-13379)

Fortigate SSL VPN路径遍历漏洞&#xff08;CVE-2018-13379&#xff09; 免责声明漏洞描述漏洞影响漏洞危害网络测绘Fofa: body"FortiToken clock drift detected" 漏洞复现1. 访问链接查看是否存在漏洞2. 查看用户名密码3. 登录后台 免责声明 仅用于技术交流,目的是…