左旋转数组 C++

tec*_*amp -2 c++ arrays algorithm compare-and-swap

这就是我想要做的:数组 A[] = {1,2,3,4,5} 左旋转 2:A:{3,4,5,1,2}

我们是否有一个简单而好的解决方案来做到这一点?我希望使用这个左旋转值更新数组 A 本身 - 没有额外的空间。

我尝试了各种方法,但对于各种测试用例,逻辑似乎不同,并且很难找到适合这个看似简单的任务的算法。

注意:我知道只需创建一个具有左旋转值的新数组即可轻松完成此操作。我正在尝试在输入数组本身中执行此操作。

请建议。简单的伪代码应该可以。

Sla*_*ica 5

std::rotate()将完全满足您的需求:

auto b = std::begin(A);
std::rotate( b, b + 2, std::end(A) );
Run Code Online (Sandbox Code Playgroud)


ric*_*ici 5

矢量旋转似乎是神秘算法的沃土。其中大部分都可以在野外找到,但有变化;所有高效的都需要一些思考来了解它们的功能。

如果你只向左旋转一个元素,你可以非常有效地做到这一点:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以使用它delta通过执行?次数(更准确地说,? % N)来按位置旋转,但这需要时间 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

交换通常比移动要多一些工作,但有可能有一个高效的特定于容器的交换实现。无论如何,这个版本将有助于理解以后的实现。

一个众所周知且经常被引用的 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);
}
Run Code Online (Sandbox Code Playgroud)

在我看来,这真的很优雅。您可以通过在纸上进行尝试来了解它是如何工作的:

 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
Run Code Online (Sandbox Code Playgroud)

这是众所周知的“颠倒句子中单词的顺序”解决方案的一个特例,它包括首先颠倒每个单词的字母,然后颠倒整个字符串。

但是,它存在一些问题。首先,当一次移动就足够时,它(几乎)每个元素移动两次。其次,std::reverse需要一个双向迭代器。这没有任何问题,但算法与任何前向迭代器一起工作会更好。

另一个简单的解决方案是注意,如果您使用第一个算法,但使用增量 Δ 而不是增量,并在迭代器到达末尾时将迭代器包装回开头,那么如果 Δ 和 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
Run Code Online (Sandbox Code Playgroud)

使用随机访问迭代器更容易(这是一个缺陷);这是一个示例实现。(该变量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);
  }
}
Run Code Online (Sandbox Code Playgroud)

如前所述,这依赖于具有随机访问迭代器的容器。

请注意,我们可以通过使用交换来消除对临时存储的需求,就像上面第一个算法的替代版本一样。如果我们这样做,那么我们就可以并行执行所有外部循环,因为它们不会相互干扰。因此,我们可以一次只向前移动一个元素,将每个元素与其 Δ-下一个对等元素交换。

这导致作为提供旅游德力样品实施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;
  }
}
Run Code Online (Sandbox Code Playgroud)

上面唯一棘手的部分是环绕的处理。请注意,first和之间的(循环)距离next始终相同(Δ)。newfirst用于跟踪环绕点:每次first到达newfirstnewfirst前进 Δ(通过将其分配给 的值next,始终为 Δ 超出first)。

next在第一个循环结束时第一次环绕。一旦发生这种情况,它就在容器末端的Δ内;第二个循环继续交换。在这个过程中,它使用欧几里德算法有效地计算了 N 和 Δ 的 GCD。