【问题标题】:how do you insert the value in a sorted vector?如何将值插入已排序的向量中?
【发布时间】:2013-03-28 10:26:25
【问题描述】:

所有,

这个问题是this one 的延续。 我认为 STL 错过了这个功能,但它只是我的恕我直言。

现在,问题来了。

考虑以下代码:

class Foo
{
public:
    Foo();
    int paramA, paramB;
    std::string name;
};

struct Sorter
{
    bool operator()(const Foo &foo1, const Foo &foo2) const
    {
         switch( paramSorter )
         {
             case 1:
                 return foo1.paramA < foo2.paramA;
             case 2:
                 return foo1.paramB < foo2.paramB;
             default:
                 return foo1.name < foo2.name;
         }
    }

    int paramSorter;
};

int main()
{
    std::vector<Foo> foo;
    Sorter sorter;
    sorter.paramSorter = 0;
        // fill the vector
    std::sort( foo.begin(), foo.end(), sorter );
}

在任何给定的时刻,向量都可以重新排序。 该类还具有在 sorter 结构中使用的 getter 方法。

在向量中插入新元素最有效的方法是什么?

我的情况是:

我有一个网格(电子表格),它使用一个类的排序向量。在任何给定时间,向量都可以重新排序,网格将相应地显示排序后的数据。

现在我需要在向量/网格中插入一个新元素。 我可以插入,然后重新排序,然后重新显示整个网格,但这非常低效,尤其是对于大网格。

任何帮助将不胜感激。

【问题讨论】:

  • 我认为你应该使用 set,如果你没有重复的话。或者 std::list 否则。 Vector 似乎不适合需要经常排序的东西。
  • std::set 基于红黑树,重新插入复杂度为 O(logn)。您可能需要考虑这个insert into sorted array 问题的相关树结构,例如 rb-tree、avl-tree 等。
  • @stardust_ 排序很好,我认为主要缺点是插入会导致元素移位,尤其是导致内存重新分配时。好的选择是出队 - 因为它整合了向量(搜索)和列表(插入)的所有良好特征,所有这些都取决于数据属性和它的操作。

标签: c++ sorting vector stl insertion-sort


【解决方案1】:

假设您真的想使用一个向量,并且排序标准或键不会改变(因此已插入元素的顺序始终保持不变): 在末尾插入元素,然后将其一次移动到前面,直到前面的元素不再大。

它不能更快​​地完成(关于渐近复杂性或“大 O 表示法”),因为您必须移动所有更大的元素。这就是 STL 不提供此功能的原因 - 因为它在向量上效率低下,如果需要,您不应该使用它们。

编辑:另一个假设:比较元素并不比移动它们贵多少。见 cmets。

编辑 2:由于我的第一个假设不成立(您想更改排序标准),请放弃此答案并查看我的另一个答案:https://stackoverflow.com/a/15843955/1413374

【讨论】:

  • 听起来像是二分搜索。
  • 您可以使用二分查找找到您要插入的位置。不过也没多大帮助。您仍然需要 O(n) 步骤来创建可以插入元素的间隙。有帮助,如果搬家便宜而比较昂贵。
  • 移动通常会便宜很多,特别是如果元素是整数,在 C++11 中甚至移动对象也可能很便宜。
【解决方案2】:

如果您需要始终保持向量排序,首先您可以考虑使用std::setstd::multiset 是否不会简化您的代码。

如果你真的需要一个排序好的向量,并且想快速插入一个元素,但又不想一直强制满足排序条件,那么你可以先使用std::lower_bound()在a中查找位置以对数时间插入元素的排序范围,然后使用vector的成员函数insert()在该位置插入元素。

如果性能是一个问题,请考虑对 std::liststd::vector 进行基准测试。对于小项目,众所周知,std::vector 更快,因为缓存命中率更高,但insert() 操作本身在列表上的计算速度更快(无需移动元素)。

【讨论】:

  • 我可以根据不同的排序标准重新排序 std::set 吗?尤其是那些具有非唯一值的?
  • @Igor:不,您不能根据不同的标准重新排序std::set。但您可以将内容移至新的std::set,并使用不同的标准进行排序。
  • 感谢您的回答。我只是用谷歌搜索“插入排序向量”并最终使用std::set。另外,请注意您关于性能的评论:std::vector 在几乎所有情况下都会获胜:Stroustrup: Why you should avoid linked lists?
  • @H2CO3:我很高兴它有帮助。事实上,Bjarne 多次提到 std::vector 应该是 C++ 中容器的默认选择。
  • 还需要注意的是setmultiset在插入元素时不会使引用和迭代器失效,vector可能会。
【解决方案3】:

而不是插入和排序。你应该做一个查找然后插入

保持向量排序。 (排序一次)。当你必须插入时

  1. 找到第一个与您要插入的元素相比更大的元素。

  2. 在该位置之前插入。

这样向量保持排序。

这是一个例子。

start {} empty vector

insert 1 -> find first greater returns end() = 1 -> insert at 1 -> {1}
insert 5 -> find first greater returns end() = 2 -> insert at 2 -> {1,5}
insert 3 -> find first greater returns 2 -> insert at 2 -> {1,3,5}
insert 4 -> find first greater returns 3 -> insert at 3 -> {1,3,4,5}

