stack、queue(priority_queue)的模拟实现和deque的简单介绍

news2025/3/13 16:00:05

stack和queue(priority_queue)

1. 容器适配器

适配器(Adapter):一种用来修饰容器(Containers)或仿函数(Functors)或迭代器(Iterator)接口的东西。

适配器是一种设计模式,该模式将一个类的接口转换成客户希望的另外一个接口。

现实中拿插座来说,一个插座可以适配不同的插头,以适应不同电器的电压或者其他要求。

在这里插入图片描述

STL中的适配器相当于一个插座,以适应不同容器,可以"插入"vector, 也可以插入 list,等等。


虽然stackqueue也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器

例如stack只提供LIFO(先进先出)的概念,对其他容器的接口进行了包装,并没有完全重新创建了一个容器。在STL中,stackqueue默认使用deque,而priority_queue默认使用vector

在这里插入图片描述

类似于listiterator的实现,实际上是更高一层的封装。__list_iterator是对Node*的封装,以支持运算符重载;相对应的,stack是对其Container的封装,以支持LIFO的上下文。

2. stack 的介绍和实现

2.1 stack 的介绍

stack文档介绍

  1. stack 是一种容器适配器,设计用于在具有后进先出操作的上下文中,其删除和插入只能从容器的末尾进行操作。

  2. stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,元素特定从容器的尾部(即栈顶)被压入和弹出。

  3. stack的底层容器可以是任何标准的容器类模板或者一些特殊设计的容器类。这些容器应该支持如下操作:

    empty()			// 判空操作
    size()  		// 得到元素个数操作
    back()  		// 得到容器尾部元素操作
    push_back()		// 尾插操作
    pop_back()		// 尾删操作
    
  4. 标准容器list,vectordeque均满足这些要求。默认情况下,如果没有为stack指定特定的底层容器,默认使用deque作为底层容器。

在这里插入图片描述

2.2 stack 的实现

stack 有如下成员函数(不包括C++11)

在这里插入图片描述

利用适配器概念进行实现:

#pragma once
#include <deque>

namespace wr
{
  template <class T, class Con = deque<T>>	
  class stack
  {
  public:
    void push(const T &x)
    {
      _con.push_back(x);
    }

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

    T &top()
    {
      return _con.back();
    }

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

    size_t size()
    {
      return _con.size();
    }

    bool empty()
    {
      return _con.empty();
    }

  private:
    Con _con;	// 底层的容器类对象作为成员变量
  };
}

可以看到,适配器模式下设计的stack十分简洁,相比用C语言实现的stack,完全体现了适配器模式的优势。

总结:从使用者角度来看,不需要知道stack底层使用的是什么容器实现,只需要知道其可以实现对应适合LIFO的操作。无论是STL中的容器,还是自定义类容器,只要拥有对应的接口,就可以作为stack的底层容器。


2. queue的介绍和实现

2.1 queue 的介绍

queue文档介绍

  1. queue 是一种容器适配器,专门用于在FIFO(先进先出)的上下文中操作,其中从容器一端插入元素,另一端提取元素。

  2. queue作为容器适配器实现,用一个底层是特定容器类的封装类来实现,queue提供了特定了成员函数来访问它的元素。元素队尾入队列,从队头出队列。

  3. queue的底层容器可以是标准容器中的一种,或者是经过特别设计的容器类。底层容器应该至少指出如下操作:

    empty()				// 判空操作
    size()				// 得到元素个数操作
    front()				// 得到容器头部元素
    back()				// 得到容器尾部元素
    push_back()			// 容器尾插元素操作
    pop_back()			// 容器头删元素操作
    
  4. STLdequelist可以满足这些需求。默认情况下,如果没有容器类被指定,默认使用deque作为queue的底层容器。

在这里插入图片描述

2.2 queue 的实现

queue有如下成员函数:

在这里插入图片描述

实现如下:

#pragma once

#include <deque>

namespace wr
{
  template <class T, class Con = deque<T>>
  class queue
  {
  public:
    void push(const T &x)
    {
      _con.push_back(x);
    }

    void pop()
    {
      _con.pop_front();
    }

    T &back()
    {
      return _con.back();
    }

    const T &back() const
    {
      return _con.back();
    }

    T &front()
    {
      return _con.front();
    }

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

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

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

  private:
    Con _con;
  };	
}

stack的实现差不多,唯一需要注意的是 queue的底层容器不能是vector,因为vector类根本不支持pop_front()的操作。并且vector的头删效率太低,需要挪动后面的所有元素。

