【问题标题】:Best Container Type最佳容器类型
【发布时间】:2017-07-12 03:42:08
【问题描述】:

我正在开发一个需要“滑动窗口”的应用程序,它只是一个字符串容器,

Window  = ["the", "dog", "is", "hungry"]

应用程序处理大型文本文件,当满足某些条件时,窗口会在末尾添加一个新字符串并删除第一个元素,

即说“现在”是下一个要添加的词,那么

Window <- Window.AddToEnd("now") and Window.DeleteFirst()

就这样,Window = ["dog", "is", "hungry", now"]

每次 Window 更改时,都会在顺序很重要(即索引很重要)的元素上运行一个进程。

最初我使用了一个字符串向量并尝试了一个双端队列。 我很好奇人们会认为什么是最好的?

总而言之,我需要一个包含索引的字符串容器,其中非常定期地添加一个新元素 + 删除第一个元素。我还需要遍历容器 A LOT。

其他一些信息:

  1. 字符串在容器中永远不会被修改一次
  2. 窗口元素永远不会改变(除了讨论的 pop 和 push)
  3. 窗口的大小直到运行时才知道(用户传递的东西)
  4. Window 永远不会改变大小,一旦在启动时初始化,它就会在整个应用程序中保持该大小。

任何建议将不胜感激,干杯大卫

【问题讨论】:

  • 您也可以只使用环形缓冲区(虽然标准库中没有现成的实现,但实现起来很简单)。
  • 嗯,列表或循环缓冲区都可以满足您的需求。我建议从列表开始。
  • 循环缓冲区是你最好的选择

标签: c++ containers stdvector deque


【解决方案1】:

除了std::deque,根据cplusplus.com

提供类似于向量的功能,但在序列的开头也可以有效地插入和删除元素,而不仅仅是在序列的结尾。但是,与向量不同,deque 不能保证将其所有元素存储在连续的存储位置

您也可以使用固定大小的向量,并保存起始索引。

所以它就像一个循环数组。例如:

一个初始为[a, b, c, d]的向量v,开始索引=0;在 'e' 进来之后,它是 [e, b, c, d],但现在开始索引 = 1。第 i 个元素是v[(beginning_index + i) % v.size()]。这个方案很容易自己实现。

【讨论】:

    【解决方案2】:

    这是一个圆形容器的类模板示例,容器的大小在运行时是恒定的,但在类模板的实例化期间设置。要使用它,必须提前知道或预先计算大小;如果您不知道大小是多少,那么您可以考虑使用与此类似的算法,而无需模板或其他容器。如果尺寸不是很大,这确实很好用。正如您在addString() 函数中看到的那样,当添加的元素开始超过所包含数组的大小时,会调用一个 for 循环,该循环必须移动该数组中的所有内容。这对于大约 100 或 1000 个元素的数组来说很好,但是当您获得大小为 100,000 或 1,000,000 个条目的数组时;这将成为瓶颈并且非常缓慢,但是这确实提供了将所有内容向左移动一个空格并在数组、列表或容器的末尾添加的机制。

    #include <iostream>
    #include <memory>
    #include <string>
    
    
    template<unsigned Size>
    class CircularContainer {
    public:
        const static unsigned SIZE = Size;
    private:
        std::string data_[SIZE];
        unsigned counter_;
    
    public:
        CircularContainer() : counter_(0) {}
    
        void addString( const std::string& str ) {
            // In a real container this would be a member and not static
            // If you have a static here, and you have multiple instances
            // It will still increment across all instances.
            //static unsigned counter = 0;
    
            if ( counter_ < SIZE ) {
                data_[counter_++ % SIZE] = str;
            } else {
                // This function can be expensive on large data sets
                // due to this for loop but for small structures this
                // is perfectly fine.
                for ( unsigned u = 0; u < SIZE-1; u++ ) {
                    data_[u] = data_[u+1];
                }
                data_[SIZE - 1] = str;
            }
        }
    
        std::string& getString( unsigned idx ) {
            if ( idx < 0 || idx >= SIZE ) {
                return std::string();
            } else {
                return data_[idx];
            }
        }
    
        unsigned size() const {
            return SIZE;
        }
    };
    
    int main() {
    
        CircularContainer<4> cc;
    
        cc.addString( "hello" );
        cc.addString( "world" );
        cc.addString( "how" );
        cc.addString( "are" );
    
        for ( unsigned u = 0; u < cc.size(); u++ ) {
            std::cout << cc.getString( u ) << "\n";
        }
        std::cout << std::endl;
    
        cc.addString( "you" );
        cc.addString( "today" );
    
        for ( unsigned u = 0; u < cc.size(); u++ ) {
            std::cout << cc.getString( u ) << "\n";
        }
    
    
        std::cout << "\nPress any key and enter to quit." << std::endl;
        char c;
        std::cin >> c;
    
        return 0;
    }
    

    现在你可以适应这个了;并交换此类中的原始字符串数组并使用带有指针链接的堆,然后您所要做的就是将您的begend 指针重新分配给适当的数据,因为中间的所有其他内容都已经链接就像链条的碎片。


    编辑

    我已经扩展了这个类来接受任何类型,而不是使用该数据类型的原始数组,我用std::shared_ptr 替换了数组。这是对上述类的重新设计。我还解决了使用static counter 的问题并将其设为会员。

    #include <iostream>
    #include <memory>
    #include <string>
    
    template<typename T, unsigned Size>
    class CircularBuffer {
    public:
        const static unsigned SIZE = Size;
    private:
        std::shared_ptr<T> data_[SIZE];
        unsigned counter_;
    public:
        CircularBuffer() : counter_(0) {}
        ~CircularBuffer() {}
    
        void addItem( const T& t ) {
    
            if ( counter_ < SIZE ) {
                data_[counter_++ % SIZE] = std::make_shared<T>( t );
            } else {
                for ( unsigned u = 0; u < SIZE - 1; u++ ) {
                    data_[u] = data_[u + 1];
                }
                data_[SIZE - 1] = std::make_shared<T>( t );
            }
        }
    
        T getItem( unsigned idx ) {
            if ( idx < 0 || idx >= SIZE ) {
                throw std::exception( "Array Buffer Out of Bounds!" );
            } else {
                return *(data_[idx].get());
            }
        }
    
        unsigned size() const {
            return SIZE;
        }
    };
    
    int main() {
        CircularBuffer<std::string, 5> cb;
        cb.addItem( "hello" );
        cb.addItem( "world" );
        cb.addItem( "how" );
        cb.addItem( "are" );
        cb.addItem( "you" );
    
        for ( unsigned u = 0; u < cb.size(); u++ ) {
            std::cout << cb.getItem( u ) << "\n";
        }
        std::cout << std::endl;
    
        cb.addItem( "today" );
        cb.addItem( "my" );
        cb.addItem( "friend" );
    
        for ( unsigned u = 0; u < cb.size(); u++ ) {
            std::cout << cb.getItem( u ) << "\n";
        }
        std::cout << std::endl;
    
    
        std::cout << "\nPress any key and enter to quit." << std::endl;
        char c;
        std::cin >> c;
    
        return 0;
    }
    

    它仍然使用指定的数组,并使用相同的技术围绕该数组的索引进行循环。这里唯一的区别是我使用了std::shared_ptr&lt;T&gt; 的数组。这将导致存储的对象位于堆上,而不是类模板作用域的本地堆栈上。清理内存应该自动完成,但并不总是保证。除非您明确需要它们,否则不需要跟踪 headtail 位置,并且添加到此类模板中应该不难。

    【讨论】:

    • @Jarod42 我知道;但我只是为了简单起见这样做,因为它会递增,并且它的模数和大小将始终环绕索引,而对于这个演示,我只想将它初始化为 0 一次。
    • static addString 中的计数器是错误的。当存在多个实例时,它们会错误地使用同一个计数器。此外,它不是循环缓冲区。您的回答与下面的@Charles 没有区别。
    • @scinart 在多个实例中都是正确的,我可以在原始答案中记下这一点
    【解决方案3】:

    您可以使用std::deque

    #include <deque>
    #include <string>
    #include <vector>
    
    int main() {
    
        std::deque<std::string> strs;
    
        // .. initialize;
        std::string tmp;
        while (true) {
            strs.pop_front();
            std::cin >> tmp;
            strs.push_back(tmp);
        }
    
        return 0;
    }
    

    std::list 也共享相同的功能。


    如果你想限制分配/释放内存的次数,你也可以像这样使用std::vector

    #include <vector>
    #include <string>
    
    int main() {
    
        std::vector<std::string> strs;
    
        // ...
    
        std::string tmp;
        while (true) {
            for (size_t i = 1; i < strs.size(); ++i)
                strs[i-1] = std::move(strs[i]);
         // strs.erase(strs.begin()); also works.
    
            std::cin >> tmp;
            strs.back() = tmp;
        }
    
        return 0;
    }
    

    这使您可以保留所需的任意多个位置,然后仅移动值。

    【讨论】:

    • 对于这么广泛的问题有点片面。
    • 我不认为传染性内存容器最适合这个问题。所以,std::vector 不是一个好的选择。
    • 您的std::vector 版本有效,但不是最理想的。你可能应该只做strs.erase(strs.begin())。至少你应该移动元素。
    • 第二种解决方案真的很糟糕,不应该使用,这是很多不必要的复制。
    • @Frank 我刚刚添加了std::move 电话。这有帮助吗?
    猜你喜欢
    • 2010-09-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-06
    • 1970-01-01
    • 2012-07-04
    • 1970-01-01
    • 2010-09-10
    相关资源
    最近更新 更多