【讨论】:

  • 但请记住,这效率不高(查找和插入都是 O(n) 操作)。如果元素多于少,我的答案 (stackoverflow.com/a/15843629/1413374) 中的算法会更有效。
  • @Sebastian:实际上,您可能希望在声明之前对其进行概要分析。效率并不完全受算法复杂性的限制。此外,在这种情况下查找将是 O(log n),因为您可以使用二进制搜索。
  • @stardust_,我不能“排序一次”。我的向量可以在任何给定时刻通过不同的类字段来使用。如果我从按名称排序的向量开始,用户可能会按 paramA 或 paramB 排序,然后插入新元素。问题是我不知道将使用哪个分拣机。
  • @Benjamin:在我的答案的 cmets 中讨论了 O(log(n)) 的发现。而且,是的,渐近复杂性并不是一切。但这是重要的第一步。
  • @Igor 实际上你甚至不必排序一次(如果你从空向量开始)。重要的是您插入的位置。如果您始终在正确的位置插入,则您的向量总是被排序。无需在任何时候对其进行排序。想想吧。
【解决方案4】:

当您想在排序顺序之间切换时,可以使用多个索引数据结构,每个索引数据结构都保持排序顺序(可能是某种平衡树,例如 std::map,它将排序键映射到向量索引, 或 std::set 来存储指向你的对象的指针 - 但具有不同的比较函数。

这是一个执行此操作的库:http://www.boost.org/doc/libs/1_53_0/libs/multi_index/doc/index.html

对于每次更改(插入新元素或更新键),您必须更新所有索引数据结构,或将它们标记为无效。

如果您的数据结构没有“太多”排序顺序并且没有“太多”更新,则此方法有效。否则 - 运气不好,每次要更改订单时都必须重新排序。

换句话说:您需要的索引越多(以加快查找操作),更新操作所需的时间就越多。当然,每个索引都需要内存。

为了减少索引的数量,您可以使用一些查询引擎来组合多个字段的索引,以支持多个字段的更复杂的排序顺序。就像一个 SQL 查询优化器。但这可能是矫枉过正...

示例:如果你有两个字段,a 和 b,你可以支持 4 种排序顺序:

  1. 一个
  2. b
  3. 先 a 然后 b
  4. 先是 b,然后是 a

有 2 个索引(3. 和 4.)。 随着更多的字段,排序顺序的可能组合变得更大、更快。但是您仍然可以使用“几乎按照您想要的方式”排序的索引,并且在查询期间,根据需要对您无法使用该索引捕获的剩余字段进行排序。对于整个数据的排序输出,这没有多大帮助。但如果你只想查找一些元素,第一个“缩小范围”会有很大帮助。

【讨论】:

    【解决方案5】:

    请注意,您也可以根据需要使用upper_boundupper_bound 将确保与其他条目等效的新条目将出现在其序列的结尾lower_bound 将确保与其他条目等效的新条目将出现在开头他们的顺序。对于某些实现可能很有用(也许类可以共享“位置”但不是所有细节!)

    两者都会向您保证向量会根据元素的&lt;结果排序,尽管插入lower_bound意味着移动更多元素。

    例子:

    insert 7 @ lower_bound of { 5, 7, 7, 9 } => { 5, *7*, 7, 7, 9 }
    insert 7 @ upper_bound of { 5, 7, 7, 9 } => { 5, 7, 7, *7*, 9 }
    

    【讨论】:

    • 它们之间的速度差异取决于比较或复制是否更昂贵以及向量有多大。上/下界函数为 O(logN),插入为 O(N)。考虑这个病态的案例:insert 0 into { 0, 0, 0, ... /* lots of zeros */, 0 }
    【解决方案6】:

    问题的简单答案:

    template< typename T >
    typename std::vector<T>::iterator 
       insert_sorted( std::vector<T> & vec, T const& item )
    {
        return vec.insert
            ( 
                std::upper_bound( vec.begin(), vec.end(), item ),
                item 
            );
    }
    

    带有谓词的版本。

    template< typename T, typename Pred >
    typename std::vector<T>::iterator
        insert_sorted( std::vector<T> & vec, T const& item, Pred pred )
    {
        return vec.insert
            ( 
               std::upper_bound( vec.begin(), vec.end(), item, pred ),
               item 
            );
    }
    

    其中 Pred 是类型 T 上的严格排序谓词。

    为此,输入向量必须已按此谓词排序。

    这样做的复杂性是O(log N) 用于upper_bound 搜索(查找插入位置),但高达O(N) 用于插入本身。

    如果没有任何重复,您可以使用std::set&lt;T&gt; 以获得更好的复杂性,如果可能有重复,则可以使用std::multiset&lt;T&gt;。这些将自动为您保留排序顺序,您也可以在这些上指定自己的谓词。

    您还可以做很多其他更复杂的事情,例如管理 vectorset / multiset / sorted vector 新添加的项目,然后在它们足够多时将它们合并。对您的集合进行任何类型的迭代都需要遍历这两个集合。

    使用第二个向量具有保持数据紧凑的优势。在这里,您的“新添加”项目vector 将相对较小,因此插入时间将为O(M) 其中M 是此向量的大小,并且可能比插入大向量中的O(N) 更可行时间。合并将是 O(N+M),这比 O(NM) 一次插入一个要好,所以总的来说,插入 M 元素然后合并是 O(N+M) + O(M²)

    您可能也会将插入向量保持在其容量,因此随着您的成长,您将不会进行任何重新分配,而只是移动元素。

    【讨论】:

    • 请注意,从逻辑上讲,使用 lower_bound 和 upper_bound 之间没有区别,因为只有当您插入的元素已经存在时才会有所不同。如果是这样,upper_bound 会稍微好一些,因为您将它插入到更靠近后面的位置,这意味着移动的元素更少。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-02-09
    • 2011-05-17
    • 2023-04-07
    • 2021-12-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多