3. priority_queue 的介绍和使用

3.1 priority_queue 的介绍

priority_queue文档介绍

  1. priority_queue(优先队列)是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含元素最大的。

  2. 此上下文类似于heap(堆),在这种上下文中,元素可以随时被插入到堆中,并且只有最大的堆元素可以被访问(位于优先队列的top())。

  3. priority_queue被作为容器适配器实现,并将特定容器类封装作为其底层容器类,同时提供了特定的一组成员函数来访问其元素。元素从特定容器的“尾部”弹出,其成为优先队列的顶部。

  4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:

    empty()				// 判空操作
    size()				// 得到元素个数操作
    front()				// 得到容器头部元素
    push_back()			// 容器尾插元素操作
    pop_back()			// 容器头删元素操作
    
  5. STLvectordeque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector

  6. 需要支持随机访问迭代器,一边始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。

优先级队列默认使用vector作为其底层的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue

需要注意的是,下面是优先级队列的模板参数,第三个参数默认使用less仿函数,以构造大堆。若指定Comparegreater仿函数,则构成小堆。

在这里插入图片描述

// 默认情况下构造小堆
priority_queue<int> pq1;		
// 相当于
priority_queue<int, vector<int>, less<int>> pq1;

// 指定底层容器构造大堆
priority_queue<int, deque<int>, greater<int>> pq2;

如果需要使用priority_queue存放自定义类元素,需要自行重载><


3.2 priority_queue 的实现

之前已经用C语言实现过堆,C++实现的重点是对STL的使用、仿函数的使用以及适配器模式概念的理解。

数据结构:堆的实现和堆排序及TopK问题

堆的插入元素和删除元素的时间复杂度是O(logN) ,默认情况下优先级队列是大堆,先不考虑使用仿函数去实现大小堆兼容的问题。首先先来实现大堆,最后在代码基础上加上仿函数的设计就可以了。


priority_queuequeue的区别主要是在pushpop上,优先级队列需要进行额外的向上调整向下调整以满足堆序。

push:先将元素尾插到容器,随后将该元素进行向上调整以满足堆序。

pop:直接将容器最后一个元素覆盖堆顶元素,随后对此时的堆顶元素进行向下调整以满足堆序。注意:不能直接将堆顶元素删除,因为不能保证左孩子和右孩子之间也满足堆序,两个孩子之间没有关系,它们只和他们的父亲有关系。

void push(const T &x)
{
	_con.push_back(x);
	adjust_up(_con.size() - 1);
}

void pop()
{
    assert(!_con.empty());
	_con[0] = _con[_con.size() - 1];
	_con.pop_back();
	adjust_down(0);
}

随后就是向上调整和向下调整算法,这里先直接用不等号直接实现大堆。

