左旋转数组 C++

Posted

技术标签:

【中文标题】左旋转数组 C++【英文标题】:left rotate array in place C++ 【发布时间】:2018-07-27 13:20:18 【问题描述】:

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

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

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

注意:我知道这可以很容易地通过创建一个带有左旋转值的新数组来完成。我正在尝试在输入数组本身中执行此操作。

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

【问题讨论】:

看看std::rotate 还有std::swap SO 是不是代码编写服务。请先说明您尝试过什么。 如果您不能使用std::rotate,请参阅std::move 和std::copy。 @AlgirdasPreidžius OP 特别要求代码。 【参考方案1】:

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

auto b = std::begin(A);
std::rotate( b, b + 2, std::end(A) );

【讨论】:

【参考方案2】:

矢量旋转似乎是神秘算法的一个特别肥沃的土壤。其中大部分都可以在野外找到,但有变化;所有高效的都需要花点心思来理解它们的功能。

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

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;
  

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

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

【讨论】:

这是最好的答案,因为它不会卖光“使用 std::rotate”,从而允许 OP 学习。【参考方案3】:

我尝试了 O(n) 中的解决方案。在这里,我制作了另一个相同大小的向量。假设你想左旋转 2,所以这里 d = 2。首先将元素从位置 2 复制到新数组中的位置 0 直到最后。然后将元素从第一个数组的开头复制到第二个数组的结尾,即从 n-d 位置。

int i = 0;
int r = d;
vector<int> b(n);
for(i=0; i< n-d; i++)

    b[i] = a[r];
    r++;

r = 0;
for(i=n-d; i<n; i++)

    b[i] = a[r];
    r++;

【讨论】:

感谢建筑师。然而,我正在寻找这是否可以在不分配额外空间的情况下完成。基本上在输入数组本身内。【参考方案4】:

这种技术称为旋转

让我们假设数组中的 0 元素是左边缘:

  0   1   2   3   4   5    <-- array indices  
+---+---+---+---+---+---+  
| 3 | 1 | 4 | 1 | 5 | 9 |  
+---+---+---+---+---+---+  

操作是A[0] = A[1]A[i] = A[i + 1]

第一个槽和最后一个槽是两个例外。

首先,将左边的元素复制到一个临时变量中:

temp      0   1   2   3   4   5    <-- array indices  
+---+    +---+---+---+---+---+---+  
| 3 |<-- | 3 | 1 | 4 | 1 | 5 | 9 |  
+---+    +---+---+---+---+---+---+  

接下来,将剩下的所有元素都复制一个:

temp      0   1   2   3   4   5    <-- array indices  
+---+    +---+---+---+---+---+---+  
| 3 |<-- | 1 | 4 | 1 | 5 | 9 | 9 |  
+---+    +---+---+---+---+---+---+  

最后,将临时变量复制到最后一个槽:

  0   1   2   3   4   5       temp  
+---+---+---+---+---+---+     +---+
| 1 | 4 | 1 | 5 | 9 | 3 | <-- | 3 |
+---+---+---+---+---+---+     +---+

向右旋转将在另一个方向上起作用:

A[i - 1] = A[i]

编辑 1 要旋转多个位置,请执行两次旋转,或修改算法以跳过一个元素。

例如,向左旋转 2:A[i] = A[i + 2]

临时存储留给读者作为练习。 :-)

【讨论】:

感谢托马斯的帖子。对不起,如果我没有说得更清楚。我试图一次性做到这一点。我同意这也是一个很好的解决方案,但想知道这是否可以在数组的 1 遍中完成。 是的,使用A[i] = A[i+2]。像我一样画出来,然后弄清楚你需要做什么。 谢谢托马斯!我还有一些顾虑。但是,如果这是我正在寻找的,让我再尝试一下您建议的方法。 @thomas:您的旋转两次修改并不像看起来那么简单。可以随走随走,但需要处理增量和长度有公因数的情况。 @rici 原来是退出情况(长度是 delta 的整数倍),这并不奇怪,因为 1 的转换总是立即生效。【参考方案5】:

毕竟是可以做到的。

