【问题标题】:Why is an STL deque not implemented as just a circular vector?为什么一个 STL 双端队列不被实现为一个循环向量?
【发布时间】:2017-01-12 10:43:07
【问题描述】:

我一直认为在 C++ 标准模板库 (STL) 中,双端队列 (deque) 是具有循环边界条件的大小可变数组(类似于向量),这意味着有一个头指针 i 和一个尾指针j 都指向数组a[0..L-1] 的某个位置。 push_front 是i--,push_back 是j++,pop_front 是i++,pop_back 是j--。当指针ij 到达L-1 时,它会重新出现在数组的另一端(分别为0L-1)。如果数组大小用尽(插入新元素后的指针i==j),则将原始大小翻倍的更大空间重新分配给a[],并像在向量中一样复制数据。考虑到循环边界条件,还有O(1) 时间随机访问。但是有人告诉我,在 STL 双端队列内部实际上有一个指针数组指向许多固定长度的数组段。它比圆形向量复杂得多。不使用简单的循环向量来实现双端队列的功能有什么好处?随机访问会变慢吗?

【问题讨论】:

  • 好处是擦除项目很快。您不必重新分配整个内存块并将所有剩余的项目从旧的复制到新的。
  • @VioletGiraffe:你也永远不必为他所说的结构重新分配擦除。从前面或后面移除将是 O(1)(头部或尾部索引的破坏和增量或减量)。从中间移除将是 O(N),但考虑到 deque 是双端队列的缩写,这似乎不会违背容器的目的。
  • deque 不是一个循环容器,它就像一个向量链表。有开始也有结束
  • FWIW,我有你描述的容器,我从来没有想用 std::deque 代替。
  • @BenjaminLindley:您的容器在删除项目时不会释放其内存,这可能是不可取的。

标签: c++ stl deque


【解决方案1】:

std::deque(双端队列)是一个索引序列容器,允许在其开头和结尾快速插入和删除。此外,在双端队列的任一端插入和删除都不会使指针或对其余元素的引用无效。

std::vector 不同,双端队列的元素不是连续存储的:典型实现使用一系列单独分配的固定大小数组。

双端队列的存储会根据需要自动扩展和收缩。扩展双端队列比扩展std::vector 便宜,因为它不涉及将现有元素复制到新的内存位置。

deques上常见操作的复杂度(效率)如下:

  • 随机访问 - 常数 O(1)
  • 在末尾或开头插入或删除元素 - 常数 O(1)
  • 插入或移除元素 - 线性 O(n)

来源:std::deque

