目 录CONTENT

文章目录

C++ STL:容器-顺序容器

TalentQ
2025-08-04 / 0 评论 / 0 点赞 / 10 阅读 / 0 字

引言

顺序容器(Sequence Containers)以线性序列的方式存储元素,支持高效的插入、删除、访问等操作。主要包括:

  • vector(动态数组)

  • deque(双端队列)

  • list(双向链表)

  • forward_list(单向链表)

  • array(定长数组)

1 vector

底层结构与原理:

  • vector采用动态数组实现,所有元素在内存中连续存储。

  • 容器内部维护一个指向首元素的指针、容量(capacity)和当前元素个数(size)。

  • 当插入元素导致容量不足时,vector会自动扩容,通常为原容量的2倍(实现相关,但大多指数增长)。

  • 扩容时会重新分配更大的内存块,并将原有元素移动(move或copy)到新内存,再释放旧内存。

性能分析:

  • 随机访问极快,O(1)

  • 尾部插入/删除为均摊O(1);扩容时最坏O(n)

  • 头部/中间插入/删除为O(n),因为需要移动大量元素。

适用场景:

  • 需要高效随机访问,主要在尾部操作。

  • 需要与C API进行内存连续的数据交互。

代码示例:

#include <vector>
#include <iostream>

// 功能:演示vector的常见操作
int main() {
  // 创建一个空的vector,类型为int
  std::vector<int> numbers;

  // 预分配空间,减少扩容次数,提高性能
  numbers.reserve(100);

  // 尾部插入元素,push_back操作均摊O(1)
  for (int i = 0; i < 10; ++i) {
    numbers.push_back(i);
  }

  // 随机访问:vector支持下标操作,时间复杂度O(1)
  std::cout << "第5个元素: " << numbers[4] << std::endl;

  // 遍历所有元素,采用范围for语句
  for (const int num : numbers) {
    std::cout << num << " ";
  }
  std::cout << std::endl;

  // 删除最后一个元素,pop_back操作O(1)
  numbers.pop_back();

  return 0;
}
  • push_back在尾部插入,效率高。

  • reserve提前分配空间,避免频繁扩容。

  • numbers[4]直接访问第5个元素,效率极高。

  • pop_back直接移除末尾元素,无需移动其他数据。

2 deque

底层结构与原理:

  • deque采用分段连续内存块(block)实现,每个块大小固定,由一个指针数组(map)管理。

  • 插入/删除时可在头尾分配或释放块,无需整体移动元素。

  • 支持随机访问,但实现比vector复杂,常数略大。

性能分析:

  • 随机访问O(1),但常数比vector大。

  • 头尾插入/删除O(1)

  • 中间插入/删除O(n)

适用场景:

  • 需要高效的头尾插入/删除,如队列、滑动窗口等。

代码示例:

#include <deque>
#include <iostream>

// 功能:演示deque两端插入与删除
int main() {
  std::deque<int> dq;

  // 在尾部插入元素
  dq.push_back(1);

  // 在头部插入元素
  dq.push_front(2);

  // 输出所有元素,顺序为头到尾
  for (const int num : dq) {
    std::cout << num << " ";
  }
  std::cout << std::endl;  // 输出:2 1

  // 删除尾部元素
  dq.pop_back();

  // 删除头部元素
  dq.pop_front();

  // 此时deque为空
  std::cout << "容器大小: " << dq.size() << std::endl;

  return 0;
}
  • push_frontpush_back分别在头尾插入元素,效率高。

  • pop_frontpop_back分别在头尾删除元素,效率同样高。

  • 内部结构保证了两端操作的高效性。

3 list

底层结构与原理:

  • list双向链表,每个节点包含数据和前驱、后继指针。

  • 插入/删除节点只需修改相邻节点指针,效率高。

  • 节点在内存中不连续,支持高效的插入/删除,但不支持随机访问。

性能分析:

  • 任意位置插入/删除(已知迭代器):O(1)

  • 随机访问:O(n),需顺序遍历。

适用场景:

  • 需要频繁在中间插入/删除元素,且不关心随机访问。

  • 实现如LRU缓存、链式数据处理等。

代码示例:

#include <list>
#include <iostream>

