当前位置: 首页 > news >正文

STL——容器——容器适配器

容器适配器

STL 中的 3 个容器适配器
适配器默认底层容器提供的抽象接口头文件
stackdeque(后进先出 LIFO)<stack>
queuedeque队列(先进先出 FIFO)<queue>
priority——queuevector优先级队列(堆)<queue>

容器适配器(Container Adapter)不是一种新的数据结构,而是一种 “包装/转换”机制

它在已有的顺序容器(vector / deque / list)之上 加一层接口限制或行为调整,从而得到另一种抽象数据结构。

注意:

容器适配器 没有 迭代器:因为本来就是要对行为 作 限制 才提供了可以使用的接口,用以实现抽象类型(LIFO、FIFO 等 其他 数据限制规则)。

迭代器的意义:迭代器是连接算法与容器的通用“指针-概念” —— 它把“如何遍历”封装起来,让 同一段算法代码 能在 任意容器 上工作,而无需关心容器内部结构。

stack

手撕环节 & 思路

STL手撕通用思路

1. 抽象 → 2. 骨架 → 3. 填肉 → 4. 异常 → 5. 拷贝 → 6. 迭代器/比较

抽象

限制:后进先出 Last In First Out

实现:容器适配器、底层可以是 vector、list、deque 等序列容器

关键接口:push、pop、top、size、empty

骨架

先把模板头、成员变量、空壳函数写出来

template<class T, class Container = std::deque<T>>
class myStack {
public:void push(const T& val) {}void pop(){}T& top(){}size_t size(){}bool empty(){}private:Container _c;
};

填肉

逐个 正向实现 成员函数,之后再考虑 异常安全

template<class T, class Container = std::deque<T>>
class myStack {
public:void push(const T& val) {_c.push_back(val);}void pop(){_c.pop_back();}T& top(){return _c.back();}size_t size(){return _c.size();}bool empty(){return _c.empty();}private:Container _c;
};

容器适配器的成员函数实现,几乎完全依赖 底层容器

异常

容器适配器的异常 一般 无需额外处理,交给底层容器

拷贝构造 / 赋值重载 / 构造、析构函数 / 运算符重载......

默认生成的、标准库中的 够用,如需特殊优化再处理

迭代器

容器适配器 不提供 迭代器

普通容器实现迭代器的方式:

1、通常写成 内部类(模板嵌套类型),主流 STL 源码(SGI/LLVM/MSVC)普遍使用 容器内部嵌套的 class iterator/class const_iterator

2、有些实现也可能把迭代器写成独立模板,再 typedef 进容器

测试

#include"myStack.h"int main() {myStack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()) {std::cout << st.top() << ' ';st.pop();}
}

queue

手撕

抽象

容器适配器、FIFO、标准底层容器用 deque

骨架

填肉

#include<iostream>
#include<queue>template<class T, class Container = std::deque<T>>
class myQueue {
public:void push(const T& val){_c.push_back();}void pop(){_c.pop();}T& front(){return _c.front();}T& back(){return _c.back();}size_t size(){return _c.size();}bool empty(){return _c.empty;}private:Container _c;
};

......

后续步骤 同 容器适配器 stack

仿函数

仿函数(Functor)= “用起来像函数的对象”
在 C++ 里,它指的是 重载了 operator() 的类/结构体实例
对象后面跟一对括号就能像普通函数那样被调用,因此得名 “function-like object”。

class Add
{
public:int operator()(int x, int y){return x + y;}
};int main()
{Add add;int a = 6, b = 8;cout << "add(a, b) = " << add(a, b) << endl;cout << "add.operator()(a, b) = " << add.operator()(a, b) << endl;return 0;
}

重载了 operator() 的类,调用时 传参方式也是直接放在括号里,这样使其 对象 调用时与函数格式一样;当然,也可以显式调用。

再套上模板

template<class T>
class Add
{
public:T operator()(T x, T y){return x + y;}
};int main()
{Add<int> add;int a = 6, b = 8;cout << "add(a, b) = " << add(a, b) << endl;cout << "add.operator()(a, b) = " << add.operator()(a, b) << endl;Add<double> addd;double c = 6.6, d = 8.8;cout << "addd(c, d) = " << addd(c, d) << endl;cout << "addd.operator()(c, d) = " << addd.operator()(c, d) << endl;return 0;
}

仿函数 替代了 函数指针的功能,而且相较 函数指针 更有优势

1、可带状态——普通函数指针做不到“记住”数据,而仿函数可以拥有成员变量。

2、可以作为模板参数——仿函数可以作为模板参数,在 priority_queue 中会用到。STL 算法中 有 大量使用仿函数的策略( std::sort, std::accumulate, std::priority_queue 的自定义比较器)。