// 向上调整算法
void adjust_up(int child)
{
  int parent = (child - 1) / 2;
  while (parent >= 0)
  {
    // 默认less,大堆,a[child] > a[parent] 交换
    if (_con[parent] < _con[child])
    {
      swap(_con[parent], _con[child]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}

// 向下调整算法
void adjust_down(int parent)
{
  int child = parent * 2 + 1;
  while (child < _con.size())
  {
    // 默认less, 如果_con[child+1] > _con[child], 更新child
    if (child + 1 < _con.size() && _con[child] < _con[child+1])
    {
      child++;
    }

    // 如果_con[parent] < _con[child] 交换
    if (_con[parent] < _con[child])
    {
      swap(_con[parent], _con[child]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}

具体实现细节在以前博客,这里不过多赘述。


随后是其他一些简单接口。注意:top()的参数和返回值都是const,保证数据不被修改。

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

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

const T &top() const
{
  return _con.front();
}

这是固定死了的大堆,如果要使用小堆,难道需要再写一个类或者在需要时再修改吗?答案显然是否定的。

在C语言阶段,可以用函数指针进行实现,通过对comp()函数的回调实现特定的需求。例如实现bubble_sort()函数时通过传入函数指针来指定比较方法。

//通用冒泡排序
void bubble_sort(void* base, size_t num, size_t size, int (*cmp)(const char*, const char*))

C++中有更好的实现方法,称为仿函数。

3.2.1 仿函数

仿函数就是仿造的函数,它并不是一个真正意义上的函数。它是一个类中的operator()重载,使之它具有类似于函数的功能。

就如仿羊皮一样,仿函数不是函数,而是一个类中的operator()重载,既然在类中,那么就可以使用模板参数,这相比函数指针就更加的范式化。

下面是greater和less的模拟实现

template <class T>
struct greater
{
    // 运算符()重载
    bool operator()(const T &a, const T &b)
    {
      return a > b;
    }
};

template <class T>
struct less
{
    bool operator()(const T &a, const T &b)
    {
      return a < b;
    }
};

使用仿函数就可以构建一个对应的类对象,随后调用operator()即可

less<int> le;
cout << le(100, 200) << endl;

将仿函数类模板放入priority_queue的模板参数列表中,就可以实现在构建优先级队列的时候指定堆序了。因为模板参数传入的是类型,在编译的时候进行传递,随后程序运行都是基于传入模板的参数类型下进行运行的。

下面是引入了仿函数的完整priority_queue完整实现:

template <class T>
class greater
{
public:
    bool operator()(const T &a, const T &b)
    {
      return a > b;
    }
};

template <class T>
class less
{
public:
    bool operator()(const T &a, const T &b)
    {
      return a < b;
    }
};

template <class T, class Con = vector<T>, class Compare = less<T>>
class priority_queue
{
    void adjust_up(int child)
    {
      int parent = (child - 1) / 2;
      while (parent >= 0)
      {
        // 默认less,大堆,a[child] > a[parent] 交换
        if (_comp(_con[parent], _con[child]))
        {
          swap(_con[parent], _con[child]);
          child = parent;
          parent = (child - 1) / 2;
        }
        else
        {
          break;
        }
      }
    }

    void adjust_down(int parent)
    {
      int child = parent * 2 + 1;
      while (child < _con.size())
      {
        // 默认less, 如果_con[child+1] > _con[child], 更新child
        if (child + 1 < _con.size() && _comp(_con[child], _con[child + 1]))
        {
          child++;
        }

        if (_comp(_con[parent], _con[child]))
        {
          swap(_con[parent], _con[child]);
          parent = child;
          child = parent * 2 + 1;
        }
        else
        {
          break;
        }
      }
    }

public:
    template <class InputIterator>
    priority_queue(InputIterator first, InputIterator last)
    {
      while (first < last)
      {
        push(*first);
        first++;
      }
    }

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

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

    const T &top() const
    {
      return _con.front();
    }

    void push(const T &x)
    {
      _con.push_back(x);
      adjust_up(_con.size() - 1);
    }

    void pop()
    {
      _con[0] = _con[_con.size() - 1];
      _con.pop_back();
      adjust_down(0);
    }

private:
    Con _con;
    Compare _comp;
};

4. deque 的简单介绍

deque 的文档介绍

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

在这里插入图片描述

4.1 deque 的实现原理

deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的。

实际的deque类似于一个动态的二维数组,如下图所示:

在这里插入图片描述

所有的小空间使用一个指针数组map进行管理的,相当于一个中控。

第一块空间的首地址存放在map的中间,第二块空间的首地址在map下一个位置,若需要头插元素,则新创建的空间首地址放在map中第一块空间首地址的前面。


双端队列底层是一段假象的连续空间,实际上是分段连续的,为了维护其“整体连续”以及随机访问的假象,这个任务落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

在这里插入图片描述

queueiterator封装了四个指针,分别是cur、first、last、node;

first:当前元素所处缓冲区的开始

last:当前元素所处缓冲区的结束

cur:当前元素值

node:指针的指针,指向map中指向buffer的指针


deque通过迭代器startfinish维护其整体结构:

在这里插入图片描述

deque::begin()返回迭代器startdeque::end()返回迭代器finish,这两个迭代器都是deque的成员变量。

如果要头插元素,而此时迭代器startnode指向为第一个buffer的首地址,则需要创建一个新的buffer空间。node指向map前一个空间,*node指向新buffer的首元素地址,同时更新first、cur、last

4.2 deque 与 vector和 list 的比较

deque的优势就是头插头删,尾插尾删,只有O(1)的时间复杂度。

但由于其复杂的底层结构,导致实现其随机访问(即[])很困难,有以下两点不足,或者说是冲突:

  • 规定deque的每一个buffer的空间一样大(为n),那么访问第i个数据的思路:如果i不是第一个buffer中的数据,则i -= 第一个buffer的长度后,第i个数据的下标为[i/n, i%10]规定deque的每一个buffer不一样大,则需要依次将i减去buffer的长度,直至i小于下一个buffer的长度,才能找到第i个元素对应的位置
  • deque中间插入数据,若需要每个buffer的空间一样大,需要挪动所有该位置前面或者后面的数据。若不需要每个buffer空间一样大,则可以只挪动当前buffer的元素。

由此可以看到,对dequebuffer的长度是否一致的规定会影响随机访问中间插入元素的效率。但无论怎么说,deque的随机访问是始终不如vector的,中间插入元素是始终不如list的。

容器优点缺点
vector物理结构连续存储 1.下标随机访问 2.缓存命中率高1.前面部分插入删除效率低 2.扩容有损耗,还会存在一定程度空间浪费
list物理结构不一定连续 1. 任意位置插入删除效率高 2. 按需申请空间,1.不支持下标随机访问 2.缓存命中率高
deque1.支持下标随机访问 2. 扩容损耗小 3. 头部和尾部对元素处理效率高 4. 缓存命中率高1.中间插入效率低 2.下标随机访问不如vector

本章完。

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

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

相关文章

Linux:Win10平台上,用VMware安装Centos7.x及系统初始化关键的相关配置(分步骤操作,详细,一篇足以)

VMware安装Centos7.x镜像的详细步骤&#xff1a;VMWare安装Centos系统&#xff08;无桌面模式&#xff09; 我这里是为了安装Hadoop集群&#xff0c;所以&#xff0c;以下这些步骤是必须进行的 如果你是学习Linux&#xff0c;可以跳过非必须的那些配置项 我安装的版本是&…

水牛社靠谱吗,水牛社可以当做副业来做吗?

水牛社这个平台是否靠谱&#xff0c;能否作为副业的选择&#xff0c;一直是网友们热议的话题。实际上&#xff0c;水牛社是一个集合了众多网上赚钱活动任务和提供资源项目教程的综合性平台&#xff0c;它并非只局限于某一特定的项目&#xff0c;而是展现出多样化的特点。随着网…

总结Java中的synchronized

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 小比特 大梦想 目录 总结*synchronized**初识synchronized*使用synchronizedsynchronized的特性(1)可重入性(2)自适应过程(3)锁消除(4)非公平锁(5)互斥锁 总结synchronized 初识synchronized 通过一个线程不安…

光明与速度:AI网络中GPU与光模块的协奏曲

&#x1f3b6;在人工智能&#xff08;AI&#xff09;的世界里&#xff0c;GPU和光模块是实现高速计算和数据传输的关键。它们如同一场精心编排的交响乐&#xff0c;每个部分都不可或缺&#xff0c;共同创造出美妙的和谐。&#x1f3bc; GPU&#xff1a;AI网络的心脏&#x1f4…

Python-GEE遥感云大数据分析、管理与可视化

原文链接&#xff1a;Python-GEE遥感云大数据分析、管理与可视化https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247601238&idx2&sn6b0557cf61451eaff65f025d648da869&chksmfa820db1cdf584a76de953b96519704177e6206d4ecd47a2f2fabbcac2f7ea619b0bce184…

MATLAB中roots函数用法

目录 语法 说明 示例 二次多项式的根 四次多项式的根 提示 roots函数的功能是求解多项式的根。 语法 r roots(p) 说明 r roots(p) 以列向量的形式返回 p 表示的多项式的根。输入 p 是一个包含 n1 多项式系数的向量&#xff0c;以 xn 系数开头。0 系数表示方程中不存…

Linux的学习之路:19、进程信号(1)

摘要 今天这张说一下信号的一部分知识 目录 摘要 一、信号 1、生活角度的信号 2、技术应用角度的信号 3、注意 4、用kill -l命令可以察看系统定义的信号列表 5、信号处理常见方式概览 二、产生信号 1、通过终端按键产生信号 2、调用系统函数向进程发信号 3、由软件…

Fisher判别:理解数据分类的经典方法

在机器学习和统计分类的领域中&#xff0c;Fisher判别&#xff08;也称为Fisher线性判别分析&#xff09;是一种非常重要的方法&#xff0c;旨在从数据中提取重要特征&#xff0c;以实现对样本的分类。即Fisher判别分析&#xff08;Fisher Discriminant Analysis, FDA&#xff…

Midjourney-01 初试上手 注册使用并生成你的第一张AI图片 详细流程 提示词 过程截图 生成结果 付费文生图的天花板!

背景介绍 Midjourney是一款基于人工智能技术的绘画软件&#xff0c;利用深度学习算法来辅助用户进行绘画创作。这款软件能够通过用户输入的文本描述生成图像&#xff0c;支持多种生成方式&#xff0c;包括文字生成图片、图片生成图片和混合图片生成图片。 图像生成方式&#…

STM32之HAL开发——FSMC控制带控制器LCD(8080时序)

STM32FSMC模拟8080接口时序 ILI9341的8080通讯接口时序可以由STM32使用普通I/O接口进行模拟&#xff0c;但这样效率太低&#xff0c;STM32提供了一种特别的控制方法——使用FSMC接口实现8080时序。 由于FSMC外设可以用于控制扩展的外部存储器&#xff0c;而MCU对液晶屏的操作实…

Jackson 2.x 系列【31】Spring Boot 集成之字典翻译

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Jackson 版本 2.17.0 本系列Spring Boot 版本 3.2.4 源码地址&#xff1a;https://gitee.com/pearl-organization/study-jaskson-demo 文章目录 1. 场景描述2. 案例演示2.1 修改枚举2.2 定义注解…

JVM垃圾收集器--分区收集器

G1收集器 G1&#xff08;Garbage-First Garbage Collector&#xff09;在 JDK 1.7 时引入&#xff0c;在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。G1 有五个属性&#xff1a;分代、增量、并行、标记整理、STW。 分代 G1收集器 将内部分为多个大小相等的区域&#xff0c;另…

2024五一劳动节市集露营生活节活动策划方案

2024五一劳动节市集露营生活节&#xff08;向野而生 躺平生活节主题&#xff09;活动策划方案 方案页码&#xff1a;72页 文件格式&#xff1a;pptx 方案简介&#xff1a; 五一躺平生活节 咖啡一饮&#xff0c;书本一翻&#xff0c;轻松又自在,看着窗外的阳光&#xff0c;…

2024商业地产五一劳动节健康大会朋克养生市集活动策划方案

2024商业地产五一劳动节健康大会朋克养生市集&#xff08;带薪健康 快乐打工主题&#xff09;活动策划方案 活动策划信息&#xff1a; 方案页码&#xff1a;53页 文件格式&#xff1a;PPT 方案简介&#xff1a; 打工不养生 赚钱养医生 期待已久的五一假期&#xff0c; …

进口主食冻干高价是营销炒作还是物有所值?百利冻干真相揭秘分享

提及高端主食冻干&#xff0c;百利无疑也隶属其中。在各大平台上搜索“主食冻干”等相关词汇时&#xff0c;百利总能脱颖而出。在历次的双11、618等购物狂欢中&#xff0c;百利的销量都还不错。说明其高昂的价格也让许多宠物主人犹豫不决&#xff0c;只能等待大促时的优惠。那么…

【python】Python成语接龙游戏[1-3难度均有](源码+数据)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

好友关注-实现分页查询收邮箱

9.5好友关注-实现分页查询收邮箱 需求&#xff1a;在个人主页的“关注”卡片中&#xff0c;查询并展示推送的Blog信息&#xff1a; 具体操作如下&#xff1a; 1、每次查询完成后&#xff0c;我们要分析出查询出数据的最小时间戳&#xff0c;这个值会作为下一次查询的条件 2…

Midjourney是什么?Midjourney怎么用?怎么注册Midjourney账号?国内怎么使用Midjourney?多人合租Midjourney拼车

Midjourney是什么 OpenAI发布的ChatGPT4引领了聊天机器人的竞争浪潮&#xff0c;随后谷歌推出了自己的AI聊天机器人Bard&#xff0c;紧接着微软推出了Bing Chat&#xff0c;百度也推出了文心一言&#xff0c;这些聊天机器人的推出&#xff0c;标志着对话式AI技术已经达到了一个…

每日一题 —— 最大子数组之和(动态规划)

1.链接 53. 最大子数组和 - 力扣&#xff08;LeetCode&#xff09; 2.描述 3.思路 4.参考代码 class Solution { public:int maxSubArray(vector<int>& nums) {int n nums.size();vector<int> dp(n1,0);int ret INT_MIN;for(int i 1;i<n;i){dp[i] ma…

pytest教程-29-重复执行用例插件-pytest-repeat

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了设置用例超时时间插件pytest-timeout&#xff0c;本小节我们讲解一下pytest重复执行用例插件pytest-repeat。 前言 做功能测试的时候&#xff0c;经常会遇到某个模块不稳定&#xff0c;偶然会…