ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++ 高频 STL 容器实战指南|序列容器、关联容器、适配器完整剖析

C++ 高频 STL 容器实战指南|序列容器、关联容器、适配器完整剖析 目录一、STL容器整体分类二、序列容器详解1、vector 动态数组最常用容器1.1 核心用法1.2 底层实现原理1.3 性能特性1.4 适用场景1.5 代码示例输出2、list 双向链表2.1 核心用法2.2 底层实现原理2.3 性能特性2.4 适用场景2.5 代码示例输出3、deque 双端队列3.1 核心用法3.2 底层实现原理3.3 性能特性3.4 适用场景3.5 内存分配策略3.6 双端队列的操作实现3.6.1 插入操作3.6.2 删除操作3.6.3 访问操作3.7 优缺点优点缺点3.8 双端队列与其他容器的比较3.9 代码示例输出三、迭代器体系详解1. 迭代器简介2. 迭代器六大分类3. 主要迭代器类别及其特性4. iterator_category 的作用5. iterator_category 的声明6. 迭代器特性Iterator Traits详解7. 自定义迭代器与 iterator_traits四、关联容器详解1、map / unordered_map 键值对容器1.1 核心原理与区别map用法map 内部实现原理unordered_map用法unordered_map 内部实现原理1.2 性能对比1.3 应用场景1.4 代码示例输出2、set / unordered_set 集合容器2.1 核心原理与区别set 用法set 内部实现原理unordered_set 用法:unordered_set 内部实现原理2.2 性能对比2.3 应用场景2.4 代码示例五、stack、queue和priority_queue容器适配器1. 用法:2. 内部实现原理:3. 性能特性:4. 应用场景:5. 代码示例六、总结结语STL 容器是 C 开发的基石。很多人分不清各类容器的底层与适用场景直接照搬使用造成程序低效、超时、迭代器报错等问题。本文覆盖序列容器、关联容器、容器适配器从原理、性能、场景到实战代码全方位解析附带完整示例兼顾新手学习与老手查漏。一、STL容器整体分类C STL容器分为三大体系各司其职覆盖所有数据存储场景序列容器vector、list、deque有序线性存储侧重元素排列与增删关联容器map/unordered_map、set/unordered_set键值匹配/唯一元素存储侧重查找与去重容器适配器stack、queue、priority_queue基于原生容器封装限定数据读写规则二、序列容器详解1、vector 动态数组最常用容器1.1 核心用法vector是STL中最常用的序列容器之一提供了动态大小的数组功能。它支持随机访问允许在末尾高效地添加和删除元素。1.2 底层实现原理vector在内部使用动态数组通常是连续的内存块来存储元素。当需要扩展容量时它会分配一块更大的内存将现有元素复制到新内存中然后释放旧内存。这种策略在平均情况下保证了push_back的常数时间复杂度。1.3 性能特性随机访问支持常数时间的随机访问O(1)。末尾插入/删除push_back和pop_back操作在摊销分析下是常数时间O(1)。中间插入/删除在中间位置插入或删除元素需要移动后续元素时间复杂度为线性时间O(n)。1.4 适用场景需要频繁随机访问元素。主要在容器末尾进行插入和删除操作。当容器大小不需要频繁调整避免频繁的内存重新分配。1.5 代码示例#include iostream #include vector int main() { // 创建一个空的整数vector std::vectorint numbers; // 向vector末尾添加元素 numbers.push_back(10); numbers.push_back(20); numbers.push_back(30); // 通过索引访问元素 std::cout 第一个元素: numbers[0] std::endl; // 遍历vector std::cout 所有元素: ; for(auto it numbers.begin(); it ! numbers.end(); it) { std::cout *it ; } std::cout std::endl; // 删除最后一个元素 numbers.pop_back(); // 打印删除后的vector std::cout 删除最后一个元素后: ; for(auto num : numbers) { std::cout num ; } std::cout std::endl; return 0; }输出2、list 双向链表2.1 核心用法list是一个实现了双向链表的数据结构适合在容器中间频繁插入和删除元素。与vector不同list不支持随机访问但在任何位置的插入和删除操作都是常数时间。2.2 底层实现原理list在内部使用双向链表每个元素包含指向前一个和后一个元素的指针。这使得在已知位置插入或删除元素时无需移动其他元素只需更新指针即可。2.3 性能特性随机访问不支持随机访问访问第n个元素需要线性时间O(n)。中间插入/删除已知位置的插入和删除操作是常数时间O(1)。遍历顺序遍历适合需要频繁遍历但不需要随机访问的场景。2.4 适用场景需要在容器中间频繁插入或删除元素。不需要进行随机访问。对内存的局部性要求不高链表元素在内存中不连续。2.5 代码示例#include iostream #include list int main() { // 创建一个空的整数list std::listint numbers; // 向list末尾添加元素 numbers.push_back(100); numbers.push_back(200); numbers.push_back(300); // 向list前端添加元素 numbers.push_front(50); // 遍历list std::cout 所有元素: ; for(auto it numbers.begin(); it ! numbers.end(); it) { std::cout *it ; } std::cout std::endl; // 插入元素 auto it numbers.begin(); it; // 指向第二个元素 numbers.insert(it, 150); // 打印插入后的list std::cout 插入元素后: ; for(auto num : numbers) { std::cout num ; } std::cout std::endl; // 删除元素 numbers.remove(200); // 打印删除后的list std::cout 删除元素后: ; for(auto num : numbers) { std::cout num ; } std::cout std::endl; return 0; }输出3、deque 双端队列3.1 核心用法deque双端队列是vector与list的结合体支持随机访问同时首尾增删效率极高完美弥补vector头插低效的缺陷。3.2 底层实现原理deque不使用单一连续内存采用分段内存块中央映射数组结构映射数组存储各数据块的地址每个数据块是连续内存。首尾扩容时仅需新增数据块无需全局内存重分配。3.3 性能特性随机访问支持常数时间的随机访问O(1)。前后插入/删除在前端和后端插入和删除元素的操作都是常数时间O(1)。中间插入/删除在中间位置插入或删除元素需要移动元素时间复杂度为线性时间O(n)。3.4 适用场景需要在容器两端频繁插入和删除元素。需要随机访问元素。不需要频繁在中间位置插入和删除元素。3.5 内存分配策略deque内部并不使用一个单一的连续内存块而是将元素分割成多个固定大小的块也称为缓冲区或页面并通过一个中央映射数组通常称为map来管理这些块。具体来说deque的内部结构可以分为以下几个部分1.中央映射数组Map一个指针数组指向各个数据块。map本身也是动态分配的可以根据需要增长或收缩。map允许deque在两端添加新的数据块而无需移动现有的数据块。2.数据块Blocks每个数据块是一个固定大小的连续内存区域用于存储元素。数据块的大小通常与编译器和平台相关但在大多数实现中数据块的大小在运行时是固定的如512字节或更多具体取决于元素类型的大小。3.起始和结束指针deque维护指向中央映射数组中第一个有效数据块的指针以及第一个无效数据块的指针。这些指针帮助deque快速地在两端添加或删除数据块。3.6 双端队列的操作实现3.6.1 插入操作在末尾插入 (push_back)1.检查当前末端数据块的剩余空间如果有空间直接在当前末端数据块中插入新元素。如果没有空间分配一个新的数据块并将其指针添加到map中然后在新块中插入元素。2.更新末尾指针如果分配了新块末尾指针指向该块的第一个元素。否则末尾指针移动到当前末端数据块的下一个位置。在前端插入 (push_front)1.检查当前前端数据块的剩余空间如果有空间直接在当前前端数据块中插入新元素。如果没有空间分配一个新的数据块并将其指针添加到map的前面然后在新块中插入元素。2.更新前端指针如果分配了新块前端指针指向该块的最后一个元素。否则前端指针移动到当前前端数据块的前一个位置。3.6.2 删除操作从末尾删除 (pop_back)1.检查末端数据块是否有元素如果有移除最后一个元素并更新末尾指针。如果数据块变为空释放该数据块并从map中移除其指针然后更新末尾指针指向前一个块。从前端删除 (pop_front)1.检查前端数据块是否有元素如果有移除第一个元素并更新前端指针。如果数据块变为空释放该数据块并从map中移除其指针然后更新前端指针指向下一个块。3.6.3 访问操作随机访问deque支持通过索引进行随机访问其内部机制如下1.计算元素的位置根据给定的索引确定对应的数据块和数据块内的偏移量。使用map数组定位到具体的块然后通过偏移量定位到块内的元素。2.访问元素一旦定位到具体的位置即可像数组一样访问元素。迭代器访问deque提供双向迭代器支持使用标准的C迭代器操作如、--等进行遍历。双端队列的性能特性理解deque的内部实现有助于理解其性能特性。以下是deque的主要操作及其时间复杂度操作时间复杂度说明随机访问通过索引常数时间O(1)通过计算块和偏移量直接访问元素插入/删除前端常数时间O(1)仅涉及前端指针和可能的数据块分配插入/删除末端常数时间O(1)仅涉及末端指针和可能的数据块分配中间插入/删除线性时间O(n)需要移动数据块内的元素可能涉及多个块的操作查找元素线性时间O(n)需要遍历元素进行查找插入单个元素平均常数时间O(1)在前端或末端插入通常不需移动大量元素插入大量元素线性时间O(n)需要分配新的数据块并进行元素复制3.7 优缺点优点双端操作高效在两端插入和删除元素非常快速不需要移动大量元素。支持随机访问可以像vector一样通过索引高效访问元素。动态增长无需预先定义大小可以根据需要自动调整。缺点内存碎片由于使用多个数据块可能导致内存碎片尤其是在大量插入和删除操作后。较低的局部性元素不连续存储可能导致缓存未命中率较高影响性能。复杂性较高内部实现相对复杂不如vector直接高效。3.8 双端队列与其他容器的比较特性vectordequelist内存结构单一连续内存块多块连续内存通过映射数组管理双向链表随机访问是常数时间O(1)是常数时间O(1)否需要线性时间O(n)前端插入/删除低效线性时间O(n)高效常数时间O(1)高效常数时间O(1)末端插入/删除高效常数时间O(1)高效常数时间O(1)高效常数时间O(1)内存碎片低由于单一连续内存块较高由于多块内存管理较高由于节点分散在内存中元素隔离高局部性较好中等分块存储提高了部分局部性低元素分散存储缓存效率低应用场景需要频繁随机访问、末端操作的场景需要频繁在两端插入/删除且偶尔随机访问的场景需要频繁在中间插入/删除且不需要随机访问的场景3.9 代码示例#include iostream #include deque int main() { // 创建一个空的deque std::dequestd::string dq; // 在末尾添加元素 dq.push_back(End1); dq.push_back(End2); // 在前端添加元素 dq.push_front(Front1); dq.push_front(Front2); // 遍历deque std::cout deque中的元素: ; for(auto it dq.begin(); it ! dq.end(); it) { std::cout *it ; } std::cout std::endl; // 访问首尾元素 std::cout 首元素: dq.front() std::endl; std::cout 尾元素: dq.back() std::endl; // 删除首元素 dq.pop_front(); // 删除尾元素 dq.pop_back(); // 打印删除后的deque std::cout 删除首尾元素后: ; for(auto num : dq) { std::cout num ; } std::cout std::endl; return 0; }输出三、迭代器体系详解1. 迭代器简介在 C 中迭代器是一种用于遍历容器如std::vector、std::list等元素的对象。它们提供了类似指针的接口使得算法可以独立于具体的容器而工作。迭代器的设计允许算法以统一的方式处理不同类型的容器。2. 迭代器六大分类为了使不同类型的迭代器能够支持不同的操作C 标准库将迭代器分为以下几种类别每种类别支持的操作能力逐级增强输入迭代器只读、单向遍历适用于单向链表输出迭代器只写、单向遍历适用于输出流前向迭代器可读写、单向遍历双向迭代器可读写、支持前后双向遍历list/map/set随机访问迭代器支持跳跃访问、算术运算vector/deque连续迭代器C20随机访问内存连续特性每个类别都继承自前一个类别具备更强的功能。例如双向迭代器不仅支持前向迭代器的所有操作还支持反向迭代即可以向后移动。3. 主要迭代器类别及其特性类别支持的操作示例容器输入迭代器只读访问、单向前进单向链表std::forward_list输出迭代器只写访问、单向前进输出流std::ostream_iterator前向迭代器读写访问、单向前进向量std::vector双向迭代器读写访问、单向前进和反向迭代双向链表std::list随机访问迭代器读写访问、单向前进、反向迭代、跳跃移动支持算术运算向量std::vector、队列std::deque无效迭代器新随机访问迭代器的所有功能且元素在内存中连续排列新的 C 容器如std::span4.iterator_category的作用iterator_category是迭代器类型中的一个别名用于标识该迭代器所属的类别。它是标准库中迭代器特性Iterator Traits的一部分标准算法会根据迭代器的类别优化其行为。为什么需要iterator_category标准库中的算法如std::sort、std::find等需要知道迭代器支持哪些操作以便选择最优的实现方式。例如对于随机访问迭代器可以使用快速的随机访问算法如快速排序。对于双向迭代器只能使用适用于双向迭代的算法如归并排序。对于输入迭代器只能进行单次遍历许多复杂算法无法使用。通过指定iterator_category你可以让标准算法了解你自定义迭代器的能力从而选择合适的方法进行操作。5. iterator_category 的声明在你的自定义迭代器类中通过以下方式声明迭代器类别using iterator_category std::bidirectional_iterator_tag;这表示该迭代器是一个双向迭代器支持向前和向后遍历。6. 迭代器特性Iterator Traits详解C 提供了迭代器特性Iterator Traits通过模板类std::iterator_traits来获取迭代器的相关信息。通过这些特性标准算法可以泛化地处理不同类型的迭代器。迭代器特性包含的信息std::iterator_traits提供以下信息iterator_category迭代器类别标签。value_type迭代器指向的元素类型。difference_type迭代器间的距离类型通常是std::ptrdiff_t。pointer指向元素的指针类型。reference对元素的引用类型。7. 自定义迭代器与iterator_traits当你定义自己的迭代器时确保提供这些类型别名以便标准库算法能够正确识别和使用你的迭代器。例如templatetypename T class Iterator { public: using iterator_category std::bidirectional_iterator_tag; using value_type T; using difference_type std::ptrdiff_t; using pointer T*; using reference T; // 其他成员函数... };这样使用std::iterator_traitsIteratorT时就能正确获取迭代器的特性。四、关联容器详解1、map / unordered_map 键值对容器1.1 核心原理与区别map用法map是一个关联容器用于存储键值对key-value。它基于键自动排序且每个键都是唯一的。map提供了快速的查找、插入和删除操作。map 内部实现原理map通常使用自平衡的二叉搜索树如红黑树实现。这确保了所有操作的时间复杂度为对数时间O(log n)且元素按照键的顺序排列。unordered_map用法unordered_map也是一种关联容器用于存储键值对但它不保证元素的顺序。unordered_map基于哈希表实现提供了平均常数时间O(1)的查找、插入和删除操作。unordered_map 内部实现原理unordered_map使用哈希表来存储元素。键通过哈希函数转换为哈希值并映射到特定的桶中。如果多个键映射到同一桶会通过链表或其他方法解决冲突。1.2 性能对比操作mapunordered_map查找O(log n)平均O(1)插入O(log n)平均O(1)删除O(log n)平均O(1)内存使用较高较低元素顺序有序无序1.3 应用场景map需要按键的顺序遍历元素。 需要有序的关联数组。 需要高效的范围查找。unordered_map对元素顺序没有要求。需要极高效的查找、插入和删除操作。不需要自定义的排序规则。1.4 代码示例map示例#include iostream #include map #include string int main() { // 创建一个空的map键为string值为int std::mapstd::string, int ageMap; // 插入键值对 ageMap[Alice] 30; ageMap[Bob] 25; ageMap[Charlie] 35; // 查找元素 std::string name Bob; if(ageMap.find(name) ! ageMap.end()) { std::cout name 的年龄是 ageMap[name] std::endl; } else { std::cout 未找到 name std::endl; } // 遍历map std::cout 所有人员和年龄: std::endl; for(auto it ageMap.begin(); it ! ageMap.end(); it) { std::cout it-first : it-second std::endl; } // 删除元素 ageMap.erase(Alice); // 打印删除后的map std::cout 删除Alice后: std::endl; for(auto it ageMap.begin(); it ! ageMap.end(); it) { std::cout it-first : it-second std::endl; } return 0; }输出map输出unordered_map示例#include iostream #include unordered_map #include string int main() { // 创建一个空的unordered_map键为string值为double std::unordered_mapstd::string, double priceMap; // 插入键值对 priceMap[Apple] 1.2; priceMap[Banana] 0.5; priceMap[Orange] 0.8; // 查找元素 std::string fruit Banana; if(priceMap.find(fruit) ! priceMap.end()) { std::cout fruit 的价格是 $ priceMap[fruit] std::endl; } else { std::cout 未找到 fruit std::endl; } // 遍历unordered_map std::cout 所有水果和价格: std::endl; for(auto it priceMap.begin(); it ! priceMap.end(); it) { std::cout it-first : $ it-second std::endl; } // 删除元素 priceMap.erase(Apple); // 打印删除后的unordered_map std::cout 删除Apple后: std::endl; for(auto it priceMap.begin(); it ! priceMap.end(); it) { std::cout it-first : $ it-second std::endl; } return 0; }unordered_map输出2、set / unordered_set 集合容器2.1 核心原理与区别set 用法set是一个关联容器用于存储唯一的、有序的元素。set基于键自动排序且每个元素都是唯一的。set 内部实现原理set通常使用自平衡的二叉搜索树如红黑树实现保证元素按顺序排列。每次插入元素时都会自动保持树的平衡并确保元素的唯一性。unordered_set用法:unordered_set也是一种集合容器用于存储唯一的元素但它不保证元素的顺序。unordered_set基于哈希表实现提供了平均常数时间O(1)的查找、插入和删除操作。unordered_set内部实现原理unordered_set使用哈希表存储元素。每个元素通过哈希函数转换为哈希值并映射到特定的桶中。冲突通过链表或其他方法解决。2.2 性能对比操作setunordered_set查找O(log n)平均O(1)插入O(log n)平均O(1)删除O(log n)平均O(1)内存使用较高较低元素顺序有序无序2.3 应用场景set需要有序的唯一元素集合。需要按顺序遍历元素。需要基于区间的操作如查找、删除某范围的元素。unordered_set对元素顺序无要求。需要极高效的查找、插入和删除操作。不需要自定义的排序规则。2.4 代码示例set示例#include iostream #include set int main() { // 创建一个空的整数set std::setint numbers; // 插入元素 numbers.insert(10); numbers.insert(20); numbers.insert(30); numbers.insert(20); // 重复元素不会被插入 // 遍历set std::cout set中的元素: ; for(auto it numbers.begin(); it ! numbers.end(); it) { std::cout *it ; } std::cout std::endl; // 查找元素 int key 20; if(numbers.find(key) ! numbers.end()) { std::cout key 在set中存在。 std::endl; } else { std::cout key 不在set中。 std::endl; } // 删除元素 numbers.erase(10); // 打印删除后的set std::cout 删除10后set中的元素: ; for(auto num : numbers) { std::cout num ; } std::cout std::endl; return 0; }set输出unordered_set示例#include iostream #include unordered_set int main() { // 创建一个空的unordered_set std::unordered_setint numbers; // 插入元素 numbers.insert(10); numbers.insert(20); numbers.insert(30); numbers.insert(20); // 重复元素不会被插入 // 遍历unordered_set std::cout unordered_set中的元素: ; for(auto it numbers.begin(); it ! numbers.end(); it) { std::cout *it ; } std::cout std::endl; // 查找元素 int key 20; if(numbers.find(key) ! numbers.end()) { std::cout key 在unordered_set中存在。 std::endl; } else { std::cout key 不在unordered_set中。 std::endl; } // 删除元素 numbers.erase(10); // 打印删除后的unordered_set std::cout 删除10后unordered_set中的元素: ; for(auto num : numbers) { std::cout num ; } std::cout std::endl; return 0; }unordered_set输出注意元素顺序可能不同五、stack、queue和priority_queue容器适配器容器适配器不存储数据是对原生容器的功能封装限制接口、固定读写规则底层默认依赖deque/vector。1. 用法:STL中的容器适配器stack、queue、priority_queue提供了特定的数据结构接口这些适配器在内部使用其他容器来存储元素默认使用deque或vector。2. 内部实现原理:stack先进后出LIFO数据结构通常使用deque或vector作为底层容器通过限制操作接口来实现。queue先进先出FIFO数据结构通常使用deque作为底层容器通过限制操作接口来实现。priority_queue基于堆的数据结构通常使用vector作为底层容器并通过堆算法如std::make_heap、std::push_heap、std::pop_heap维护元素的优先级顺序。3. 性能特性:stack访问顶部元素O(1)插入和删除O(1)queue访问前端和后端元素O(1)插入和删除O(1)priority_queue访问顶部最大或最小元素O(1)插入和删除O(log n)4. 应用场景:stack实现函数调用栈。处理撤销操作。深度优先搜索DFS。queue实现任务调度。广度优先搜索BFS。数据流处理。priority_queue实现优先级调度。求解最短路径算法如Dijkstra。任意需要按优先级处理元素的场景。5. 代码示例stack示例#include iostream #include stack int main() { // 创建一个空的stack底层使用vector std::stackint s; // 压入元素 s.push(1); s.push(2); s.push(3); // 访问栈顶元素 std::cout 栈顶元素: s.top() std::endl; // 弹出元素 s.pop(); std::cout 弹出一个元素后新的栈顶: s.top() std::endl; // 判断栈是否为空 if(!s.empty()) { std::cout 栈不为空元素数量: s.size() std::endl; } return 0; }stack输出queue示例#include iostream #include queue int main() { // 创建一个空的queue底层使用deque std::queuestd::string q; // 入队元素 q.push(First); q.push(Second); q.push(Third); // 访问队首元素 std::cout 队首元素: q.front() std::endl; // 访问队尾元素 std::cout 队尾元素: q.back() std::endl; // 出队元素 q.pop(); std::cout 出队后新的队首: q.front() std::endl; // 判断队列是否为空 if(!q.empty()) { std::cout 队列不为空元素数量: q.size() std::endl; } return 0; }queue输出priority_queue示例#include iostream #include queue #include vector int main() { // 创建一个空的priority_queue默认是最大堆 std::priority_queueint pq; // 插入元素 pq.push(30); pq.push(10); pq.push(20); pq.push(40); // 访问堆顶元素 std::cout 优先级最高的元素: pq.top() std::endl; // 弹出元素 pq.pop(); std::cout 弹出一个元素后新的堆顶: pq.top() std::endl; // 遍历priority_queue需要复制因为无法直接遍历 std::priority_queueint copy pq; std::cout 剩余的元素: ; while(!copy.empty()) { std::cout copy.top() ; copy.pop(); } std::cout std::endl; return 0; }priority_queue输出六、总结C STL提供了丰富多样的容器适用于各种不同的数据存储和管理需求。理解每种容器的特点、内部实现原理以及性能特性可以帮助开发者在实际应用中做出最佳的选择从而编写出高效且可维护的代码。优先用vector绝大多数顺序存储、随机访问、尾部操作场景频繁首尾增删用deque频繁中间增删用 list无序查询优先unordered_map/unordered_set有序遍历、排序需求用 map/set栈场景用stack、队列任务用 queue、优先级调度用 priority_queue结语本文全覆盖了C所有高频STL容器从原理、性能、场景、实战代码全方位拆解做到看完即会、学完即用。熟练掌握这些容器的取舍逻辑不仅能轻松应对算法刷题、面试笔试更能写出高效、优雅、健壮的工程代码。建议收藏本文日常开发遇到容器选型、语法遗忘时随时查阅彻底吃透STL夯实C核心功底
返回列表