【C++】【STL】别再混淆!C++容器适配器不是“容器”,这篇讲清它的本质

【C++】【STL】别再混淆!C++容器适配器不是“容器”,这篇讲清它的本质


                                                 

                                                         🔥拾Ծ光个人主页

👏👏👏欢迎来到我的专栏:《C++》《C++类和对象》《数据结构》《C语言》

📌文档栈(stack)队列(queue)双端队列(deque)priority_queue

与本文相关博客:《容器进阶:deque的“双端优势” vs list的“链式灵活” vs vector的“连续高效”》

在前面的一篇文章我们讲解了deque容器,其非常适合作为适配器stack和queue的底层数据结构,下面我就来带大家看看这两个适配器是如何与deque结合的!

一、什么是容器适配器?

在 C++ 标准库中,容器适配器(Container Adapters) 是一种基于现有容器实现的特殊容器,它们提供了更特定的接口和功能,隐藏了底层容器的部分特性,仅暴露适配后的操作方式。

⭐️容器适配器不直接存储数据,而是通过封装底层容器(如 vectordequelist 等)来实现功能,底层容器可以通过模板参数指定(通常有默认值)。

C++中最常用的容器适配器有三种std::stack(栈);std::queue(队列);std::priority_queue(优先队列)。
容器适配器的特点接口简化:仅提供适配场景所需的操作(如 stack 没有迭代器,无法遍历内部元素)。底层可配置:通过模板参数指定底层容器(需满足适配器的操作要求,如 stack 需要 push_backpop_back 等接口)。无迭代器:适配器不提供迭代器,无法像普通容器那样遍历元素,只能通过其特定接口操作。

栈和队列的接口比较简单,所以我们不再作介绍,具体接口可以通过上面的文档查看。下面我们直接模拟实现,来加深我们对这种特殊结构的理解!!!

由于适配器的底层结构为其他容器(vector,list,deque...),像插入删除执行操作,其实是由底层的容器实现,所以,我们只需要运用底层容器的函数接口。同样我们创建一个自己的命名空间,在自己的命名空间中写代码

二、3大容器适配器

1、栈——stack

 文档栈(stack)

// 将deque作为默认容器 template<class T, class Container = deque<T>> class stack { public: // ... private: Container _con; // 成员变量 };

成员变量其实就是我们通过底层容器实例化出的一个对象,我们只需要用这个对象来找到相应的函数接口。

2.1、在栈顶插入——push

void push(const T& x) { _con.push_back(x); }

2.2、获取有效数据个数——size

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

2.3、获取栈顶数据——top

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

2.4、删除栈顶数据——pop

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

2.5、判空——empty

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

2、队列——queue

 文档队列(queue)

// 将deque作为默认容器 template<class T, class Container = deque<T>> class queue { public: // ... private: Container _con; // 成员变量 };

3.1、在队尾插入——push

void push(const T& x) { _con.push_back(x); }

3.2、删除队头数据——pop

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

3.3、获取有效数据个数——size

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

3.4、获取队头数据——front

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

3.5、获取队尾数据——back

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

3.6、判空——empty

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

3、优先级队列——priority_queue

文档:priority_queue

1、priority_queue介绍

priority_queue底层选择用vector作为默认容器它基于底层容器实现,能保证每次从顶部取出走的元素都是 “优先级最高” 的(默认是最大值)。其核心特性是通过堆(heap)算法维护优先级(通常是大根堆,即最大值在顶部),插入元素后会自动调整结构以满足优先级规则。

即priority_queue的底层支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用 算法函数建堆(make_heap)、堆尾插入(push_heap)和堆顶删除(pop_heap)来自动完成此操作。

相关接口:

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

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

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

push():在队尾插入元素

pop():删除队头元素

2、特性测试

插入5个没有排好序的数进行排序:

void test01() { priority_queue<int> pq; pq.push(5); pq.push(2); pq.push(4); pq.push(3); pq.push(1); while (!pq.empty()) { cout << pq.top() << endl; pq.pop(); } }

priority_queue底层默认建大堆,即输出结果为降序

