【STL】stack、queue、priority_queue模拟实现

简介: 一. deque简单介绍1.1 deque的功能介绍deque(双端队列

一. deque简单介绍


1.1 deque的功能介绍


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

image.png


1.2 deque的本体框架


虽说是连续性存储空间,但这种连续性只是表面上的,实际上它在堆上分配了一块一块的动态储存区,叫做缓冲区(buff),每一块缓冲区本身是连续的,deque自身的机制把这一块一块的缓冲区虚拟地连在一起,即把它们的首元素地址存在一个指针数组map里,注意这里的map不是STL里的map容器,而是deque的中控器,是个指针数组。

template<class T>                                                                                                             
class deque     
{    
  protected:    
  iterator start; // 指向指针数组第一个元素的迭代器    
  iterator finish;// 指向指针数组最后一个元素的迭代器    
  T** map;        // 中控的指针数组    
  size_t map_size;// 指针数组大小    
};

为了满足头尾的插入删除,一开始的buff会放到中间位置,这样如果有需要的话,往前往后都可以存buff。当map指向的这块空间不够存放内存指针的时候,就会另觅一块更大的连续性空间,然后把指针一个一个复制过去,并销毁旧的空间。利用这种数据结构,deque就能方便地模拟自身的存储区是连续性空间的假象,并且可以实现双向插入删除的功能。

image.png


1.3 deque的迭代器框架


迭代器就是模拟指针的操作,比如解引用得到具体的数据,箭头访问内部成员变量,自增、自减等等操作。那么deque的迭代器应该具备怎么结构?我们可以考虑以下几点。首先,需要知道该元素位于哪个buff的哪个位置;其次,一旦迭代器前进和后退有可能会跳跃至上一个或下一个缓冲区,为了判断跳跃的条件就需要知道,当前元素所在buff的首尾指针。最后,如果前进或后退必须跳跃至下一个或上一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心(map),通过map可以知道跳跃的缓冲区。 所以在迭代器中需要定义如下参数:

template<class T, class Ref, class Ptr, size_t N>    
struct _deque_iterator     
{    
  T* cur;  //指向buff里面当前数据的位置    
  T* first;//buff第一个位置指针    
  T* last; //buff最后一个位置指针    
  T** node;//map中控中当前buff的地址    
} 

迭代器和中控器之间的关系如下图所示

7.png


1.2 deque的优缺点


优点

与vector相比:头部和中间的插入删除不需要大量挪动数据,时间复杂的为O(1);扩容时也不需要大量挪动数据。

与list相比:空间利用率较高,内存碎片少,不是需要一个才开辟一个。

缺点

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

总结

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


二. 容器适配器


2.1 什么是适配器


适配器是一种设计模式,该种模式是将一个类进行封装,利用原来类的接口转换成客户希望的另外一个接口。


2.2 STL标准库中的stack和queue底层结构


虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,其中STL中stack和queue默认容器使用deque。

image.png


2.3 stack和queue的模拟实现


2.3.1 stack容器剖析


两者的实现原理一样,下面我们主要分析stack的实现

stack要做到存储不同类型数据,那就应该是个类模板,模板参数是要存储的数据的类型和底层容器的种类。

image.png


注意:容器也是可以作为模板参数的,因为模板作用就是处理不同类型但逻辑相同的问题,我们对传入的不同类型统一管理,像vector,list等容器算自定义类型,我们也可以作为模板参数传入。

stack的框架

成员变量只有一个,就是底层的容器的一个实例化对象

// 这里我们的底层容器默认缺省是deque
template<class T, class Container = deque<T>>
class stack
{
public:
  // 构造函数
  stack()
       // 调用容器的无参的默认构造函数
    :_con()
  {}
private:
  // 底层结构,即容器对象
  Container _con;
};

入栈

只能栈顶入数据,就是deque的尾插,对应调用底层容器deque的push_back()

PS:直接调用就行,不用考虑空间不够增容的问题,这些归底层容器deque管

// 入栈
void push(const T& data)
{
  _con.push_back(data);
}

出栈

从栈顶出,就是deque的尾删,封装deque的back_pop()

// 出栈
void pop()
{
  _con.pop_back();
}

获取栈顶元素

就是获取deque的最后一个元素,这里要注意不同stack对象调用对应的返回值不同,所以有下面两种写法:const的栈对象返回const的值,非const的栈对象返回非const的值。

// 普通对象获取栈顶元素
T& top()
{
  return _con.back();
}
// const对象获取栈顶元素
const T& top()const
{
  return _con.back();
}

stack的完整模拟实现

template<class T,class Container=deque<T>>
class stack
{
public:
  // 构造函数
  stack()
    :_con()
  {}
  // 入栈
  void push(const T& data)
  {
    _con.push_back(data);
  }
  // 出栈
  void pop()
  {
    _con.pop_back();
  }
  // 判空
  bool empty()
  {
    return _con.empty();
  }
  // 长度
  size_t size()
  {
    return _con.size();
  }
  // 普通对象获取栈顶元素
  T& top()
  {
    return _con.back();
  }
  // const对象获取栈顶元素
  const T& top()const
  {
    return _con.back();
  }
private:
  // 底层容器
  Container _con;
};


2.3.2 queue的完整模拟实现


与stack大同小异,由于两者的特性不同(stack是后进先出,queue是先进先出),对应容器调用的接口也就不一样。

template<class T,class Container=deque<T>>
class queue
{
public:
  // 构造函数
  queue()
    :_con()
  {}
  // 队列尾部插入元素
  void push(const T& data)
  {
    _con.push_back(data);
  }
  // 队列头部出元素
  void pop()
  {
    _con.pop_front();
  }
  // 返回队列当前元素个数
  size_t size()
  {
    return _con.size();
  }
  // 判断队列是否为空
  bool empty()
  {
    return _con.empty();
  }
  // 普通对象直接返回对头元素的引用(可修改)
  T& front()
  {
    return _con.front();
  }
  // const对象返回对偷的元素不能修改
  const T& front()const
  {
    return _con.front();
  }
  // 普通对象直接返回对尾元素的引用(可修改)
  T& back()
  {
    return _con.back();
  }
  // const对象返回对尾的元素不能修改
  const T& back()const
  {
    return _con.back();
  }
private:
  Container _con;
};


三. priority_queue


3.1 priority_queue介绍


优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆。所有需要用到堆的场景,都可以考虑使用priority_queue(注意默认情况下priority_queue是大堆)

它的接口和栈的接口类似

push():数据进堆

pop():删除堆顶数据

top():拿到堆顶数据

empty():堆是否为空?为空的话返回true,不空返回false

size():当前堆元素的数量


3.2 priority_queue的模拟实现


PS:容器vector的数据位置要用到堆结构处理,堆的知识和相关堆算法,这些堆的内容可以参考下面文章:二叉树和堆

仿函数

定义一个类专门重载operator(),使该类实例化出来的对象可以像调用函数一样的调用,从而实现特定功能,你如比较两个数据的大小。

image.png

priority的基本框架

模板参数,除了数据类型和容器,还加了仿函数类Compare,为了使用户可以自主选择大堆和小堆才引入的,当然默认情况是大堆,根STL里一致,大堆传less的仿函数,小堆传greater

image.png


仿函数我们在priority_queue外单独定义

// 仿函数less
template<class T>
struct less
{
  bool operator()(const T& x1, const T& x2)
  {
    return x1 < x2;
  }
};
// 仿函数greater
template<class T>
struct greater
{
  bool operator()(const T& x1, const T& x2)
  {
    return x1 > x2;
  }
};
// priority_queue
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
  // 无参的默认构造函数
  priority_queue()
    :_con()
  {}
private:
  Container _con;
};

