矢量旋转似乎是神秘算法的一个特别肥沃的土壤。其中大部分都可以在野外找到,但有变化;所有高效的都需要花点心思来理解它们的功能。
如果你只向左旋转一个元素,你可以非常高效地做到这一点:
template<typename Iter>
void rotate_one(Iter first, Iter last) {
using Value = typename Iter::value_type;
if (first != last) {
Value temp = std::move(*first);
for (Iter next = std::next(first);
next != last;
first = next, next = std::next(next))
*first = std::move(*next);
*first = std::move(temp);
}
}
您可以通过执行Δ 次(更准确地说,Δ % N)来使用它来旋转delta 位置,但这需要时间 O(NΔ),对于任意 Δ,这实际上是 O(N²)。
虽然这种解决方案经常如上所示,但也可以在没有临时值对象的情况下实现它,使用交换而不是移动:
template<typename Iter>
void rotate_one(Iter first, Iter last) {
if (first != last) {
for (Iter next = std::next(first); next != last; ++first, ++next)
std::iterswap(first, next);
}
}
交换通常比移动需要更多的工作,但有可能存在一个高效的特定于容器的交换实现。无论如何,这个版本将有助于理解以后的实现。
一个众所周知且经常被引用的 O(N) 解决方案是执行三个反向操作:
template<typename Iter>
void rotate_one(Iter first, Iter newfirst, Iter last) {
std::reverse(first, newfirst);
std::reverse(newfirst, last);
std::reverse(first, last);
}
在我看来,这真的很优雅。您可以通过在纸上尝试来了解它是如何工作的:
a b ... c d w x ... y z
d c ... b a w x ... y z first rotate
d c ... b a z y ... x w second rotate
w x ... y z a b ... c d third rotate
这是众所周知的“反转句子中单词顺序”解决方案的一个特例,它首先反转每个单词的字母,然后反转整个字符串。
但是,它存在几个问题。首先,当一次移动就足够时,它(几乎)将每个元素移动两次。其次,std::reverse 需要一个双向迭代器。这没有什么问题,但算法与任何前向迭代器一起工作会更好。
另一个简单的解决方案是注意,如果您使用第一个算法,但使用增量 Δ 而不是 1,并在迭代器到达末尾时将它们包装回开头,那么如果 Δ 和 N,您将正确旋转向量是相对优质的。但是,如果它们不是相对质数,则您只会旋转一些元素;指数为 0 模 gcd(N, Δ) 的那些。要旋转整个向量,您需要对向量中的每个前 gcd(N, Δ) 元素执行 gcd(N, Δ) 次。
这是一个包含 12 个元素和 Δ 为 3 的插图:
a b c d e f g h i j k l
\ / / /
\/ / /
/ \ / /
/ /\ /
/ / \/
/ / / \
d b c g e f j h i a k l first loop
\ / / /
\/ / /
/ \ / /
/ /\ /
/ / \/
/ / / \
d e c g h f j k i a b l second loop
\ / / /
\/ / /
/ \ / /
/ /\ /
/ / \/
/ / / \
d e f g h i j k l a b c third loop
使用随机访问迭代器更容易(这是一个缺陷);这是一个示例实现。 (变量count 计算已移动的元素个数;每个元素移动一次,所以当count 达到0 时,旋转完成。这避免了必须计算GCD 才能知道运行多少次外循环。)
template<typename Container>
void rotate(Container& A, typename Container::size_type delta) {
using Value = typename Container::value_type;
using Index = typename Container::size_type;
Index n = A.size();
delta %= n;
for (Index loop = 0, count = n;
count;
++loop, --count) {
Index dst = loop;
Value tmp = std::move(A[dst]);
for (Index src = loop + delta;
src != loop;
--count, dst = src, src += (src < n - delta ? delta : delta - n))
A[dst] = std::move(A[src]);
A[dst] = std::move(tmp);
}
}
如前所述,这依赖于具有随机访问迭代器的容器。
请注意,我们可以通过使用交换来消除对临时存储的需求,就像上面第一个算法的替代版本一样。如果我们这样做,那么我们可以并行执行所有外部循环,因为它们不会相互干扰。所以我们可以一次只向前移动一个元素,将每个元素与它的 Δ-next peer 交换。
这导致以the sample implementation of std::rotate 提供的巡回演出。它确实执行 N 次交换,这可能比上面的解决方案效率稍低(执行 N + gcd(N, Δ) 移动),但只需要前向迭代器和交换:(下面的代码稍作修改以更好地符合上面的例子。)
template <class Iter>
void rotate(Iter first, Iter newfirst, Iter last) {
if(first == newfirst || newfirst == last) return;
Iter next = newfirst;
do {
std::iter_swap(first++, next++);
if (first == newfirst) newfirst = next;
} while (next != last);
for(next = newfirst; next != last; ) {
std::iter_swap(first++, next++);
if (first == newfirst) newfirst = next;
else if (next == last) next = newfirst;
}
}
上面唯一棘手的部分是环绕的处理。请注意first 和next 之间的(循环)距离始终相同(Δ)。 newfirst 用于跟踪环绕点:每次first 到达newfirst,newfirst 前进Δ(通过将其分配给next 的值,始终超出first Δ)。
next 在第一个循环结束时第一次回绕。一旦发生这种情况,它就在容器末端的Δ以内;第二个循环继续交换。在此过程中,通过欧几里得算法有效地计算了N和Δ的GCD。