向左旋转数组到位C ++

时间:2018-07-27 13:20:18

标签: c++ arrays algorithm compare-and-swap

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

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

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

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

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

5 个答案:

答案 0 :(得分:4)

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

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

答案 1 :(得分:3)

向量旋转似乎是神秘算法的特别沃土。其中大多数都可以在野外发现,且种类繁多。所有有效率的人都需要一些思想来理解其功能。

如果仅向左旋转一个元素,则可以非常高效地完成此操作:

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);
  }
}

您可以通过执行delta次(更准确地说,Δ)来使用它来旋转Δ % N个位置,但这需要时间O(NΔ),即O(NΔ) 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需要双向迭代器。没什么错,但是对于任何正向迭代器来说,算法都更好。

另一种简单的解决方案是,如果使用第一种算法,但使用Δ而不是增量Δ,并在迭代器到达末尾时将迭代器回绕到头,则如果Δ和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对等交换。

这导致了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到达newfirst时,newfirst前进Δ(将其分配给next的值,它总是比first的Δ)。

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

答案 2 :(得分:1)

该技术称为旋转

让我们假设数组中的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]

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

答案 3 :(得分:1)

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

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)

毕竟可以做到。

@ThomasMatthews建议的内容可以作为起点:您可以简单地开始交换array[i]array[i+rotate]的元素,最多可以交换i=0...last-rotate。问题在于,除非数组的长度是rotate的整数倍,否则最后rotate个元素的顺序将很杂乱,尽管这种情况看起来很好我不知道为什么(这只是第一次出现)。
使用此代码段,您可以交互式检查这些元素的外观,并且您可能会注意到,其余元素需要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计算有些相似,因此,如果有人不得不说一些复杂性,我会开始寻找这个方向。