// 功能:演示list中间插入和删除
int main() {
  // 初始化list,包含3个元素
  std::list<int> lst = {1, 2, 3};

  // 获取第二个元素的迭代器
  auto it = lst.begin();
  ++it;  // 指向2

  // 在第二个元素前插入4
  lst.insert(it, 4);  // 结果:1 4 2 3

  // 遍历输出所有元素
  for (const int num : lst) {
    std::cout << num << " ";
  }
  std::cout << std::endl;

  // 删除刚插入的4
  --it;  // it现在指向4
  lst.erase(it);

  // 再次输出
  for (const int num : lst) {
    std::cout << num << " ";
  }
  std::cout << std::endl;  // 输出:1 2 3

  return 0;
}
  • 通过迭代器可以在任意位置插入和删除,效率高。

  • 插入和删除不会影响其他节点的存储位置。

4 forward_list

底层结构与原理:

  • forward_list单向链表,每个节点只包含数据和后继指针。

  • 更节省内存,适合只需单向遍历的场景。

  • 节点在内存中不连续,插入/删除需已知前驱节点。

性能分析:

  • 插入/删除(已知前驱):O(1)

  • 随机访问:O(n)

适用场景:

  • 内存敏感、仅需单向遍历的应用,如哈希表的拉链法实现。

代码示例:

#include <forward_list>
#include <iostream>

// 功能:演示forward_list头部操作
int main() {
  // 初始化forward_list,包含3个元素
  std::forward_list<int> fl = {1, 2, 3};

  // 在头部插入元素
  fl.push_front(0);  // 结果:0 1 2 3

  // 遍历输出
  for (const int num : fl) {
    std::cout << num << " ";
  }
  std::cout << std::endl;

  // 删除头部元素
  fl.pop_front();  // 结果:1 2 3

  // 再次输出
  for (const int num : fl) {
    std::cout << num << " ";
  }
  std::cout << std::endl;

  return 0;
}
  • push_frontpop_front分别在头部插入和删除,效率高。

  • 只能单向遍历,不支持随机访问。

5 array

底层结构与原理:

  • array封装的原生数组,内存连续,大小在编译期确定。

  • 无法动态扩容,效率极高,支持所有STL算法。

性能分析:

  • 所有操作O(1)

适用场景:

  • 数组大小已知且不变,追求极致性能和内存局部性。

代码示例:

#include <array>
#include <iostream>

// 功能:演示array的基本用法
int main() {
  // 创建一个长度为3的array
  std::array<int, 3> arr = {1, 2, 3};

  // 随机访问第二个元素
  std::cout << "第二个元素: " << arr[1] << std::endl;

  // 遍历所有元素
  for (const int num : arr) {
    std::cout << num << " ";
  }
  std::cout << std::endl;

  // array的大小在编译期确定,不支持插入/删除

  return 0;
}
  • array的元素在内存中连续,支持高效随机访问。

  • 不支持动态扩容和插入/删除操作。

6 性能对比

容器

内存布局

随机访问

头尾插入/删除

中间插入/删除

扩容/收缩

适用场景

vector

连续

O(1)

尾部O(1)

O(n)

自动扩容

随机访问、尾部操作

deque

分段连续

O(1)

O(1)

O(n)

自动扩容

双端操作

list

非连续(双向链)

O(n)

O(1)

O(1)

O(1)

频繁插入/删除

forward_list

非连续(单向链)

O(n)

头部O(1)

O(1)

O(1)

单向遍历

array

连续

O(1)

不支持

不支持

固定

固定大小

7 与原生实现及其他方法对比

  • 原生数组:手动管理内存,容易越界,无自动扩容和丰富成员函数,易出错。

  • 手写链表:灵活但实现复杂,维护成本高,容易内存泄漏。

  • STL容器:自动管理内存,接口统一,异常安全。推荐优先使用。

示例对比:vector与原生数组

// 原生数组
int arr[100];
arr[0] = 1;  // 需要手动管理长度与边界,易出错

// vector
std::vector<int> vec;
vec.push_back(1);  // 自动扩容,安全

vector自动扩容、越界检查、异常安全、支持迭代器,推荐使用。

8 总结与建议

  • vector:优先选择,适合大多数顺序存储需求,内存连续,效率高。

  • deque:适合双端高效操作,内存分段,随机访问稍慢。

  • list/forward_list:插入/删除频繁、元素量大时可选,但不支持高效随机访问。

  • array:适合定长、性能敏感场景。

选择容器时应结合数据访问模式、操作频率、内存需求等因素综合考虑。

0

评论区