什么是“旋转”(循环移位)二维数组的好方法?

时间:2016-03-25 06:48:37

标签: arrays multidimensional-array language-agnostic swap

这实际上是一个概念上的(语言不可知的)问题,但为了便于解释,我将使用C ++。我更喜欢可以移植到其他语言的答案(没有指针算术或内存技巧)。

假设我们有:

  • arr,我们的矩形某种任意类型的T
  • 二维数组
  • void shift(int dx, int dy),执行“轮换”的功能
  • numRows,行数
  • numCols,列数

shift()移动数组,使所有行向下移动dx个位置,并且超出边界的行将环绕到开头。 (同样对于列和dy。)让我们说这是我们的数组最初的样子:

{{a1, a2, a3, a4},
 {b1, b2, b3, b4},
 {c1, c2, c3, c4},
 {d1, d2, d3, d4}};

在我们调用我们的函数后:shift(2,1)arr应该如下所示:

{{c4, c1, c2, c3},
 {d4, d1, d2, d3},
 {a4, a1, a2, a3},
 {b4, b1, b2, b3}};

在这种情况下,dx 2 ,所以所有内容都移动向下两个位置,dy 1 ,所以一切都转移到正确一个地方。

以下是解决此问题的方法:

void shift(int dx, int dy)
{
   T newArr[numRows][numCols];
   for(int r = 0; r < numRows; r++)
   {
      for(int c = 0; c < numCols; c++)
         newArr[(r + dx) % numRows][(c + dy) % numCols] = arr[r][c];
   }
   for(int r = 0; r < numRows; r++)
   {
      for(int c = 0; c < numCols; c++)
         arr[r][c] = newArr[r][c];
   }
}

我对这段代码不满意,因为它既不节省时间也不节省空间。我正在寻找一个更优雅的解决方案,通过更少的循环使用更少的内存来完成更多工作。

5 个答案:

答案 0 :(得分:2)

另一种可能性是根本不移动元素。我们的想法是拥有一个函数来转换所使用的索引,使原始数组出现旋转。

通过将原始数组包装在适当的数据类型中,可以获得轻微的性能损失。但无论何时你在旋转(或镜像,或反向,或其他),你都会在记忆和时间方面获得。

答案 1 :(得分:1)

我建议采用以下解决方案:

#include <stdio.h>
#include <memory>

int main() {
    const int nrows = 4, ncols = 5;
    const int dx = 2, dy = 1;
    int a[nrows ][ncols] = { {1, 2, 3, 4, 5}, 
        { 6, 7, 8, 9, 10 },
        { 11, 12, 13, 14, 15 },
        { 16, 17, 18, 19, 20 }
    };
    int tmp[nrows][ncols];
    for (int i = 0; i < nrows; i++)
        for (int j = 0; j < ncols; j++)
            tmp[(i + dx) % nrows][(j + dy) % ncols] = a[i][j];
    memcpy(a, tmp, sizeof(tmp));
    for (int i = 0; i < nrows; i++)
        for (int j = 0; j < ncols; j++)
            printf(j < ncols - 1 ? "%3d " : "%3d\n", a[i][j]);
}

Demo

使用内存复制的替代方法特定于c++。这是可能的,因为在c++的内存中存储多维数组的方法是连续的。行的最后一个元素后跟下一个元素。

#include <stdio.h>
#include <memory>

int main() {
    const int nrows = 4, ncols = 5;
    const int dx = 2, dy = 1;
    int a[nrows][ncols] = { {1, 2, 3, 4, 5},
        { 6, 7, 8, 9, 10 },
        { 11, 12, 13, 14, 15 },
        { 16, 17, 18, 19, 20 }
    };
    int tmp[nrows][ncols];
    memcpy(tmp + dx, a, (nrows - dx) * ncols * sizeof(int));
    memcpy(tmp, a + (nrows - dx), dx * ncols * sizeof(int));
    memcpy(a, tmp, sizeof(tmp));
    for (int i = 0; i < nrows; i++) {
        memcpy(tmp[i] + dy, a[i], (ncols - dy) * sizeof(int));
        memcpy(tmp[i], a[i] + ncols - dy, dy * sizeof(int));
    }

    memcpy(a, tmp, sizeof(tmp));
    for (int i = 0; i < nrows; i++)
        for (int j = 0; j < ncols; j++)
            printf(j < ncols - 1 ? "%3d " : "%3d\n", a[i][j]);
}

