为什么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功能更好的东西,但我仍然很好奇。
答案 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;
当a
和b
是每个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的n
和m
,您现在就完成了。否则,您必须为第一个n/m
个连续元素(此处假设m
)重复该方案n > m
时间。
这个更复杂的算法要快得多。
对于双向迭代器,可以使用另一种传奇的O(3n)算法,称为“翻转手”。根据Jon Bentley的书Programming Pearls,它在早期的UNIX编辑器中用于移动文本:
将手放在你面前,一个在另一个上方,竖起大拇指。现在
在代码中:
reverse(first, middle);
reverse(middle, last);
reverse(first, last);
对于随机访问迭代器,可以通过swap_ranges()
(或POD类型的memmove()
操作)重新定位大块内存。
利用汇编程序操作进行微优化可以提供额外的加速度,可以在禁食算法的基础上完成。
使用连续元素而不是在内存中“跳转”的算法也会导致现代计算机体系结构中的缓存未命中次数减少。