一.List与vector
List 与 Vector 的区别
在编程中,List 和 Vector 是两种常见的数据结构,它们在内部实现、性能特性和适用场景上有显著差异。
List(链表)
- 动态数据结构,元素通过指针连接,无需连续内存空间。
- 插入和删除操作高效,时间复杂度为 O(1)(已知位置时)。
- 随机访问效率低,时间复杂度为 O(n),需要遍历节点。
- 典型实现:C++ 中的
std::list,Python 中的list(实际为动态数组)。
Vector(动态数组)
- 基于连续内存存储,支持快速随机访问,时间复杂度为 O(1)。
- 尾部插入/删除高效,但中间或头部操作需移动元素,时间复杂度为 O(n)。
- 内存预先分配,扩容时可能触发拷贝,但均摊时间复杂度仍为 O(1)。
- 典型实现:C++ 中的
std::vector,Java 中的Vector(线程安全)。
性能对比
-
访问速度
Vector 的随机访问性能远优于 List,因内存连续性适合 CPU 缓存预取。
-
插入/删除
List 在任意位置插入/删除更快,而 Vector 仅在尾部高效。
-
内存占用
List 每个元素需额外存储指针,空间开销较大;Vector 内存利用率更高。
List:
cpp
struct ListNode {
T data; // 存储的数据
ListNode* prev; // 前驱节点指针
ListNode* next; // 后继节点指针
};
gherkin
+---------+ +---------+ +---------+
| Prev |<---| Prev |<---| Prev |
| Data#1 | | Data#2 | | Data#3 |
| Next |--->| Next |--->| Next |
+---------+ +---------+ +---------+
vector:
在 C++ 中,std::vector 是一个动态数组,其内存布局通常由三个关键指针管理:
_M_start(或begin):指向动态数组的起始位置。_M_finish(或end):指向最后一个有效元素的下一个位置。_M_end_of_storage:指向动态数组分配的存储空间的末尾。
jboss-cli
+-------------+-------------+-----+-------------+-------------+
| 元素1 | 元素2 | ... | 元素N | 未使用空间 |
+-------------+-------------+-----+-------------+-------------+
^ ^ ^ ^
| | | |
_M_start _M_start + 1 _M_finish _M_end_of_storage
二.deque:
deque(双端队列)是 C++ STL 中的一种序列容器,支持在头部和尾部高效地插入和删除元素。与 vector 相比,deque 在头部插入和删除的时间复杂度为 O(1),但随机访问的性能略低于 vector。
deque 的核心特性
- 动态扩展:deque 由多个连续的存储块组成,可以动态扩展。
- 双端操作 :支持
push_front、pop_front、push_back、pop_back等操作。 - 随机访问 :支持通过下标(
operator[]或at())访问元素,时间复杂度为 O(1),但比 vector 稍慢。
deque 的常用操作:
cpp
#include <deque>
using namespace std;
// 初始化
deque<int> dq = {1, 2, 3};
// 头部插入
dq.push_front(0); // dq: {0, 1, 2, 3}
// 尾部插入
dq.push_back(4); // dq: {0, 1, 2, 3, 4}
// 头部删除
dq.pop_front(); // dq: {1, 2, 3, 4}
// 尾部删除
dq.pop_back(); // dq: {1, 2, 3}
// 随机访问
int val = dq[1]; // val = 2
int val2 = dq.at(2); // val2 = 3
deque 的迭代器支持
deque 支持双向迭代器,可以使用 begin()、end()、rbegin()、rend() 进行遍历:
cpp
for (auto it = dq.begin(); it != dq.end(); ++it) {
cout << *it << " ";
}
deque 的性能分析
- 插入/删除 :
- 头部或尾部操作:O(1)
- 中间插入或删除:O(n)(需要移动元素)
- 访问 :
- 随机访问:O(1)(但比 vector 慢)
- 内存占用 :
- 由于分段存储,内存开销略高于 vector。
deque 的应用场景
- 需要频繁在头部和尾部进行插入或删除操作。
- 需要随机访问,但对性能要求不如 vector 严格。
- 不适合频繁在中间位置进行插入或删除的场景。
deque 与 vector 的比较
| 特性 | deque | vector |
|---|---|---|
| 头部插入/删除 | O(1) | O(n) |
| 尾部插入/删除 | O(1) | O(1)(均摊) |
| 随机访问 | O(1)(稍慢) | O(1)(更快) |
| 中间插入/删除 | O(n) | O(n) |
| 内存连续性 | 不连续 | 连续 |
三.STL底层数据结构
STL 容器分类与特性
STL(Standard Template Library)中的数据结构主要分为序列容器 、关联容器 、无序关联容器 和容器适配器四大类。以下为详细分类及特性说明:
序列容器
序列容器按线性顺序存储元素,支持特定位置的插入和访问。
vector
- 动态数组,支持随机访问(O(1))。
- 尾部插入/删除高效(O(1)),中间或头部操作需移动元素(O(n))。
- 自动扩容,但可能引发内存重新分配。
- 典型用途:需要频繁随机访问的场景。
deque
- 双端队列,支持头尾高效插入/删除(O(1))。
- 随机访问效率接近vector(O(1)),但实际略慢。
- 内部由分段连续空间实现,扩容无vector的内存拷贝开销。
- 典型用途:队列或栈的底层实现。
list
- 双向链表,支持任意位置高效插入/删除(O(1))。
- 不支持随机访问(需遍历,O(n))。
- 提供
splice、merge等链表专用操作。 - 典型用途:频繁中间插入的场景。
forward_list
- 单向链表 ,比
list更节省空间。 - 仅支持单向遍历,插入/删除需前置迭代器。
- 无
size()方法,需手动计数。
array
- 固定大小数组,封装C风格数组,提供STL接口。
- 不支持动态扩容,但安全性高于原生数组。
- 典型用途:需编译期确定大小的场景。
关联容器
关联容器基于键(key)自动排序,使用红黑树实现,操作复杂度通常为O(log n)。
set
- 唯一键集合,元素即键且不可重复。
- 自动按升序排列(可通过比较函数自定义)。
multiset
- 允许键重复的
set,其余特性相同。
map
- 键值对集合,键唯一且排序。
- 通过键快速查找值(O(log n))。
multimap
- 允许键重复的
map,支持一键多值。
无序关联容器(C++11引入)
基于哈希表实现,元素无序,平均操作复杂度为O(1),最坏情况O(n)。
unordered_set
- 哈希实现的
set,键唯一但不排序。
unordered_multiset
- 允许键重复的
unordered_set。
unordered_map
- 哈希实现的
map,查找效率高,但内存开销较大。
unordered_multimap
- 允许键重复的
unordered_map。
容器适配器
基于其他容器封装,提供特定接口。
stack
- 后进先出(LIFO) ,默认基于
deque实现。 - 支持
push、pop、top操作。
queue
- 先进先出(FIFO) ,默认基于
deque实现。 - 支持
push(尾)、pop(头)操作。
priority_queue
- 优先级队列 ,默认基于
vector实现堆结构。 - 元素按优先级出队(默认最大堆,可自定义比较函数)。
其他特殊容器
bitset
- 固定大小的位集合,支持位操作(如与、或、移位)。
- 非STL正式容器,但常与STL配合使用。
string
- 专为字符串设计的容器,支持类似
vector的操作。 - 提供
substr、find等字符串特有方法。
选择容器的准则
- 随机访问需求 :
vector或deque。 - 频繁插入/删除 :
list或forward_list。 - 有序存储与查找 :
set/map。 - 极致查找速度(无顺序要求) :
unordered_*系列。 - 特定数据结构需求 :适配器(如
stack、queue)。
四:vector动态扩展的原理
内存分配策略
vector内部通过连续内存块存储元素。初始时分配一定容量(capacity),当元素数量(size)超过容量时触发扩容。扩容通常涉及申请更大的内存块(如原容量的1.5或2倍),具体倍数由实现决定。
cpp
std::vector<int> v = {1, 2, 3};
int* old_ptr = v.data();
v.push_back(4); // 可能触发扩容
bool is_moved = (old_ptr != v.data()); // 检查指针是否变化
元素迁移过程
扩容时需要将旧内存中的元素按顺序拷贝到新内存,并释放旧内存块。此过程保证元素的严格顺序和迭代器有效性(但原有迭代器会失效)。
关键步骤伪代码
- 计算新容量:
new_capacity = max(old_capacity * factor, new_size) - 分配新内存:
new_buffer = allocator::allocate(new_capacity) - 拷贝元素:
uninitialized_copy(old_buffer, old_buffer + size, new_buffer) - 释放旧内存:
allocator::deallocate(old_buffer, old_capacity)
优化策略
- 预留空间 :通过
reserve()预先分配足够容量避免多次扩容。 - 移动语义:C++11后支持元素移动而非拷贝,提升对象迁移效率。
- 小型缓冲区优化:某些实现对小规模数据使用栈内存避免动态分配。
五:prioriry_queue优先级队列的底层数据结构
优先级队列的底层数据结构
优先级队列(Priority Queue)的实现通常基于以下几种数据结构,每种结构在时间和空间复杂度上有不同的权衡:
二叉堆(Binary Heap)
二叉堆是最常见的优先级队列实现方式,分为最大堆和最小堆。二叉堆是一个完全二叉树,满足堆性质(父节点的优先级始终高于或低于子节点)。
- 插入操作:元素插入到堆的末尾,通过"上浮"(swim)操作调整位置,时间复杂度为 O(\\log n)。
- 提取操作:移除堆顶元素后,将末尾元素移至堆顶,通过"下沉"(sink)操作调整,时间复杂度为 O(\\log n)。
- 空间复杂度:O(n),通常用数组实现,无需额外指针开销。
斐波那契堆(Fibonacci Heap)
斐波那契堆是一种更高效但实现复杂的数据结构,适用于需要频繁插入和合并的场景。
- 插入操作:O(1) 时间复杂度,直接插入到根链表。
- 提取操作:O(\\log n) 均摊时间复杂度,需合并树并调整。
- 空间复杂度:较高,因需维护多指针和标记位。
二项堆(Binomial Heap)
二项堆由一组二项树组成,支持高效合并。
- 插入与合并:O(\\log n) 时间复杂度。
- 提取操作:O(\\log n),需合并相同阶的二项树。
平衡二叉搜索树(Balanced BST)
如 AVL 树或红黑树,也可用于实现优先级队列。
- 操作复杂度:插入、删除、查找均为 O(\\log n)。
- 优势:支持按优先级范围查询等扩展操作。
数组或链表(简单实现)
无序数组或链表可以通过以下方式实现,但效率较低:
- 插入:O(1)(直接追加)。
- 提取:O(n)(需遍历查找优先级最高的元素)。
选择依据
- 默认选择 :二叉堆在大多数场景下综合性能最优,尤其是标准库实现(如 C++
std::priority_queue)。 - 高级需求:斐波那契堆适合图算法(如 Dijkstra)中频繁修改优先级的场景。
- 扩展功能:若需支持动态优先级修改或范围查询,平衡二叉搜索树更合适。