堆的向上和向下调整算法

我们尾部插入元素要利用向上调整算法调堆,删除堆顶元素要用向下调整算法调堆。只是在插入和删除操作时调用,一般外部不用直接调用(因为没意义),所以我们把这两个算法设置成私有。

但不能把他们设置为内联,因为他们内部有循环操作,且代码较长。

private:
// 向下调整算法
void AdjustDown(int parent)
{
  int n = _con.size();
  int child = parent * 2 + 1;
  while(child < n)
  {
    if (child + 1 < n&&Compare()(_con[child], _con[child + 1]))
    {
      ++child;
    }
    if (Compare()(_con[parent], _con[child]))
    {
      swap(_con[parent], _con[child]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      return;
    }
  }
}
// 向上调整算法
void AdjustUp(int child)
{
  int n = _con.size();
  while (child > 0)
  {
    int parent = (child - 1) / 2;
    if (Compare()(_con[parent], _con[child]))
    {
      swap(_con[parent], _con[child]);
      child = parent;
    }
    else
    {
      return;
    }
  }
}
Container _con;

用迭代器区间构造一个priority_queue对象

像下图一样构造一个优先级队列的对象

image.png

template<class Iterator>
priority_queue(Iterator first, Iterator last)
    // 先把数据存入容器对象
  : _con(first, last)
{
  // 向下调整法建堆
  int n = _con.size();
  for (int i = (n - 1 - 1) / 2; i >= 0; --i)
  {
  AdjustDown(i);
  }
}

入队列

就是vector的尾插,再把插入的元素向上调整,保持堆的结构

// 入队列
void push(const T& data)
{
  _con.push_back(data);
  AdjustUp(_con.size()-1);
}

出队列

删除堆顶元素。交换一下堆顶和堆底元素,不用重新建堆,vector尾删,最后在给堆顶元素进行向下调整算法。

// 出队列
void pop()
{
  swap(_con[0], _con[_con.size() - 1]);
  _con.pop_back();
  AdjustDown(0);
}

获取堆顶元素

堆顶元素不允许修改,因为:堆顶元素修改可以会破坏堆的特性。

// 获取栈顶(堆顶)元素
const T& top()const
{
  return _con.front();
}

image.png

完整代码

#include<iostream>
#include<vector>
using namespace std;
namespace MyPriorityQueue
{
  // 仿函数less
  template<class T>
  struct less
  {
    bool operator()(const T& x1, const T& x2)
    {
      return x1 < x2;
    }
  };
  // 仿函数greater
  template<class T>
  struct greater
  {
    bool operator()(const T& x1, const T& x2)
    {
      return x1 > x2;
    }
  };
  // priority_queue
  template<class T, class Container = vector<T>, class Compare = less<T>>
  class priority_queue
  {
  public:
    // 无参的默认构造函数
    priority_queue()
      :_con()
    {}
    // 传迭代对应容器类型对象的迭代器来构造
    template<class Iterator>
    priority_queue(Iterator first, Iterator last)
      : _con(first, last)
    {
      // 向下调整法建堆
      int n = _con.size();
      for (int i = (n - 1 - 1) / 2; i >= 0; --i)
      {
        AdjustDown(i);
      }
    }
    // 入队列
    void push(const T& data)
    {
      _con.push_back(data);
      AdjustUp(_con.size()-1);
    }
    // 出队列
    void pop()
    {
      swap(_con[0], _con[_con.size() - 1]);
      _con.pop_back();
      AdjustDown(0);
    }
    // 长度
    int size()const
    {
      return _con.size();
    }
    // 判空
    bool empty()const
    {
      return _con.empty();
    }
    // 获取栈顶(堆顶)元素
    const T& top()const
    {
      return _con.front();
    }
  private:
    // 向下调整算法
    void AdjustDown(int parent)
    {
      int n = _con.size();
      int child = parent * 2 + 1;
      while(child < n)
      {
        if (child + 1 < n&&Compare()(_con[child], _con[child + 1]))
        {
          ++child;
        }
        if (Compare()(_con[parent], _con[child]))
        {
          swap(_con[parent], _con[child]);
          parent = child;
          child = parent * 2 + 1;
        }
        else
        {
          return;
        }
      }
    }
    // 向上调整算法
    void AdjustUp(int child)
    {
      int n = _con.size();
      while (child > 0)
      {
        int parent = (child - 1) / 2;
        if (Compare()(_con[parent], _con[child]))
        {
          swap(_con[parent], _con[child]);
          child = parent;
        }
        else
        {
          return;
        }
      }
    }
  private:
    Container _con;
  };
  void test()
  {
    vector<int> v = { 2,1,3,3,2,0,6 };
    priority_queue<int> p(v.begin(), v.end());
  }
}
相关文章
|
运维 监控 数据可视化
日志服务 HarmonyOS NEXT 日志采集最佳实践
鸿蒙操作系统(HarmonyOS)上的日志服务(SLS)SDK 提供了针对 IoT、移动端到服务端的全场景日志采集、处理和分析能力,旨在满足万物互联时代下应用的多元化设备接入、高效协同和安全可靠运行的需求。
117952 101
|
缓存
npm install 一直卡着不动如何解决
npm install 一直卡着不动如何解决
7652 0
|
2月前
|
关系型数据库 MySQL 数据库
阿里云数据库RDS费用价格:MySQL、SQL Server、PostgreSQL和MariaDB引擎收费标准
阿里云RDS数据库支持MySQL、SQL Server、PostgreSQL、MariaDB,多种引擎优惠上线!MySQL倚天版88元/年,SQL Server 2核4G仅299元/年,PostgreSQL 227元/年起。高可用、可弹性伸缩,安全稳定。详情见官网活动页。
|
10月前
|
安全 数据安全/隐私保护 Docker
docker私有仓库harbor安装
通过以上步骤,您可以成功在企业内部安装和配置Harbor私有仓库,方便地管理和分发Docker镜像。Harbor不仅提供了基础的镜像管理功能,还增强了安全性、身份管理和审计功能,使其成为企业级容器镜像管理的理想选择。
462 22
|
缓存 NoSQL Redis
Redis高可用技术方案对比
Redis高可用技术方案对比
263 0
|
存储 安全 数据库
Flask-Login 扩展中,如何安全地存储用户密码?
【10月更文挑战第4天】Flask-Login 扩展中,如何安全地存储用户密码?
|
自动驾驶 物联网 5G
波束跟踪技术在5G中的重要性及其应用
波束跟踪技术在5G中的重要性及其应用
336 0
|
存储 SQL Oracle
细说MySQL锁机制:S锁、X锁、意向锁...
好久没有深入地写文章了,这次来发一篇,通过mysql事物 | Joseph's Blog (gitee.io)和其他一些博客有感进行一些补充,InnoDB详解在下期发布 加锁机制 乐观锁和悲观锁 之前在JVM中其实也讲到,JVM在对象初始化的过程中其实也是使用的乐观锁
897 0
细说MySQL锁机制:S锁、X锁、意向锁...
|
消息中间件 Kafka 数据处理
实时计算 Flink版产品使用合集之消费Kafka数据时,实现限流如何解决
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
存储 Linux C++
【C++】Vector -- 详解(上)
【C++】Vector -- 详解(上)