@ThomasMatthews 的建议可以作为一个起点:您可以简单地开始交换 array[i]array[i+rotate] 的元素,直到 i=0...last-rotate。问题是最后一个rotate元素的顺序会很忙,除非数组的长度是rotate的整数倍,在这种情况下它似乎很好,虽然我不知道为什么(这只是第一次出现)。 使用这个 sn-p,您可以交互地检查这些元素的外观,您可能会注意到其余元素需要length of array % rotate(即末尾的单个数字)量才能正确移动.我当然不知道为什么。

function test()
  var count=document.getElementById("count").valueAsNumber;
  var rotate=document.getElementById("rotate").valueAsNumber;
  var arr=[...Array(count).keys()];
  for(var i=0;i<count-rotate;i++)
    var t=arr[i];
    arr[i]=arr[i+rotate];
    arr[i+rotate]=t;    
  
  document.getElementById("trivial").innerhtml=arr.slice(0,count-rotate).join();
  document.getElementById("tail").innerHTML=arr.slice(count-rotate).join();
  document.getElementById("remainder").innerHTML=count%rotate;

test();
<input type="number" id="count" oninput="test()" min="1" value="41"><br>
<input type="number" id="rotate" oninput="test()" min="1" value="12"><br>
<div id="trivial"></div>
<div id="tail"></div>
<div id="remainder"></div>

然后我只是添加了一些递归,还利用了右旋转可以作为左旋转的事实(移位量必须从元素的数量中减去),所以我不必单独编写方法,因为我很懒。块显示在单独的行中(部分结果正常显示,输入块在括号中),因此更容易理解。这里我使用 array.slice() 创建一个新数组,但它可以通过传递起始索引和长度来代替,因此它可以作为 C/C++ 中的就地操作。

function rot(arr,rotate)
  var retbase="("+rotate+":"+arr.join()+")<br>";    // input in parentheses
  for(var i=0;i<arr.length-rotate;i++)
    var t=arr[i];
    arr[i]=arr[i+rotate];
    arr[i+rotate]=t;
  
  var rightrot=arr.length % rotate;                 // amount of right-rotation missing
  if(rightrot===0)                                  // done
    return retbase+arr.join();
  else                                             // needs fixing
    retbase+=arr.slice(0,arr.length-rotate)+"<br>"; // partial result
    arr=arr.slice(arr.length-rotate);
    return retbase
      +rot(arr,arr.length-rightrot);                // flipping right-rotation left-rotation
  

function test()
  var count=document.getElementById("count").valueAsNumber;
  var rotate=document.getElementById("rotate").valueAsNumber;
  var arr=[...Array(count).keys()];
  document.getElementById("result").innerHTML=rot(arr,rotate);

test();
<input type="number" id="count" oninput="test()" min="1" value="41"><br>
<input type="number" id="rotate" oninput="test()" min="1" value="12"><br>
<div id="result"></div>

递归的深度/模式与 GCD 计算有一些相似之处,所以如果有人不得不谈论复杂性,我会开始寻找那个方向。

【讨论】:

您应该查看std::rotate 的示例实现(链接在其他答案中),这与您的解决方案基本相同(但迭代而不是递归)。它真的非常漂亮,即使它的工作原理并不完全清楚。 @rici 是的,我倾向于忘记 cppreference.com 经常有示例实现,所以我只是认为答案是“使用这个”,而不是“如何”。也许我应该开始删除它,因为它是一个仅限链接的答案:evilgrin: 如果问题是“我如何在 C++ 中做到这一点”,那么“使用标准库”在我看来是一个完全合理的答案,而实际的实现是有趣但不必要的信息。如果问题是“什么是矢量旋转的好/常见/有趣的算法”,那么标准库实现是几种可能性之一。我试图在扩展答案中列出并解释它们。

以上是关于左旋转数组 C++的主要内容,如果未能解决你的问题,请参考以下文章

左旋转数组 C++

排序旋转整数数组,搜索算法[重复]

在 JavaScript 中旋转数组中的元素

左旋转右旋转双旋

UIButtons 在向右旋转时起作用,但在向左旋转时不起作用。嗯?

我制作了一个 android 相机应用程序,但我想将它向左旋转 90 度和向右旋转 90 度,我该怎么做?