深入解析:浅谈deque的底层机制

目录

一、deque底层结构:分段连续内存

1️⃣核心组成

2️⃣迭代器、中控数组与缓冲区的关联图解

3️⃣deque迭代器的实现

二、deque核心机制:双端扩容与内存管理

1、双端扩容机制

2、元素存储的“伪连续”特性

三、deque的核心操作与性能

1、双端的增删:O(1)的复杂度

①头部插入(push_front)

②尾部插入(push_back)

③头部 / 尾部删除(pop_front/ pop_back)

2、随机访问:O(1)

3、中间插入 / 删除:O(n)

四、deque与vector、list的差异

⚠️五、deque使用需注意的点

1、老生常谈的迭代器失效的情况

2、避免频繁的中间操作

六、总结


在C++STL序列式容器中,deque(双端队列)是一个兼顾了vector随机访问和list双端高效操作的容器,支持首尾O(1)增删,也支持随机访问,其底层通过分段连续内存实现

deque示意图:

dequevector的最大差异,一是deque允许在常数时间内对两端元素进行插入或删除,二是deque没有所谓容量(capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。换句话说,像vector那样“因为旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque是不会发生的。


一、deque底层结构:分段连续内存

deque 既不像 vector 那样是单一连续内存块,也不像 list 那样完全离散,而是由 多个固定大小的连续内存块(缓冲区)和一个中控数组(map,不是STL中的map容器)组成的 “分段连续” 结构。

1️⃣核心组成

GCC 标准库中 deque 的底层结构包含三个关键部分:

  1. 缓冲区(buffer):大小固定的、连续的内存块,存储实际元素
  2. 中控数组(map):指针数组,每个元素都是指针,分别指向不同缓冲区的起始地址
  3. 迭代器(iterator):封装了指向当前缓冲区的指针、当前元素指针、缓冲区首尾指针,以及中控数组指针,支持跨缓冲区移动

2️⃣迭代器、中控数组与缓冲区的关联图解

注意:_M_cur可能会认为它是可以指向缓冲区已使用空间内的任一元素,其实描述的不太准确,容易产生范围上的歧义——会被误解为“整个缓冲区的物理空间中已经被使用的部分”,而忽略了deque的有效元素范围是多个缓冲区的整体,而不是单个缓冲区

假设,deque有两个缓冲区:

  • 缓冲区A(物理范围0 ~ 2):存放了三个有效元素,填满了
  • 缓冲区B(物理范围0 ~ 2):只存放了一个有效元素,在下标0位置

此时:

  • 如果说 “_M_cur指向缓冲区已使用空间的任意元素”,对缓冲区 A 来说没问题(指向 0-2),但对缓冲区 B 来说,容易让人误以为 “缓冲区 B 的已使用空间是 0,所以_M_cur只能指向缓冲区 B 的 0”—— 但deque的有效元素是 “缓冲区 A 的 0-2 + 缓冲区 B 的 0”,_M_cur还能指向缓冲区 A 的任意元素,而非局限于单个缓冲区的已用空间。

所以,更严谨的表达是:_M_cur指向deque整体有效元素序列中的任意一个元素(该元素同时属于某个缓冲区的物理范围)


3️⃣deque迭代器的实现

template 
struct _Deque_iterator {
    T* _M_cur;    // 指向当前元素
    T* _M_first;  // 指向当前缓冲区的起始
    T* _M_last;   // 指向当前缓冲区的末尾(尾后)
    T** _M_node;  // 指向中控数组中当前缓冲区的指针
    // 重载 ++ 运算符:跨缓冲区处理
    _Deque_iterator& operator++() {
        ++_M_cur;
        if (_M_cur == _M_last) {  // 当前缓冲区已到末尾
            _M_set_node(_M_node + 1);  // 切换到下一个缓冲区
            _M_cur = _M_first;         // 指向新缓冲区的起始
        }
        return *this;
    }
    // 重载 +n 运算符:随机访问
    _Deque_iterator operator+(size_t n) const {
        _Deque_iterator tmp = *this;
        tmp._M_cur += n;
        // 处理跨缓冲区的情况(省略具体逻辑)
        return tmp;
    }
    // 切换缓冲区的辅助函数
    void _M_set_node(T** new_node) {
        _M_node = new_node;
        _M_first = *new_node;
        _M_last = _M_first + BufSiz;
    }
};
  • 迭代器通过 _M_node 关联中控数组,通过 _M_first/_M_last 标记当前缓冲区的边界
  • 跨缓冲区移动时,自动切换中控数组的指针并重置缓冲区边界,保证迭代器的连续性

二、deque核心机制:双端扩容与内存管理

1、双端扩容机制

deque 的中控数组支持两端扩容

  • 头部不足时,在中控数组头部分配新空间,新增缓冲区;
  • 尾部不足时,在中控数组尾部分配新空间,新增缓冲区;
  • 中控数组扩容时,只需分配新的指针数组(通常扩大一倍),将原指针拷贝到新数组中间位置,无需移动缓冲区的元素(因为缓冲区是独立的)。

示例:初始中控数组大小为 8,当头部新增缓冲区时,中控数组扩容到 16,原指针从索引 4 开始存放,头部留出索引 0-3 用于新增缓冲区。

2、元素存储的“伪连续”特性

deque 虽然对外表现为 “连续”(支持随机访问),但底层是分段连续的:

  • 访问 deque[i] 时,先通过 i / 缓冲区大小 计算中控数组的索引(node_idx),再通过 i % 缓冲区大小 计算缓冲区内的偏移(elem_idx),最终定位到元素:*(map[node_idx] + elem_idx)
  • 随机访问的时间复杂度仍为 O (1)(计算索引是常数操作),但比 vector 多了缓冲区索引计算的开销

三、deque的核心操作与性能

1、双端的增删:O(1)的复杂度

deque的头部和尾部的增删操作都为O(1) (没有扩容的情况)

①头部插入(push_front
  1. 先检查头部缓冲区是否还有空闲时间(如果头部缓冲区已满,则分配新缓冲区并添加到中控数组头部)
  2. 然后将元素构造到头部缓冲区的空闲位置
  3. 更新头部迭代器
②尾部插入(push_back

类似头部插入,检查尾部缓冲区是否有空闲,无则分配新缓冲区,构造元素并更新尾部迭代器

③头部 / 尾部删除(pop_front/ pop_back

直接移动头部 / 尾部迭代器,销毁元素(若缓冲区为空,释放缓冲区并调整中控数组),时间复杂度 O (1)


2、随机访问:O(1)

deque支持operator[]at(),但是底层需要计算缓冲区索引和偏移,所以效率没有vector

reference operator[](size_type n) {
    return *(_M_start + n);  // 调用迭代器的 +n 运算符
}

3、中间插入 / 删除:O(n)

vector类似,中间插入 / 删除需要移动元素(若跨缓冲区,需移动多个缓冲区的元素),时间复杂度 O (n),且效率低于 listlist 中间操作 O (1))


四、deque与vector、list的差异

特性

deque(双端队列)

vector(动态数组)

list(双向链表)

内存布局

分段连续(中控数组 + 缓冲区)

单一连续内存块

完全离散(节点 + 指针)

随机访问

支持(O (1),但略慢)

支持(O (1),最快)

不支持(O (n))

头部增删

O (1)(无扩容时)

O (n)(需移动元素)

O(1)

尾部增删

O (1)(无扩容时)

O (1)(无扩容时)/ O (n)(扩容时)

O(1)

中间增删

O (n)(需移动元素)

O (n)(需移动元素)

O (1)(仅调整指针)

迭代器失效规则

插入时所有迭代器失效(中控数组扩容);删除时仅被删元素迭代器失效

扩容时所有失效;插入 / 删除中间时部分失效

仅被删元素迭代器失效

内存利用率

缓冲区可能有少量浪费;中控数组有指针开销

连续内存,可能有预留空间浪费

节点有指针开销(每个节点 2 个指针)

缓存友好性

较好(分段连续)

最好(完全连续)

较差(完全离散)

  • 双端高效增删+随机访问(如实现队列 / 栈的混合操作),选 deque
  • 极致随机访问性能(如大量 [] 操作),选 vector
  • 频繁中间增删(如链表结构场景),选 list

⚠️五、deque使用需注意的点

1、老生常谈的迭代器失效的情况

deque 迭代器失效规则比 vector 复杂:

  • 插入元素时
    • 若中控数组未扩容,迭代器可能有效(但不保证)
    • 若中控数组扩容,所有迭代器、指针、引用失效(中控数组地址改变)
  • 删除元素时
    • 若删除的是头部 / 尾部元素,仅指向被删元素的迭代器失效
    • 若删除的是中间元素,被删元素之后的迭代器失效(需重新获取)
deque d = {1,2,3,4};
auto it = d.begin() + 2;  // 指向 3
d.push_front(0);          // 若中控数组扩容,it 失效
d.erase(d.begin() + 1);   // 删除 1,it 仍指向 3(有效)

2、避免频繁的中间操作

deque 中间插入 / 删除的效率低于 list(需移动元素),且高于 vector(分段移动,无需整体拷贝),但总体仍为 O (n),频繁操作会导致性能下降


六、总结

deque 以 “分段连续内存” 为核心,通过中控数组缓冲区和复杂迭代器的配合,兼顾了 vector 的随机访问和 list 的双端高效操作,是 STL 中功能最均衡的序列式容器之一。其底层的 “中控数组 + 缓冲区 + 迭代器” 设计,决定了它在双端操作场景中的优势,同时也带来了迭代器实现复杂、随机访问效率略低的特点。

posted on 2025-12-21 21:29  ljbguanli  阅读(0)  评论(0)    收藏  举报