void test02() { priority_queue<int, vector<int>,Great<int>> pq; // Great:建小堆 pq.push(5); pq.push(2); pq.push(4); pq.push(3); pq.push(1); while (!pq.empty()) { cout << pq.top() << endl; pq.pop(); } }
这里需要注意:当我们建大堆时——降序;建小堆——升序。

3、模拟实现

/*第二个参数:默认适配容器为vector数组容器 第三个参数:采用仿函数,默认建大堆*/ template<class T,class Container = vector<T>,class Compare = Less<T>> class priority_queue { public: // ... private: Container _con; // 成员变量 };

⭐️我们要用一个类实现建大堆和建小堆的功能,这里就需要仿函数(也就是类模板的第三个参数)来实现

/*为了能够满足priority_queue实现升序(建小堆)和降序(建大堆)的功能,我们实现两个仿函数的类作为priority_queue类模板的第三个参数 用于随时满足priority_queue实现升序和降序的效果*/ template<class T> class Less { public: bool operator()(const T& x, const T& y) { return x < y; } }; template<class T> class Great { public: bool operator()(const T& x, const T& y) { return x > y; } };
3.1、获取有效数据个数
size_t size()const { return _con.size(); }
3.2、获取队头数据
const T& top() { return _con.front(); }
3.3、队尾插入

在队尾插入数据后,堆的结构就可能被破坏,所以我们要向上调整建堆来维持数据的优先级。

// 向上调整建堆 void AdjustUp(int child) { Compare com; // 实例化一个仿函数类的对象 int parent = (child - 1) / 2; while (child >= 0) { if (com(_con[parent], _con[child])) { swap(_con[parent], _con[child]); child = parent; parent = (child - 1) / 2; } else break; } }
void push(const T& x) { _con.push_back(x); AdjustUp(_con.size() - 1); }
3.4、队头删除

删除队头数据后,堆的结构被破坏,我们需要将最后一个数据放到原来第一个数据的位置,保证堆的剩余结构不被破坏,然后通过向下调整建堆的方式,维持数据的优先级。

// 向下调整建堆 void AdjustDown(int parent) { Compare com; // 实例化一个仿函数类的对象 int child = 2 * parent + 1; //先假设左孩子较大 while (child < _con.size()) { if ((child + 1)<_con.size() && com(_con[child], _con[child + 1])) { child++; } if (com(_con[parent], _con[child])) { swap(_con[parent], _con[child]); parent = child; child = 2 * parent + 1; } else break; } }
void pop() { swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); AdjustDown(0); }
3.5、判空
bool empty()const { return _con.empty(); }

三、仿函数

在上面测试priority_queue以及实现priority_queue的push和pop时就使用了仿函数,因为,当我们想要让一组数升序排序或者降序排序时,我们不可能说去实现两个priority_queue的类,一个默认建大堆,降序排序;一个默认建小堆,升序排序。所以,C++引入了仿函数的概念,在实现类模板时,可以将仿函数作为模板参数,这样在我们使用时调用对应的仿函数,就能达到我们想要的效果。

1、什么是仿函数?

仿函数(Functor):也称为函数对象(Function Object),是一种在 C++ 中通过类或结构体实现的可调用实体,其实就是一个类。

仿函数的核心特点是 重载了 operator() 运算符,使得类的对象可以像普通函数一样被调用。
仿函数是 C++ 中 “以类模拟函数” 的重要机制,这种机制结合了面向对象的封装性和函数的灵活性,在 STL和算法中有着广泛应用。

仿函数的核心特点:

1. 重载 operator()

使用:

Add add; //创建仿函数对象 int result = add(2, 3); //像函数一样调用,结果为5 

通过在类中定义 operator() 运算符,使得该类的对象可以像函数一样被调用:

class Add { public: int operator()(int a, int b) // 重载()运算符 { return a + b; } }; 

2、仿函数的使用⭐️

让冒泡排序既可以升序又可以降序排序:

#include <iostream> using namespace std; /*-----------------------第一部分:实现比较仿函数(类模板)-----------------------*/ template<class T> class Less { public: bool operator()(const T& x, const T& y) const //注意:加const提高通用性 { return x < y; } }; template<class T> class Greater { public: bool operator()(const T& x, const T& y) const { return x > y; } }; /*-----------------------第二部分:实现冒泡排序(函数模板)-----------------------*/ template<class Compare> void BubbleSort(int* a, int n, Compare com) { for (int i = 1; i <= n - 1; i++) { int flag = 1; for (int j = 1; j <= n - i; j++) { if (com(a[j], a[j - 1])) //用仿函数来比较a[j]和a[j-1]的大小 { swap(a[j - 1], a[j]); flag = 0; } if (flag) break; //如果某一趟排序没有进行交换,说明当前数组中的元素已经有序了,直接退出 } } int main() { // 创建仿函数对象 Less<int> less; //实例化仿函数对象 Greater<int> greater; // 冒泡排序结合仿函数进行排序 int a[] = { 9,1,2,8,5,7,4,6,3 }; int n = sizeof(a) / sizeof(a[0]); //1.使用仿函数进行冒泡排序(升序/降序) BubbleSort(a, n, less); //仿函数Less<int> less”进行升序排序 for (int i = 0; i < n; i++) { cout << a[i] << " "; } cout << endl; BubbleSort(a, n, greaterFunc); // 仿函数Greater<int>进行降序排序 for (int i = 0; i < n; i++) { cout << a[i] << " "; } cout << endl; // 也可直接传递匿名对象(临时对象) BubbleSort(a, n, Less<int>()); // 升序 for (int i = 0; i < n; i++) { cout << a[i] << " "; } cout << endl; BubbleSort(a, n, Greater<int>()); // 降序 for (int i = 0; i < n; i++) { cout << a[i] << " "; } cout << endl; return 0; } 

四、模拟实现完整代码

#include<stack.h>
#include<iostream> #include<vector> #include<list> #include<deque> using namespace std; namespace hds { template<class T, class Container = deque<T>> class stack { public: stack() { } // 获取有效数据个数 const T& size()const { return _con.size(); } // 获取栈顶数据 const T& top()const { return _con.back(); } // 向栈顶插入数据 void push(const T& x) { _con.push_back(x); } // 删除栈顶数据 void pop() { _con.pop_back(); } // 判空 bool empty() { return _con.empty(); } private: Container _con; }; }
#include<queue.h>
#include<iostream> #include<vector> #include<list> #include<deque> using namespace std; // deque是vector和list的混合 // deque的头插和尾插,以及头删和尾删效率很高,更甚于vector和list // deque的下标访问效率还行,略逊于vector // deque在中间位置插入数据,效率极低 // 而栈和队列一般不会中间插入数据,所以deque作为栈和队列的默认容器非常合适 namespace hds { template<class T, class Container = deque<T>> class queue { public: queue() { } // 获取有效数据个数 const size_t size()const { return _con.size(); } // 获取队头数据 const T& front()const { return _con.front(); } // 获取队尾数据 const T& back()const { return _con.back(); } // 再队尾插入数据(先进先出) void push(const T& x) { _con.push_back(x); } // 删除队头数据(先进先出) void pop() { _con.pop_front(); } // 判空 bool empty() { return _con.empty(); } private: Container _con; }; }
#include<priority_queue>
#include<iostream> #include<queue> #include<list> #include<vector> using namespace std; // 仿函数:本质是一个类,这个类重载operator(), 他的对象可以像函数一样使用 /*为了能够满足priority_queue实现升序(建小堆)和降序(建大堆)的功能,我们实现两个仿函数的类作为priority_queue类模板的第三个参数 用于随时满足priority_queue实现升序和降序的效果*/ template<class T> class Less { public: bool operator()(const T& x, const T& y) { return x < y; } }; template<class T> class Great { public: bool operator()(const T& x, const T& y) { return x > y; } }; namespace hds { /*第二个参数:默认适配容器为vector数组容器 第三个参数:采用仿函数,默认建大堆*/ template<class T,class Container = vector<T>,class Compare = Less<T>> class priority_queue { public: size_t size()const { return _con.size(); } const T& top() { return _con.front(); } void AdjustUp(int child) { Compare com; // 实例化一个仿函数类的对象 int parent = (child - 1) / 2; while (child >= 0) { if (com(_con[parent], _con[child])) { swap(_con[parent], _con[child]); child = parent; parent = (child - 1) / 2; } else break; } } void push(const T& x) { _con.push_back(x); AdjustUp(_con.size() - 1); } void AdjustDown(int parent) { Compare com; // 实例化一个仿函数类的对象 int child = 2 * parent + 1; //先假设左孩子较大 while (child < _con.size()) { if ((child + 1)<_con.size() && com(_con[child], _con[child + 1])) { child++; } if (com(_con[parent], _con[child])) { swap(_con[parent], _con[child]); parent = child; child = 2 * parent + 1; } else break; } } void pop() { swap(_con[0], _con[_con.size() - 1]); _con.pop_back(); AdjustDown(0); } bool empty()const { return _con.empty(); } private: Container _con; }; }

今天的分享到此结束!!!

Read more

【算法】滑动窗口(一)-长度最小的子数组

【算法】滑动窗口(一)-长度最小的子数组

目录 一、题目介绍 二、算法原理 1.排必然非结果情况 1.1.2区域 (1)预证区 (2)已证区 2.滑动窗口 三、提交代码 一、题目介绍 209. 长度最小的子数组 - 力扣(LeetCode) 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 示例 1: 输入:target = 7, nums = [2,3,

By Ne0inhk
【算法】堆排序

【算法】堆排序

算法系列四:堆排序 一、原理 1.大根堆得最大值 2.新堆得接下来最大值 二、实现 1.end 2.升序降序 三、性质 1.时间复杂度 2.空间复杂度 3.稳定性 一、原理 1.大根堆得最大值 将待排序数组所有数据创建成大根堆排列,排出数组首元素为数组中的最大值,将数组最大值排放到数组末尾总体完成它一个数据的排列 2.新堆得接下来最大值 交换排序好后整体堆的结构被破坏,因为数组中的最大值找到并放到了数组末尾完成了它数据的排列,接下来重新维护出堆得最大值排序就是要得此最大值之外剩余数据中的最大值即第二大的值接着放到第一大值前面这样来进行排序的,所以接下来继续维护出堆得最大值就去包含已排好序最大值之外剩下的接下来的最大值,所以回到堆结构被破坏,此时要再维护出堆得接下来的最大值维护的堆就不再包含那些得到交换下来的过去最大值了,不包含它们以后,去维护的此时新堆就为交换到堆顶数据一个破坏点其它全堆的结构,用一次向下调整就能维护出继续出接下来最大值再往后放成最大值们的升序排放,最后,所有数据最大值、剩余数据最大值、再剩余数据最大值、所有再剩余数据最大值都堆排比

By Ne0inhk
说说常见的限流算法及如何使用Redisson实现多机限流

说说常见的限流算法及如何使用Redisson实现多机限流

本文介绍了四种常见的限流算法及其实现方式。固定窗口限流简单但存在临界突刺问题;滑动窗口通过时间片滑动解决突刺问题,但滑动单位选择困难;漏桶算法以固定速率处理请求,能削峰缓冲但不够灵活;令牌桶算法允许突发流量,并发性能更好但时间单位选择仍需考量。实现层面,单机限流可使用Guava的RateLimiter,分布式限流推荐Redisson或网关层工具如Sentinel。提供了Redisson的配置示例和两种限流设置方式,建议采用基于Duration的新API实现更简洁的限流控制。 限流算法介绍 1、固定窗口限流 在单位时间内允许部分操作。 就比如,单位时间(窗口)为1小时,一小时内只能有100个用户能访问,如果在一小时内就已经达到了次数上限100,那么这时不论来多少请求都会被拒绝掉,也就是说1小时内设定了多少次就是只能有多少次,只有到1小时才会重置次数。但是它有一个突刺问题,例如在前59分钟都没有一个用户进行访问,在59分钟的时候100个用户同时访问,然后1小时01分时候又有100个用户都访问了,那就是2分钟有200个用户同时访问,可能瞬间通过200个请求,在两个窗口的临界突刺,

By Ne0inhk
干预式对比学习(ICL)算法

干预式对比学习(ICL)算法

干预式对比学习(ICL)的发展紧密依托对比学习的技术演进与因果推理的融合应用。 第一阶段为萌芽探索期(2020年前),此时传统对比学习(如SimCLR、MoCo等)已在计算机视觉、自然语言处理领域崭露头角,但“虚假关联”导致的泛化能力不足问题逐渐凸显。研究人员开始尝试引入因果思想,通过简单的特征干预手段优化对比学习,但尚未形成系统的算法框架,干预方式较为单一,仅能针对特定场景(如图像背景干扰)进行简单调整,未实现因果逻辑与对比学习的深度融合,这一阶段的探索为后续ICL算法的成型奠定了实践基础。 第二阶段为算法成型期(2020-2023年),随着因果表征学习的兴起,ICL算法逐步形成完整体系。研究人员正式提出“干预式对比学习”的概念,将结构因果模型(SCM)与对比学习深度耦合,明确了“无干预-有干预”的样本对比核心思路,设计出专用的因果对比损失函数,解决了传统对比学习中混杂因子干扰的核心痛点。这一阶段的关键突破的是实现了干预机制的通用化,可适配多领域数据,无需人工预设因果假设,同时验证了ICL算法与现有对比学习方法的兼容性,相关研究成果逐步应用于简单的图像分类、文本表征任务,初步体现出其

By Ne0inhk