Demo

答案 2 :(得分:0)

假设你有一个较低级别的函数可以有效地复制Matrix的子块(处理低级别的内存排列,行主要列主要排序等...),操作可以在概念上分解为4个子块副本。使用matlab数组表示法和基于1的数组索引,就像:

tmp(1:nr , 1:nc) = a(end-nr+1:end , end-nc+1:end );
tmp(1:nr , nc+1:end) = a(end-nr+1:end , 1:end-nc );
tmp(nr+1:end , 1:nc) = a(1:end-nr , end-nc+1:end );
tmp(nr+1:end , nc+1:end) = a(1:end-nr , 1:end-nc );

请注意,让低级别函数执行低级别作业的假设非常常见(例如BLAS和LAPACK)。

答案 3 :(得分:0)

void scroll_left(int Array[8][8])
{
  for (u=0; u<8; u++)
  {
    for(int r=0; r<8; r++)
    {
      for (int c=0; c<8; c++)
      temp[r][c]=A[r][(c+u)%8];
    }
    delay_1(1000);
  }
}

答案 4 :(得分:0)

可以使用O(1)辅助空间以几种方式完成此操作。所有这些都是旋转一维数组的算法的改编。为了使描述/符号保持简单,我假设dxdy是肯定的,并假设如果它们是否定的,您可以弄清楚该怎么做。

用于dx,dy的简单,慢速解决方案

有一个非常简单的算法,可以在线性时间内将一维数组旋转1步:将最后一个元素存储在一个临时变量中,将其余元素向右移动一个,然后将该临时变量写入数组中的第一个位置。

可以直接将其水平或垂直旋转2D阵列1步,对角线则需要一点点额外的工作。因此,您可以应用dx水平旋转和dy垂直旋转;或者您可以先进行min(dx, dy)个对角旋转,然后进行max(dx, dy) - min(dx, dy)个正交旋转。

这需要O(numRows * numCols * (dx + dy))时间,因此不适用于大班次。但是,如果您只移动一个空格,那不只是很好;还可以。最佳。

循环分解

anumRowsdx的{​​{3}},而bnumRowsdy的GCD 。然后,所需的旋转将在a * b个独立的长度为numCols * numRows / (a * b)的循环中对数组进行置换。可以通过GCD以对数时间计算GCD。

对于i = 0 to a - 1j = 0 to b - 1,使用临时变量方法将从arr[j][i]开始的循环旋转一步。此循环包含元素arr[j][i],然后是arr[j + dy][i + dx],然后是arr[j + 2*dy][i + 2*dx],依此类推,其中索引分别以numRowsnumCols为模。 / p>

时间复杂度为O(numRows * numCols),因为每个数组组件只能读取一次,并且只能写入一次。但是,由于以下两个原因,该算法的性能可能会降低:进行大量的模运算(当数组维数不是2的幂时,这是错误的);并且连续的读/写操作不在相邻的数组位置(当整个数组位置时,是不好的)数组不适用于缓存)。

反向子数组

长度为n的一维数组可以通过以下算法向右旋转k步:

  • 反转整个数组,
  • 反转子数组0 .. k-1
  • 反转子数组k .. n-1

我们可以通过将行旋转dx,然后将列旋转dy来执行2D旋转。

此算法读取和写入每个数组组件四次,因此其时间复杂度也为O(numRows * numCols),但与使用循环分解的算法相比,它在数组上的传递次数更多。但是,它不进行模运算,并且可能有较少的高速缓存未命中。实施起来也更简单。