【讨论】:

    【解决方案2】:

    正如cppreference 所写

    与 std::vector 不同,双端队列的元素不是连续存储的:典型的实现使用一系列单独分配的固定大小数组。

    这意味着std::vector 偶尔会进行大型内部重新分配,而不是由std::deque 执行。当空间用完时,只添加一个小的固定大小的数组。 (当空间因擦除而变得太大时,会发生相同但相反的情况。)

    这是一个小测试:

    #include <vector>
    #include <deque>
    #include <string>
    #include <iostream>
    #include <chrono>
    
    
    using namespace std;
    
    
    int main()
    {
        {
            const auto start = chrono::high_resolution_clock::now();
    
            vector<string> v;
            for(size_t i = 0; i < 9999999; ++i)
                v.push_back(string("hello"));
    
            cout << chrono::duration_cast<chrono::milliseconds>(chrono::high_resolution_clock::now() - start).count() << endl;
        }
    
        {
            const auto start = chrono::high_resolution_clock::now();
    
            deque<string> v;
            for(size_t i = 0; i < 9999999; ++i)
                v.push_back(string("hello"));
    
            cout << chrono::duration_cast<chrono::milliseconds>(chrono::high_resolution_clock::now() - start).count() << endl;
        }
    
        return 0;
    }
    

    在我的机器上,在这种情况下,它显示双端队列的速度是向量的两倍:

    $ ./a.out 
    301
    164
    

    【讨论】:

    • 因子 2 是有道理的。当push_back 一个元素到大小为2^n 的向量时,向量需要将其容量加倍到2^(n+1),将所有2^n 元素移动到新空间并添加一个新元素。从大小0 到大小2^n 的分配总数是1+(2^0+1)+(2^1+1+1)+(2^2+1+...+1)+...+(2^(n-1)+1+...+1) = 1+2+2^2+...+2^n = 2^(n+1)-1,大约是2^n 的两倍。 Deque 通过移动指针数组中的数据块的内部指针而不是单个数据项,几乎避免了这个因素 2。所以双端队列被设计用来处理更多的项目。
    • 你知道每块数据有多大吗?有没有可以调整这个大小的参数?
    • @ZhuoranHe 不幸的是,它依赖于实现且不透明 - 接口不允许查询它。
    • 但是,固定大小数组的链表的分配频率远大于循环向量。为什么分配小块多次比分配少块大块快?如果有的话,我会想到另一种方式,因为大部分内存分配成本是每次分配的(无论分配的内存大小如何)。最重要的是,一旦队列大小稳定下来,循环向量就根本不需要分配,而当前的实现总是需要分配/释放内存块。
    • @max 这些都是很好的观点,但这是一个权衡。一方面,分配更频繁,另一方面复制更少。唯一要做的就是测试您拥有的特定实现和用例的替代方案,看看哪个更快。至于你的最后一句话,听起来确实有道理,一旦队列稳定,将其复制到向量中(但是,再次,每个案例测试是必要的)。
    【解决方案3】:

    std::deque 方法的主要优点是一旦插入容器中的元素永远不会移动,如果您从两端添加或删除元素。因此,在执行这些操作时,对元素的引用(和指针)不会失效(请注意,令人惊讶的是,iteratorsdeque 元素在末端进行插入或删除时反而会失效)。

    这虽然使实现更加复杂,但可以在不影响正式的 big-O 复杂性的情况下完成,并使 std::deque 成为一个非常有用的容器。

    您可以拥有std::deque 的“胖”对象,而无需使用额外的间接级别来避免移动操作并保持效率。

    【讨论】:

    • @Mehrdad:我个人不会使用太多迭代器,但我会添加一个注释。
    • 哎呀,对不起,那是误导。我的意思是指针。迭代器在任一端插入时都会失效,但指针(和引用)不会。见here
    • 我惊讶地发现,根据deque 两端的标准插入,deque 的所有迭代器都无效。
    • 您上次的编辑引入了关于迭代器失效的错误陈述。
    • 已修复。就像我说的那样,我没有使用太多带有 vectors 或 deques 的迭代器......在我看来,迭代器抽象不是很有用。
    【解决方案4】:

    23.3.8.4 [deque.modifiers](重点是我的)

    deque 中间的插入会使所有迭代器无效 以及对deque 元素的引用。在两端插入 deque 使 deque 的所有迭代器无效,但没有 对deque 元素的引用有效性的影响。

    这在类似循环向量的实现中是不可能的。

    【讨论】:

    • IIUC,使用循环数组,当您在 deque 的任一端插入时,您将不会使迭代器无效(因为由于索引,迭代器可以实现为简单的索引查找为 O(1) 与循环数组)。迭代器实际上是一种更强大的引用,从使迭代器无效但不是引用到使引用无效但不是迭代器似乎实际上是一种净改进。
    • @max 如果迭代器被实现为一个简单的索引(+ 对容器的引用),那么在更温和的意义上,由于索引的变化,在前面插入仍然会使所有迭代器无效(以前的元素 #0 现在变成元素 #1,依此类推)。
    猜你喜欢
    • 2021-12-31
    • 2012-09-27
    • 1970-01-01
    • 2011-08-09
    • 2016-06-04
    • 2014-08-29
    • 1970-01-01
    • 1970-01-01
    • 2020-04-19
    相关资源
    最近更新 更多