3、内联友好——函数对象通常是轻量级类,编译器易于内联,效率高于函数指针。

注:C++11 起,lambda 表达式会被编译器自动生成一个匿名仿函数类

所以:仿函数 = 重载了 operator() 的类,用起来像函数,却能携带状态和类型信息,是 STL 算法与 lambda 的底层基石。

priority_queue

优先级队列(抽象数据类型),底层默认是个堆(具体数据结构),大堆 —— 因为 类模板参数中有仿函数 less 的原因

标准库中的 优先级队列,默认使用 vector 适配生成

手撕

抽象

底层是堆结构(完全二叉树,父子节点保持大小关系一致),使用 vector 适配

堆结构 意味着:push 操作需要 尾插,并向上调整( O(n*logn) );pop 操作 删除堆顶数据 需要 首尾交换,删掉尾部数据并 向下调整( O(n) )。

骨架

别忘了 在 成员变量中 用 Compare 声明 比较器对象类型 ,我们将用这个比较器 灵活实现 大堆小堆,默认小堆(当然 具体比较逻辑 在 adjust_up、adjust_down 向上调整和向下调整的 函数中实现)

#include<iostream>
#include<vector>template<class T>
class myLess
{
public:bool operator()(const T& x, const T& y)	// 实现 () 重载,形成仿函数{}
};template<class T>
class myGreater
{
public:bool operator()(const T& x, const T& y)	// 实现 () 重载,形成仿函数{}
};template<class T, class Container = std::vector<T>, class Compare = myLess<T>>
class mmyPriority
{
public:void adjust_up(size_t child)	// push 需要向上调整,传入 孩子 下标(即 尾插后的 最后一个元素 下标){}void push(const T& val){}void adjust_down(size_t parent)	// pop 需要向下调整,传入 父亲 下标(即 首尾交换过后,堆顶元素下标 0){}void pop()	// 删除堆顶元素{}T& top(){}size_t size(){}bool empty(){}
private:Container _c;Compare _com;	// 比较器对象,仿函数
};

填肉

template<class T>
class myLess
{
public:bool operator()(const T& x, const T& y)	// 实现 () 重载,形成仿函数{return x < y;}
};template<class T>
class myGreater
{
public:bool operator()(const T& x, const T& y)	// 实现 () 重载,形成仿函数{return x > y;}
};template<class T, class Container = std::vector<T>, class Compare = myLess<T>>
class mmyPriority
{
public:void adjust_up(size_t child)	// push 需要向上调整,传入 孩子 下标(即 尾插后的 最后一个元素 下标){}void push(const T& val){_c.push_back(val);	// 尾插adjust_up(_c.size() - 1);	// 向上调整 最后一个元素}void adjust_down(size_t parent)	// pop 需要向下调整,传入 父亲 下标(即 首尾交换过后,堆顶元素下标 0){}void pop()	// 删除堆顶元素{std::swap(_c[0], _c[_c.size() - 1]);	// 交换 堆顶 堆底 元素,用了 std 提供的全局 swap_c.pop_back();	// 删除 堆底 元素adjust_down(0);	// 堆顶元素 向下调整,维持 堆序}const T& top() const{return _c[0];}size_t size() const{return _c.size();}bool empty() const{return _c.empty();}
private:Container _c;Compare _com;	// 比较器对象,仿函数
};

重点关注 重温 堆 的 向上调整 和 向下调整 实现:

adjust_up 向上调整

对一个堆进行尾插,并传入最后一个元素下标 child,判断是否需要调整:

void adjust_up(size_t child)	// push 需要向上调整,传入 孩子 下标(即 尾插后的 最后一个元素 下标)
{size_t parent = (child - 1) / 2;	// 计算 父节点 下标while (child > 0)	// 循环结束条件:孩子节点一直向上调整到了 根 的位置{					// 或者 当前 父子节点 关系 已满足 堆序if (_com(_c[parent], _c[child])	// 父节点 < 孩子 ?????{// 向上调整:交换 父子std::swap(_c[parent], _c[child]);child = parent;parent = (child - 1) / 2;}else{// 已满足堆序break;}}
}

adjust_down 向下调整

删除堆顶元素:交换首尾元素后 删掉尾部元素,并将 堆顶元素 向下调整

向下调整:判断父节点 是否已经   大于   左右孩子中的较大者

