STL 容器
STL
是C++的核心组成部分,其主要包括了容器、迭代器、算法三大组件。
其中容器负责存储数据,迭代器是容器和算法的桥梁,负责对容器中的元素进行操作。本文重点介绍容器部分内容。
STL
主要容器
STL
容器根据特性进行分类,可以分为序列式容器、关联容器、容器适配器
序列容器
序列容器重点在于根据元素的插入顺序而进行线性排列。
常见的序列容器有5种,分别是vector
,list
,deque
,array
,forward_list
,这5种序列容器的底层实现原理和核心特性总结如下:
容器 | 底层原理 | 核心特性 |
---|---|---|
vector | 拥有连续存储空间的动态数组,底层由3个指针,分别是:start ,finish ,end_of_stroage ,通过这三个底层指针可以实现一些常规操作;具备动态扩容机制(一般为2倍) | 支持随机访问([] /at() ),尾插和删除的复杂度为O(1) ,中间插入/头插/删除元素 O(n) |
list | 底层数据结构是双向链表(内存空间不连续),每个节点都有数据,前驱指针和后继指针 | 不支持随机访问,在任意位置插入/删除为 O(1) ,遍历元素效率O(n) |
deque | 分段连续内存,有一个指针数组,存放每一段连续存储空间的地址 | 支持双端高效插入/删除 O(1) ,随机访问效率略低于vector ,整体来说集成了vector 和list 的优点 |
array | 固定大小的静态数组,内存分配在栈上 | 大小不可变,随机访问高效,适合存储已知固定数量的元素 |
forward_list | 单向链表(仅包含后继指针) | 内存占用比list 更小,仅支持单向遍历,插入/删除 O(1) |
关联式容器
关联容器重点在于键值对的关联,元素根据按照键Key排序,元素的插入顺序不会影响到元素所处的位置,可以分为有序关联容器和无序关联容器。
有序关联容器
有序关联容器以键为核心,元素会自动按照键的大小进行排序(默认情况下是升序排列std::less<key>
,降序std::greater<int>
)。这一类容器的底层数据结构是采用的红黑树(Red-Black Tree),红黑树是一种自平衡的二叉搜索树,红黑树的插入、删除、查找的时间复杂度都是**O(logn)
**.
有序关联容器主要分为4种:set
、map
、multiset
、multimap
。它们的核心区别在于键是否唯一以及是否存储键值对.
1、set
存储唯一键的有序容器,其元素本身就是键(键=值),并且这里也不允许键重复,新插入的元素会按照键的大小自动排序,同时set
种的元素是常量,不能直接修改(可能会破坏红黑树的性质),修改的正确操作过程应该是先删除旧元素,再插入新元素。由于红黑树的性质,在set
插入或删除元素时,除被删除元素的迭代器外,其他迭代器均不会失效。
常见的接口操作:
insert(key)
返回的是pair<iterator, bool>
,标识是否插入成功erase(key)/erase(iterator)
删除匹配键的元素和迭代器指向的元素find(key)
查找某个key
count(key)
计算某个key
出现的次数lower_bound(key)
找到第一个不小于key
(≥key)的迭代器,upper_bound(key)
返回第一个大于key
的元素迭代器
2、multiset
可以在set
的基础上允许插入重复的键,元素按照键的大小有序排列。
常见的接口操作:
insert(key)
返回新插入元素的迭代器erase(key)
删除所有匹配键的元素count(key)
计算某个key
出现的次数equal_range(key)
返回是一个pair
包含所有匹配键的元素范围[first, second)
,左闭右开区间
3、map
map
是存储键值对(key-value) 的有序容器,键(key)唯一,值(value)可修改。元素按键的大小自动排序,通过键可以快速访问对应的值。
底层基于红黑树,树的每个节点存储一个pair<const key, value>
,所以键不可修改,值可以通过迭代器和[]
修改
在插入时,不允许插入重复的键,insert
会忽略重复键的插入;键一般用来查找和排序,值用来存储实际的元素,并且按照默认升序排列。
常见接口:
insert(pair<key, value>)
(返回pair<iterator, bool>
)、map[key] = value
(若键存在则修改值,否则插入新键值对)。erase(key)
(删除键对应的键值对)、erase(iterator)
。find(key)
(返回指向键值对的迭代器)。at(key)
(安全访问,键不存在时抛异常)、map[key]
(非安全访问,键不存在时插入默认值)。lower_bound
、upper_bound
和set
一样
4、multimap
在map
的基础上允许插入重复的键,键值对按照键的大小有序排列。多个键值对可以有相同的键,按键有序排列(相同键的键值对相邻),由于键不唯一,所以不能使用[]
操作符号,只能使用find
或者equal_range
常见接口:
insert(pair<key, value>)
总是成功,返回新元素迭代器equal_range(key)
返回包含所有相同键的键值对范围erase(key)
、find(key)
返回第一个匹配键的迭代器,与map
类似
所以有序管理容器的总结如下:
容器 | 底层原理 | 核心特点 |
---|---|---|
set | 红黑树,键值唯一,元素就是值 | 元素自动按键升序排列,插入 / 删除 / 查找复杂度为 O (log n) |
multiset | 红黑树,键可以重复插入 | 特性同set ,但支持键重复,插入 / 删除 / 查找仍为 O (log n) |
map | 红黑树,存储键值对,键唯一 | 按键升序排列,通过键快速访问值([] /at() ),键不可重复 |
mutlimap | 红黑树,键值对,键可以重复,插入到相邻位置 | 特性同map ,但键可重复,不支持[] 操作符(需用find() /equal_range() ) |
无序关联容器
无序关联容器以键(key) 为核心的容器,其元素不按键的大小排序,而是通过哈希表(Hash Table)实现存储和访问。插入/删除/查找的平均时间复杂度为O(1)
。
底层的核心原理是哈希表(hashtable
),重点包含以下三个部分:
- 桶(Buckets):一个动态数组,每个元素(桶)是一个链表或红黑树的头指针,用于存储哈希值相同的元素(处理哈希冲突)
- 哈希函数(Hash Function):将键(key)映射为桶的索引(整数),决定元素应该放入哪个桶
- 相等比较器(Equality Comparator):判断两个键是否 “相等”(用于处理哈希冲突时,区分不同键但哈希值相同的元素)
在插入新元素时,通过哈希函数计算键的哈希值,得到桶的索引,将元素放入对应桶中;查找元素时,先通过哈希函数定位桶索引,然后在桶内遍历比较键,找到目标元素;当元素过多,超过负载因子时,触发重hash
,分配更大的桶数组,重新计算哈希值,并且迁移到新的桶中。
1、unordered_set
存储唯一键的无序容器,元素本身就是键(“键 = 值”),键不允许重复,元素无序排列。适用于需要快速去重但是不需要关注元素顺序的场景
在插入重复键时,insert
会被忽略,并且元素的顺序由哈希函数和插入顺序决定,于键的大小无关;由于键是常量,所有更新键的信息,需要删除旧元素插入新元素;插入时可能导致rehash
导致所有迭代器失效,删除时只有被删除的元素的迭代器失效。
常见接口:
insert(key)
返回pair<iterator, bool>
,bool
表示是否插入成功erase(key)
删除所有匹配键的元素,返回删除数量、erase(iterator)
find(key)
返回指向键的迭代器,不存在则返回end()
count(key)
返回键的出现次数,只能是 0 或 1bucket_count()
当前桶数量、load_factor()
当前负载因子、rehash(n)
强制将桶数量设置为≥n
2、unordered_map
存储键值对(key-value) 的无序容器,键(key)唯一,值(value)可修改,元素无序排列,适用于需要通过唯一键快速查询值且不关心键顺序的场景,例如:缓存(键为 ID,值为缓存数据)、字典(无需按字母排序的键值映射)
插入重复键时,insert
操作忽略,[]
操作符会覆盖旧值;键用于哈希和查找,值存储实际数据,值可通过迭代器或[]
修改;键值对顺序由哈希函数决定,与插入顺序和键大小无关。
常见接口:
insert(pair<key, value>)
、map[key] = value
键不存在则插入,存在则修改值at(key)
安全访问,键不存在抛异常、map[key]
不安全访问,键不存在插入默认值find(key)
、erase(key)
等与unordered_set
类似
3、unordered_multiset
unordered_set
的 “多重” 版本,允许存储重复的键(键可以不唯一),元素无序排列(相同键的元素相邻),适用于需要存储重复元素且不关心顺序的场景,例如:统计元素出现频率(如词频统计)、存储多个相同优先级的任务
4、unordered_multimap
unordered_map
的 “多重” 版本,允许存储重复的键(键值对的键可以不唯一),元素无序排列(相同键的键值对相邻),多个键值对可拥有相同的键,相同键的键值对在同一桶中相邻,适用于需要通过重复键映射多个值且不关心顺序的场景。
无序关联容器总结:
容器 | 底层原理 | 核心特性 |
---|---|---|
unordered_set | 哈希表(数组 + 链表 / 红黑树),键唯一 | 元素无序,插入 / 删除 / 查找平均复杂度 O (1) (最坏 O (n) ,取决于哈希冲突),键唯一。 |
unordered_map | 哈希表,存储键值对,键唯一 | 无序,通过键快速访问值,平均 O (1) 操作,键唯一 |
unordered_multiset | 哈希表,键可重复 | 无序,支持键重复,平均 O (1) 操作 |
unordered_multimap | 哈希表,键值对,键可重复 | 无序,键可重复,平均O (1) 操作 |
容器适配器
容器适配器是一类特殊的容器,它们不直接实现数据存储,而是通过封装底层容器(如vector
,deque
,list
等)来提供特定的接口和功能。核心作用是:限制对底层容器的访问方式,只暴露符合特定数据结构逻辑的操作
主要特点:
- 本身不存储数据,数据实际上存储在底层容器中,适配器仅仅提供上层的接口
- 屏蔽底层容器的大部分操作,只保留符合自身发展逻辑的接口(
stack
仅允许栈顶操作) - 适配器没有迭代器,不支持遍历或者随机访问
- 默认使用特定的容器作为底层实现(如
stack
使用deque
),但是也可以通过模板参数指定其他符合要求的容器
主要的容器适配器类型
1、stack
stack
遵循后进先出规则,仅仅允许在栈顶进行操作,插入、删除和访问等。
其底层的容器为deque
,主要原因是:
deque
的尾部插入和删除的效率都是O(1)
deque
可以减少频繁扩容导致的内存重分配- 跟
list
相比,deque
的内存连续性更好,缓存利用率更高
核心操作有:
push(val)
:在栈顶插入元素(调用底层容器的push_back
)。pop()
:删除栈顶元素(调用底层容器的pop_back
,不返回元素)。top()
:返回栈顶元素的引用(调用底层容器的back
)。empty()
:判断栈是否为空。size()
:返回元素个数。
实现
template <typename T, typename Sequence = std::deque<T>>
class stack {
public:using value_type = typename Sequence::value_type;using size_type = typename Sequence::size_type;using reference = typename Sequence::reference;using const_reference = typename Sequence::const_reference;using container_type = Sequence;// 各类接口bool empty() const {return container_.empty();}size_type size() const {return container_.size();}const_reference top() const {return container_.back();}void push(const value_type &v) {container_.push_back(v);}void pop() {container_.pop_back();}template<typename... Args>void emplace(Args&& ...args) {container_.emplace_back(std::forward<Args>(args)...);}void swap(stack& other) {container_.swap(other.container_);}private:// 唯一的成员变量就是底层的容器containter_type container_;
}
2、queue
queuq
遵循FIFO规则,允许在队尾插入,队头删除和访问。
底层的默认容器还是deque
,主要原因是:
deque
支持队尾插入(push_back
)和队头删除(pop_front
),两者效率均为O (1)
- 若用
vector
作为底层,pop_front
操作需移动所有元素(O (n)
,效率低) list
虽支持O (1)
的头尾操作,但内存连续性差,缓存命中率低
核心操作有:
push(val)
:在队尾插入元素(调用push_back
)。pop()
:删除队头元素(调用pop_front
,不返回元素)。front()
:返回队头元素的引用(调用front
)。back()
:返回队尾元素的引用(调用back
)。empty()
/size()
:判断空或返回元素个数。
实现
template <typename T, typename Sequence = std::deque<T>>
class queue {
public:using value_type = typename Sequence::value_type;using size_type = typename Sequence::size_type;using reference = typename Sequence::reference;using const_reference = typename Sequence::const_reference;using container_type = Sequence;bool empty() const {return container_.empty();}size_type size() const {return container_.size();}reference front() const {return container_.front();}const_reference front() const {return container_.front();}reference_back() {return container_.back();}const_reference back() const {return container_.back();}void push(value_type& v) {container_.push_back(v);}void pop() {container_.pop_front();}template<typename ...Args>void emplace_back(Args&& ...args) {container_.emplace_back(std::forward<Args>(args)...);}void swap(queue& other) {container_.swap(other.container_);}private:container_type container_;
}
3、priority_que
priority_que
是优先级队列,也是一种适配器,每次删除的其中的优先级最高的元素(默认最大元素),插入时会自动调整元素的位置以维持优先级顺序。
priority_que
底层使用的容器是vector
,主要原因是:
priority_que
逻辑是一个二叉堆,需要随机访问定位父子节点的位置(如父节点的下标为i
,那么其左孩子的下标为2*i + 1
,右孩子的下标为2*i + 2
)vector
随机访问的效率最好,内存连续,具有局部性原理,并且堆的上浮和下沉操作效率更高
它默认使用std::less<T>
作为比较器(大顶堆),std::greater<T>
为小顶堆
如priority_queue<int, vector<int>, std::greater<int>> min_heap
核心操作有
push(val)
:插入元素并调整堆结构(确保优先级最高的元素在顶部,复杂度 O (log n))。pop()
:删除顶部(优先级最高)的元素并调整堆(复杂度 O (log n),不返回元素)。top()
:返回顶部元素的引用(调用底层容器的front
)。empty()
/size()
:判断空或返回元素个数。
实现
template<typename T, typename Container = std::vector<int>,typename Comp = std::less<typename Container::value_type>>class priority_queue {public:using value_type = T;using size_type = typename Container::size_type;/// 构造函数explicit priority_queue(const Container& container = Container(), const Comp& comp = Comp()) : comp_(comp), container_(container){/// 利用容器的头尾迭代器构造一个堆std::make_heap(container_.begin(), container_.end(), comp_);}// 拷贝构造和移动构造,赋值,移动赋值都是默认priority_queue(priority_queue &&) noexcept = default;priority_queue &operator=(priority_queue &&) noexcept= default;priority_queue(const priority_queue &) = default;priority_queue &operator=(const priority_queue &) = default;/// 输入迭代器版本的构造函数template<typename InputIterator>priority_queue(InputIterator first, InputIterator last, const Comp& comp = Comp(), const Container& container = Container()) : container_(container), comp_(comp) {std::make_heap(container_.begin(), container_.end(), comp_);}// 下面是常见的接口bool empty() const {return container_.empty();}size_type size() const {return container_.size();}const value_type& top() const {return container_.front();}value_type& top() {return container_.front();}void push(const valye_type& value) {container_.push_back(value);std::push_heap(container_begin(), container_.end(), comp_);}void push(valye_type&& value) {container_.push_back(std::move(value));std::push_heap(container_begin(), container_.end(), comp_);}void pop() {std::pop_heap(container_.begin(), container_end(), comp_);container_.pop_back();}void swap(priority_que& other) {std::swap(comp_, other.comp_);std:;swap(container_, other.container_);}template<typename ...Args>void emplace_back(Args&& ...args) {container_.emplace_back(std::forward<Args>(args)...);std::push_heap(container_.begin(), container_.end(), comp_);}private:// 成员变量主要有比较器和容器Container container_;Comp comp_;}
最后,如有错误,请各位大佬海涵,我一定努力改正,如有侵权,请联系我删除~