为什么std ::旋转这么快?

时间:2014-01-16 11:44:34

标签: c++ algorithm sorting c++11 stl

为什么std::rotate比cplusplus.com描述的等效函数快得多?

cplusplus.com的实施:

template <class ForwardIterator>
  void rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last)
{
  ForwardIterator next= middle;

  while (first != next)
  {
    swap (*first++, *next++);

    if(next == last)
        next= middle;
    else if (first==middle)
        middle= next;
  }
}

我有两个完全相同的插入排序算法,除了一个使用std::rotate,一个使用cplusplus.com的等效函数。我将它们设置为使用1000个int元素对1000个向量进行排序。使用std::rotate的排序需要0.376秒,而另一个需要8.181秒。

这是为什么?我不打算尝试做出比STL功能更好的东西,但我仍然很好奇。

2 个答案:

答案 0 :(得分:28)

正如评论员所说,这取决于您的标准库实施。但是您发布的代码即使对于前向迭代器也是有效的。因此,它只需要很少的要求(只有这些迭代器可以递增和解除引用)。

Stepanov的经典Elements of Programming将整个章节(10)用于rotate和其他重排算法。对于前向迭代器,代码中的一系列交换会提供O(3N)分配。对于双向迭代器,对reverse的三次连续调用会产生另一个O(3N)算法。对于随机访问迭代器std::rotate可以通过定义索引的排列 w.r.t实现为O(N)分配。到起始迭代器first

所有上述算法都是就地的。使用内存缓冲区,随机访问版本可能会受益于memcpy()memmove()的更高缓存局部性(如果基础值类型为POD),其中整个连续内存块可以是交换。如果您对数组或std::vector进行了插入排序,则标准库可能会利用此优化。

TL; DR :相信您的标准库,不要重新发明轮子!

答案 1 :(得分:19)

编辑:

由于未给出上下文,因此您的代码调用std::swap()或其他swap(a,b)算法(如

)尚不清楚
T tmp = a; a = b; b = tmp;

ab是每个int s的向量时,这将复制所有向量元素3次。 std::swap()这样的容器的专用版本std::vector<T>调用容器a.swap(b)方法,实质上只交换容器的动态数据指针。

此外,对于不同的迭代器类型,std::rotate()实现可以使用一些优化(请参阅下面的旧版,可能会产生误导性的答案)。


警告:std::rotate()的实现依赖于实现。 对于不同的迭代器类别,可以使用不同的算法 (例如,在GNU g ++的__rotate(标题中查找bits/stl_algo.h。)

要将n元素移动m=std::distance(first,middle)一个简单的(幼稚)算法,例如m个旋转,需要 O(n * m)移动或复制操作。但是只需要 O(n)移动,当每个元素直接放置到正确的位置时,这会导致(大致) m 倍的算法。

举例说明:通过三个元素旋转字符串s = "abcdefg"

abcdefg : store 'a' in temporary place
dbcdefg : move s[3] to s[0] (where it belongs in the end, directly)
dbcgefg : move s[6] to s[3]
dbcgefc : move s[9%7] to s[6] (wrapping index modulo container size: 9%7 == 2)
dbfgefc : move s[5] to s[2]
dbfgebc : move s[1] to s[5] (another wrapping around)
defgebc : move s[4] to s[1]
defgabc : move 'a' from temporary place to s[4]

对于具有最大公约数1的nm,您现在就完成了。否则,您必须为第一个n/m个连续元素(此处假设m)重复该方案n > m时间。 这个更复杂的算法要快得多。

对于双向迭代器,可以使用另一种传奇的O(3n)算法,称为“翻转手”。根据Jon Bentley的书Programming Pearls,它在早期的UNIX编辑器中用于移动文本:

将手放在你面前,一个在另一个上方,竖起大拇指。现在

  1. 转过一只手。
  2. 转过另一个。
  3. 转动两者,相互连接。
  4. 在代码中:

    reverse(first, middle);
    reverse(middle, last);
    reverse(first, last);
    

    对于随机访问迭代器,可以通过swap_ranges()(或POD类型的memmove()操作)重新定位大块内存。

    利用汇编程序操作进行微优化可以提供额外的加速度,可以在禁食算法的基础上完成。

    使用连续元素而不是在内存中“跳转”的算法也会导致现代计算机体系结构中的缓存未命中次数减少。