STL详解(九)—— stack和queue的模拟实现

文章目录

  • 一、容器适配器
    • [1.1 什么是适配器](#1.1 什么是适配器)
    • [1.2 stack和queue的底层结构](#1.2 stack和queue的底层结构)
    • [1.3 deque详解](#1.3 deque详解)
      • [1.3.1 deque的原理介绍](#1.3.1 deque的原理介绍)
      • [1.3.2 deque的底层结构](#1.3.2 deque的底层结构)
      • [1.3.3 deque的优缺点](#1.3.3 deque的优缺点)
      • [1.3.4 选择deque的原因](#1.3.4 选择deque的原因)
  • 二、stack的模拟实现
  • 三、queue的模拟实现

一、容器适配器

1.1 什么是适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多人知晓的、经过分类编目的、代码设计经验的总结),该设计模式是将一个类的接口转换成客户希望的另一个接口

从应用角度出发,STL 中的适配器可以分为三类:

  • 容器适配器 container adapters
  • 迭代器适配器 iterator adapters
  • 仿函数适配器 functor adapters

其中,容器适配器 可修改底层为指定容器,如由 vector 构成的栈、由 list 构成的队列;迭代器适配器可以 实现其他容器的反向迭代器(后续介绍);最后的仿函数适配器就厉害了,几乎可以 无限制的创造出各种可能的表达式

本文介绍的是容器适配器,即 栈 和 队列,最后还会介绍一下常作为这两种容器适配器的默认底层容器 双端队列

1.2 stack和queue的底层结构

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


注意: 容器支持 迭代器,但是容器适配器不支持迭代器,因为栈和队列这种数据结构不能随便去遍历,不然会导致发生变化,不易维护

1.3 deque详解

1.3.1 deque的原理介绍

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

1.3.2 deque的底层结构

deque(双端队列)的底层结构通常由多个固定大小的缓冲区组成,每个缓冲区是一个连续的存储块。这些缓冲区通过一个指向前一个缓冲区和一个指向后一个缓冲区的指针进行连接,形成了一个双向链表

deque的内部缓冲区以分块的形式存储元素。每个缓冲区有一个固定的大小,它通常是2的幂次方,例如512、1024等。缓冲区中的元素被存储在数组中,以保持元素的连续性

deque的双向链表由一个或多个缓冲区组成,每个缓冲区都包含一个指向前一个缓冲区和一个指向后一个缓冲区的指针。第一个缓冲区的指向前一个缓冲区的指针为空指针,最后一个缓冲区的指向后一个缓冲区的指针也为空指针

当需要在deque的头部或尾部插入或删除元素时,只涉及到相关缓冲区的操作,而不会涉及其他缓冲区。这种设计使得deque的插入和删除操作时间复杂度为常数级别(O(1))

1.3.3 deque的优缺点

deque(双端队列)在大多数情况下是非常高效且灵活的数据结构,但它也有一些缺点需要注意

【vector】的优缺点:

  • 优点:适合尾插尾删,随机访问
  • 缺点:不适合头部或者中部插入删除,效率低,需要挪动数据;扩容有一定性能消耗,还可能存在一定程度的空间浪费

【list】的优缺点:

  • 优点:在任意位置插入删除效率高;按需申请释放空间
  • 缺点:不支持随机访问;CPU高速缓存命中率低

【deque】就结合了 【vector】和 【list】的优缺点而为之发明

【deque】 与 【vector】相比较的优势:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要移动大量元素,因此其效率是比 【vector】高

【deque】 与 【list】相比较 的优势:其底层是连续空间,空间利用率较高,不需要存储额外字段

【deque】的致命缺陷:不适合遍历!!! 因为在遍历时,【deque】的迭代器要频繁的去检查其是否移动到某段空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑【vector】和 【list】。

1.3.4 选择deque的原因

【stack】是一种,后进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可以作为 【stack】的底层容器,比如 【vector】和 【list】都可以

【queue】是一种,先进先出的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可以作为 【queue】的底层容器,比如 【list】

注意:string 、vector 不支持 头删,因此无法适配 queue

但是在 STL 中对 【stack】和 【queue】默认选择 【queue】作为其底层容器,原因为:

  • 【stack】 和 【queue】不需要遍历(因此stack 和 queue 没有迭代器),只需要在固定的一端或者两端进行操作
  • 在【satck】中元素增长时,【deque】比 【vector】的效率更高(扩容时不需要移动大量元素)
  • 在【queue】中元素增长时,【deque】不仅效率高,而且内存使用率高

总结来说:【deque】结合了所有 【stack】 和 【queue】所需要的优点,而完美的避开了其缺陷

二、stack的模拟实现

知道了容器适配器后,stack的模拟实现就显得相当简单,我们只需要调用所指定容器的各个成员函数即可实现stack的各个函数接口

成员函数 函数作用 实现方法
push 元素入栈 调用所指定容器的push_back
pop 元素出栈 调用所指定容器的pop_back
top 获取栈顶元素 调用所指定容器的back
size 获取栈中有效元素个数 调用所指定容器的size
empty 判断栈是否为空 调用所指定容器的empty
swap 交换两个栈中的数据 调用所指定容器的swap
cpp 复制代码
#pragma once

#include<iostream>
#include<deque>
using namespace std;

namespace xt
{

	template<class T, class Container = deque<T>>
	class Stack
	{
	public:
		// 入栈
		void push(const T& x)
		{
			_con.push_back(x);
		}

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

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

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

		//获取栈中有效元素个数
		size_t size() const
		{
			return _con.size();
		}

		//判断栈是否为空
		bool empty() const
		{
			return _con.empty();
		}

		//交换两个栈中的数据
		void swap(Stack<T, Container>& st)
		{
			_con.swap(st._con);
		}
	private:
		Container _con;
	};
}

适配器的厉害之处就在于 只要底层容器有我需要的函数接口,那么我就可以为其适配出一个容器适配器,比如 vector 构成的栈、list 构成的栈、deque 构成的栈,甚至是 string 也能适配出一个栈,只要符合条件,都可以作为栈的底层容器,当然不同结构的效率不同,因此库中选用的是效率较高的 deque 作为默认底层容器

三、queue的模拟实现

同样的方式,我们也是通过调用所指定容器的各个成员函数来实现queue的

成员函数 函数作用 实现方法
push 队尾入队列 调用所指定容器的push_back
pop 队头出队列 调用所指定容器的pop_front
front 获取队头元素 调用所指定容器的front
back 获取队尾元素 调用所指定容器的back
size 获取栈中有效元素个数 调用所指定容器的size
empty 判断栈是否为空 调用所指定容器的empty
swap 交换两个栈中的数据 调用所指定容器的swap
cpp 复制代码
#include<iostream>
#include<deque>
using namespace std;
namespace xt
{
	template<class T, class Container = deque<T>>
	class Queue
	{
	public:
		// 入队
		void push(const T& x)
		{
			_con.push_back(x);
		}

		// 出队
		void pop()
		{
			_con.pop_front();
		}

		// 获取队头元素
		T& front()
		{
			return _con.front();
		}

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

		// 获取队尾元素
		T& back()
		{
			return _con.back();
		}

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

		//获取队列中有效元素个数
		size_t size() const
		{
			return _con.size();
		}

		//判断队列是否为空
		bool empty() const
		{
			return _con.empty();
		}

		//交换两个栈中的数据
		void swap(Queue<T, Container>& q)
		{
			_con.swap(q._con);
		}
	private:
		Container _con;
	};

}


相关推荐
xqqxqxxq2 小时前
洛谷算法1-1 模拟与高精度(NOIP经典真题解析)java(持续更新)
java·开发语言·算法
沐知全栈开发2 小时前
Rust 函数
开发语言
dgaf2 小时前
【疯狂的往左】用 C 语言播放《下山》
c语言·c++
卷卷的小趴菜学编程2 小时前
项目篇----仿tcmalloc的内存池设计(central cache篇)
c++·tcmalloc·内存池·central cache
txinyu的博客2 小时前
解析muduo源码之 Channel.h & Channel.cc
c++
zhougl9962 小时前
Java 枚举类(enum)详解
java·开发语言·python
yong99902 小时前
基于势能原理的圆柱齿轮啮合刚度计算MATLAB程序实现
开发语言·matlab
仰泳的熊猫2 小时前
题目1434:蓝桥杯历届试题-回文数字
数据结构·c++·算法·蓝桥杯
lsx2024062 小时前
R 数组:深入探索与高效使用
开发语言