CPH-第四章 数据结构

C++ 标准库中提供了最重要、最常用的数据结构。针对具体问题选择合适的数据结构,往往比从头实现算法更能节省时间。

4.1 动态数组(Dynamic Arrays)

动态数组的大小可以在程序运行期间改变。C++ 中最常用的动态数组是 vector。

  • 可以像普通数组一样通过 [] 访问元素
  • 支持 push_back、pop_back、size、back 等操作
  • push_back 的平均时间复杂度为 (O(1))

string 本质上也是一种动态数组,额外提供:

  • 字符串拼接:+
  • 取子串:substr
  • 查找:find

4.2 集合结构(Set Structures)

  • 集合用于维护一组元素,支持插入、查找和删除。C++ 提供了两种主要实现:
结构 底层实现 核心操作复杂度 特点
set 平衡二叉搜索树 \(O(\log n)\) 元素有序,支持范围查询
unordered_set 哈希表 平均 \(O(1)\) 元素无序,但通常更快
  • set 中的元素互不相同。如果需要存储重复元素,可以使用:
    • multiset
    • unordered_multiset
  • 注意: multiset 中删除一个元素时,使用st.erase(st.find(x))。

4.3 映射结构(Map Structures)

  • map 用于存储键值对,可以看作一种“带下标的集合”。

    • 基于平衡二叉搜索树
    • 操作复杂度为 \(O(\log n)\)
    • 键是有序的
  • map 的典型用法包括:

    • 计数
    • 建立对象之间的关联
    • 判断某个键是否存在(count)

4.4 迭代器与范围(Iterators and Ranges)

  • 迭代器是“指向数据结构中元素”的变量。

    • begin() 指向第一个元素
    • end() 指向最后一个元素之后的位置
    • 二者构成一个半开半闭区间:[begin, end)
  • 迭代器的意义:许多标准库算法都通过迭代器范围来操作数据,例如:

    • sort
    • reverse
    • random_shuffle
  • 因此这些算法不仅适用于 vector,也适用于普通数组(传入指针即可)。

4.5 其他结构(Other Structures)

bitset

  • 每个元素只占 1 bit
  • 比普通数组节省约 32 倍内存
  • 天然支持位运算
  • 适合状态压缩或优化布尔数组

deque

  • 双端队列
  • 在两端插入和删除元素都是 \(O(1)\)
  • vector 只擅长尾端操作
  • 常数因子通常比 vector 大

stack / queue

  • stack:后进先出(LIFO)
  • queue:先进先出(FIFO)
  • 核心操作均为 \(O(1)\)

priority_queue

  • 维护一个“最值”集合
  • 插入和删除最值:\(O(\log n)\)
  • 取最值:\(O(1)\)
  • 默认是大根堆
  • 可以通过 greater 改为小根堆,priority_queue<int, vector<int>, greater<int>> q;
  • 与有序集合相比,常数因子更小

5. Policy-based data structures(GNU 扩展)

  • 类型:typedef tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update> indexed_set;

  • indexed_set 在 set 的基础上增加了按排名访问元素的能力:

    • find_by_order
    • order_of_key
  • 复杂度仍为 \(O(\log n)\)。

4.6 与排序的比较(Comparison to Sorting)

问题:计算两个列表中公共元素的个数。

  • 三种算法的思路与复杂度如下:
算法 核心操作 时间复杂度
算法 1 用 set 存储列表 A,遍历 B 查询 \(O(n \log n)\)
算法 2 用 unordered_set 替代 set 平均 \(O(n)\)
算法 3 排序两个列表,然后双指针扫描 \(O(n \log n)\)
  • 尽管算法 1 和算法 3 的渐进复杂度相同(均为 \(O(n \log n)\)),但实际运行时间相差悬殊:

    • 在 \(n = 10^6\) 时,算法 1 用时 1.5 秒

    • 算法 3 仅需 0.2 秒

    • 原因在于常数因子:

      • 排序是一个非常“紧凑”的过程
      • 只需在开头做一次排序
      • 后续扫描是线性的
      • 基于平衡树的 set 在每一步操作中都伴随指针跳转和内存分配,开销更大
  • 核心启示:

    • 渐进复杂度只是选型的第一层依据,常数因子和实际数据规模同样值得关注。
    • 在某些场景下,“先排序,再处理”可能比维护一个动态数据结构更高效。
posted @ 2026-10-28 16:35  LifeLotus  阅读(2)  评论(0)    收藏  举报