void adjust_down(size_t parent)	// pop 需要向下调整,传入 父亲 下标(即 首尾交换过后,堆顶元素下标 0)
{size_t child = parent * 2 + 1;	// 左孩子while (child < _c.size())	// 循环结束条件:child 仍在容器范围内{							// 或者  当前 父子节点已满足 堆序if (child + 1 < _c.size() && _com(_c[child], _c[child + 1]))	// 如果 右孩子 存在,且 右孩子 > 左孩子{child++;	// 让右孩子 成为 最大的孩子,接下来与 父节点 比较,是否需要调整}if (_com(_c[parent], _c[child]))	// 父节点 < 孩子 ? 调整 : break{std::swap(_c[parent], _c[child]);	// 交换 父子parent = child;child = parent * 2 + 1;}else{break;}}
}

其实还有一种 粗暴的写法更为简洁并且 可读性更高:直接 两两判断父节点和两个孩子的关系(如果孩子存在):

void adjust_down(size_t parent)
{size_t n = _c.size();while (true){size_t largest = parent;    // 假设 父节点 最大size_t left  = 2 * parent + 1;size_t right = 2 * parent + 2;if (left < n && _com(_c[largest], _c[left]))  largest = left;if (right < n && _com(_c[largest], _c[right])) largest = right;if (largest == parent) break;std::swap(_c[parent], _c[largest]);parent = largest;}
}

异常

有越界访问 或者 堆空时删除的操作,底层容器 vector 中已经处理。

拷贝构造 / 赋值重载 / 构造、析构函数 / 运算符重载......

默认生成的、标准库中的 够用,如需特殊优化再处理

迭代器

容器适配器 不提供 迭代器

优先级队列 限制了 只暴露 操作 首元素(堆顶元素)的接口;如果想要遍历元素 等操作内部任意元素,那么 堆 这种 结构,才会提供这种接口

测试

#include"mmyPriority_queue.h"
using namespace std;void test_priority_queue()
{mmyPriority_queue<int> pq;pq.push(1);pq.push(3);pq.push(5);pq.push(2);pq.push(68);pq.push(99);pq.push(55);pq.push(0);while (!pq.empty()){cout << pq.top() << " ";pq.pop();}cout << endl;
}int main()
{test_priority_queue();return 0;
}

补充:序列容器 deque

双端队列 两端都可以在 O(1) 时间内完成插入/删除,且支持随机访问

并非 受到队列一样 FIFO 的限制;

内部结构:

分块链表 + 索引表(中央控制器 + 若干固定大小的 buffer)。

逻辑上连续,物理上分块,因此迭代器比 vector 复杂,但依然满足 RandomAccessIterator

http://www.lryc.cn/news/620923.html

相关文章:

  • Mac chrome浏览器下载DevEco Studio 6.0.0 Beta2失败
  • MacOS 系统计算机专业好用工具安装
  • Spring Boot 深度解析:从原理到实践
  • 亚马逊手工制品分类体系革新:从流量重构到运营升级的深度解析
  • [已解决]当启动 Spring Boot 应用时出现 Using generated security password xxx提示
  • Rust Web框架Axum学习指南之入门初体验
  • vue部署正式环境上传nginx后遇到的问题
  • MySQL中的DML(二)
  • mysql查询中的filesort是指什么
  • 第三方软件检测:软件性能测试报告(一)
  • CMake笔记:Alias Target在哪些地方可以使用
  • 使用Docker安装MeiliSearch搜索引擎
  • 【完整源码+数据集+部署教程】柠檬质量检测系统源码和数据集:改进yolo11-DBBNCSPELAN
  • nginx入门需知(含安装教程)
  • 知识的本质
  • 【MATLAB技巧】已知平面上的一些点,拟合得到一个圆的例程,给出最小二乘与非线性迭代两种解法,附下载链接
  • Swift 数据类型全景解析(基础到高阶)
  • Gradle(四)Maven 项目迁移 Gradle 项目实践
  • [激光原理与应用-274]:理论 - 波动光学 - 光是电磁波,无线电磁波可以通过天线接收和发送,为什么可见光不可以?
  • 滑动窗口题目:定长子串中元音的最大数目
  • 【读代码】开源流式语音编码器SecoustiCodec
  • MySQL的索引(索引的创建和设计原则):
  • python自学笔记8 二维和三维可视化
  • 业务敏捷性对SAP驱动型企业意味着什么?如何保持企业敏捷性?
  • 网络通信全过程:sk_buff的关键作用
  • ⭐CVPR2025 3D 高斯探测视觉基础模型3D能力
  • Mybatis学习笔记(五)
  • 3D-R1、Scene-R1、SpaceR论文解读
  • 区块链 + 域名Web3时代域名投资的新风口(上)
  • CTFSHOW | nodejs题解 web334 - web344