前言:
本篇是对于c++中常用容器的基本实现原理,使用该容器优缺点及部分方法的时间空间复杂度的讲解。
vector容器
底层原理:
std::vector
是一种动态数组容器,可以根据需要自动扩展和收缩。它的底层实现主要依赖于一个连续的内存块。
当创建一个空的std::vector
时,其底层会分配一块初始大小的内存空间作为存储容器元素的区域。随着向std::vector
添加元素,如果超过了当前内存空间的大小,它会重新分配更大的内存块,并将原有元素复制到新分配的内存中
优缺点:
优点:
-
高效的随机访问:由于
std::vector
使用连续的内存块来存储元素,因此可以通过下标直接访问元素,具有较高的访问速度。 -
动态大小:
std::vector
可以根据需要动态增长或缩小,灵活适应不同的数据量需求。但是当后续内存需求超过当前内存碎片大小,则整个容器进行新的地址选择。若一直未找到合适的内存块,则容器会分散存储数据。当删除数据时,内存不会自动删除,以备后续使用 -
连续内存分配:由于元素在内存中是连续存储的,所以在某些情况下可以提高 CPU 缓存命中率和性能。
-
快速尾部插入和删除:由于
std::vector
内部使用指针管理元素,在末尾进行插入和删除操作非常高效。
缺点:
-
插入和删除开销较大:在中间位置插入或删除元素时,需要将后续元素移动到新位置。这可能导致较大的时间开销。不如链表等非连续存储结构的插入
-
动态扩展会导致重新分配与拷贝:当
std::vector
的容量不足以容纳新的元素时,会触发重新分配,并将原有元素拷贝到新的内存空间中。这可能导致性能损失。 -
不适合频繁插入和删除操作:如果需要频繁进行插入和删除操作,特别是在中间位置,可能不是最优的选择。其他容器如
std::list
或std::deque
在这方面更为高效。
综合考虑,在大部分情况下,std::vector
是一个高效、灵活的容器,特别适用于需要随机访问和快速尾部插入/删除的场景。但对于频繁的插入和删除操作,以及需要保证稳定内存地址的需求,则可以考虑其他容器。
set容器和unordered_set容器
set:
底层原理:
std::set
容器是基于红黑树(Red-Black Tree)实现的,它是一种自平衡二叉搜索树。红黑树具有以下特性:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色的。
- 所有叶子节点(NULL 节点)都是黑色的。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 对于任意节点到其所有后代叶子节点的简单路径上,经过的黑色节点数量相同。
这些特性确保了红黑树在插入、删除和查找操作时保持平衡,从而提供了较好的性能。
用迭代器访问时,*(迭代器),可以显示出迭代器指向的元素。
优缺点:
优点:
-
唯一性:set容器中的元素是唯一的,不会存在重复值。当需要确保集合中没有重复元素时,可以使用set。
-
自动排序:set容器中的元素默认按照升序进行排序,这对于需要有序存储数据的场景非常方便。但每次插入和查找数据的时间复杂度为O(log n ),
-
快速查找:由于set内部使用了二叉搜索树(红黑树)实现,所以在其中查找特定元素的效率很高。
-
插入和删除效率较高:与向数组或链表插入或删除元素相比,向set容器插入或删除元素通常具有较好的性能,这是因为内部的存储结构并非为连续的,删除插入元素并不会影响其他元素。
缺点:
不支持随机访问(即没有下标访问),内存开销较大。插入和查找数据的时间复杂度为O(log n )。只能以迭代器访问元素。
unordered_set
:
底层原理:
std::unordered_set
容器则是使用哈希表(Hash Table)实现的。哈希表通过将元素与其对应的哈希值进行关联来存储和检索数据。内部使用数组来保存元素,并使用散列函数将键转换为数组索引。如果多个键具有相同的散列值,则使用链表或其他方法处理冲突。
哈希表具有快速的插入、删除和查找操作,平均情况下具有常数时间复杂度。然而,在最坏情况下,哈希表可能会出现碰撞(collision),导致性能下降并增加搜索时间。
总结起来,std::set
使用红黑树实现,保持有序性,而 std::unordered_set
则使用哈希表实现,提供更快的插入、删除和查找操作,但不保持有序。
优缺点:
优点:
- 快速的查找:由于使用了哈希表,unordered_set具有快速的查找性能,平均时间复杂度为O(1)。
- 高效的插入和删除操作:无序集合支持高效地进行元素的插入和删除操作,平均时间复杂度也为O(1)。
- 不重复性:unordered_set确保存储的元素不会重复。
缺点:
- 无序性:作为一个无序容器,unordered_set中元素没有特定的顺序。如果需要按照一定顺序遍历或访问元素,则不适合使用该容器。
- 内存占用较大:相比于有序容器,unordered_set通常需要更多的内存空间来维护哈希表结构。
- 哈希冲突:由于使用哈希表,可能会出现不同元素映射到相同位置(哈希冲突)的情况。当哈希冲突较多时,查询性能可能会降低。
map和unordered_map容器
map容器:
底层原理:
map容器是C++ STL中的关联容器之一,它实现了键值对(key-value)的存储和快速查找。其内部实现基于红黑树数据结构。map容器的键在默认情况下按递增排序。值不进行排序。
优缺点:
优点:
- 有序性:map会按照键的顺序进行排序,这样可以方便地进行范围查找或遍历操作。
- 动态性:map容器支持动态插入和删除元素,可以在运行时根据需要进行修改。
- 对于特定的规则的查找(例如大于或小于某些数字)的时间复杂度为log(n)。
缺点:
- 内存开销:相比于其他数据结构如vector或array,map占用的内存空间较大。因为它需要额外保存键值对之间的关联信息。
- 速度略慢:与数组或哈希表相比,在插入和访问元素时,由于红黑树维护有序性的额外开销,map容器可能稍微慢一些。
- 迭代器失效:当插入或删除元素时,迭代器可能会失效。这意味着在循环遍历过程中做改变可能导致不可预料的结果。
- 插入和删除:由于map内部是基于红黑树实现的,删除和插入操作的时间复杂度(O(log n))。
unordered_map:
底层实现:
哈希表。哈希函数:首先,对于每个键(Key),使用哈希函数将其转换为一个整数值。这个整数值就作为该键在哈希表中的位置索引。
桶结构:哈希表内部由多个桶(buckets)组成,每个桶可以存储一个或多个键值对。通过哈希函数计算得到的位置索引决定了具体放入哪个桶中。
容器特点:
优点:
- 快速的插入和查找:unordered_map 使用哈希表实现,具有常数时间复杂度(O(1))的插入和查找操作,相对于其他关联容器来说速度更快。和数组的查询时间一致,和链表插入时间一致
- 灵活性:unordered_map 可以存储任意类型的键值对,并且支持自定义的哈希函数和比较函数。
- 内存效率:由于使用哈希表实现,unordered_map 可以动态地调整桶的大小,节省内存空间。
缺点:
- 无序性:由于 unordered_map 是基于哈希表实现的,其元素在内部是无序存储的。这在某些应用场景下可能不符合需求。
- 迭代顺序不确定性:由于无序性,unordered_map 的迭代顺序是不确定的,即使元素没有改变也可能导致遍历结果不同。
- 哈希冲突影响性能:当哈希冲突发生时,需要额外的链表或红黑树来处理冲突。这可能导致一些操作在最坏情况下时间复杂度达到 O(n),尽管平均情况下仍然是常数时间复杂度。
list容器
实现原理:
数据结构:list 是由一个个节点组成的双向链表,每个节点包含两个指针,分别指向前一个节点和后一个节点。这种数据结构使得在 list 中插入、删除元素时效率很高。
-
迭代器:list 提供了双向迭代器(bidirectional iterator),可以在容器内部进行遍历操作。
优缺点:
-
插入操作:在 list 中插入元素时,只需调整相邻节点的指针即可,不需要移动其他元素。因此,在任何位置插入或删除元素都是常数时间复杂度(O(1))。
-
删除操作:同样地,删除元素只需调整相邻节点的指针即可完成。与数组不同,在 list 中删除元素不会引起其他元素的移动。
-
空间分配:当有新的元素被插入时,list 动态地分配新的内存空间来保存新节点,并自动处理内存管理。这使得 list 可以灵活地增长或缩小。并且由于list并非是连续存储结构
-
元素存储和访问:每个节点中存储着实际的元素值,可以通过迭代器进行访问和修改。
总体而言,list 采用双向链表作为底层数据结构实现,在插入、删除元素时具有较高的效率。但由于链表节点需要额外的指针来维护连接关系,会占用更多的内存空间,并且不支持随机访问,因此在需要频繁随机访问元素或对内存占用有严格要求的情况下,可能不是最优选择。
queue容器:
底层原理:
queue容器是堆的存储结构,遵循先进先出:即最早进入队列的元素首先被移除。容器只支持对两端进行操作。
底层容器选择:默认情况下,queue 使用 deque 作为底层容器。deque 是一个双端队列,支持在两端进行元素的插入和删除操作。
优缺点:
-
入队操作:当向 queue 中插入元素时,会调用底层容器的 push_back() 方法,在 deque 的尾部插入新元素。时间复杂度为o(1)
-
出队操作:当从 queue 中取出元素时,会调用底层容器的 front() 方法获取队头元素,并调用 pop_front() 方法将其移除。
-
大小统计:可以使用 size() 方法获取 queue 中当前存储的元素个数。
适用场景:
-
消息队列:在异步处理任务时,可以将任务添加到队列中,然后按照添加顺序依次处理。
-
广度优先搜索:在图或树等数据结构的广度优先搜索算法中,使用队列来存储待访问的节点,以确保按层级顺序进行遍历。
-
缓存管理:当需要缓存最近使用的元素时,可以使用队列来维护缓存,并通过出队操作来移除最旧的元素。
-
请求调度:在多线程或多进程环境中,可以使用队列作为任务调度器,将请求添加到队列中,并由工作线程或进程按照顺序进行处理。
stack栈容器:
实现原理:
使用 std::deque
作为其底层容器。也可以通过指定不同的底层容器类型来创建不同类型的栈,比如使用 vector
或 list
。无论使用哪种底层容器,栈的基本操作都是通过调用相应底层容器提供的操作来完成。栈容器的特点是先进后出,且只能在头部进行操作。
常用函数:
push()
:将元素添加到底层容器的末尾。pop()
:移除底层容器末尾的元素。top()
:访问并返回底层容器末尾的元素。empty()
:检查底层容器是否为空。size()
:获取底层容器中元素的数量。
由于容器特征,函数操作的时间复杂度都为O(1)。