【问题标题】:What is a good way to "rotate" (circular shift) a two dimensional array?什么是“旋转”(循环移位)二维数组的好方法?
【发布时间】:2016-03-25 06:48:37
【问题描述】:

这确实是一个概念性(与语言无关)的问题,但为了解释起见,我将使用 C++。我更喜欢可以移植到其他语言的答案(没有指针算术或内存技巧)。


假设我们有:

  • arr,我们的 rectangular 一些任意类型的二维数组 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}};

在本例中,dx2,所以所有内容都向下移动了两个位置,dy1,所以所有内容也移到了右边一个地方。

这是我解决这个问题的方法:

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

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

【问题讨论】:

  • 我不明白你为什么需要 4 个 for 循环来解决这个问题。你能展示你的代码吗?
  • @IvanGritsenko 抱歉,如果这太晚了,但我已经用更多细节更新了我的问题。

标签: arrays multidimensional-array language-agnostic swap


【解决方案1】:

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

将原始数组包装成适当的数据类型会稍微降低性能。但是,每当您旋转(或镜像,或反向,或其他)时,您都会获得内存和时间。

【讨论】:

  • shift() 的意义在于永久地改变数据结构,因此将来任何arr 的寻址都将自动检索新放置的元素。实现您的想法的一种方法是将T arr[n][m] 更改为T* arr[n][m],并简单地更改指针指向的内容。在像 Java 这样的仅堆分配语言中,默认情况下会发生这种情况。虽然这确实大大降低了空间复杂度,但它仍然是 O(n*m),因为 n*m 指针存储在临时数组中。
【解决方案2】:

我建议以下解决方案:

#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.

【讨论】:

  • 第一种解决方案更好,因为它更便携,但是如果数组不是由指针或原语组成,空间复杂度非常高。
【解决方案3】:

假设您有一个较低级别的函数可以有效地复制 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)。

【讨论】:

    【解决方案4】:
    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);
      }
    }
    

    【讨论】:

      【解决方案5】:

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

      小 dx、dy 的简单、缓慢的解决方案

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

      这可以直接调整为将 2D 阵列水平或垂直旋转 1 步,并且需要一些额外的工作,对角线。所以你可以应用dx水平旋转和dy垂直旋转;或者你可以做min(dx, dy)对角旋转,然后max(dx, dy) - min(dx, dy)正交旋转。

      这需要O(numRows * numCols * (dx + dy)) 时间,因此不适合大班次。但是,如果您只移动一个空格,那就不合适了。这是最佳选择。

      循环分解

      anumRowsdxGCD,并且bnumRowsdy 的GCD。然后所需的旋转将在a * b 长度为numCols * numRows / (a * b) 的独立循环中置换数组。 GCD 可以通过Euclid's algorithm 以对数时间计算。

      对于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 为模计算。

      时间复杂度为O(numRows * numCols),因为每个数组组件只读取一次,只写入一次。然而,该算法的性能会因两个原因而降低:它执行大量模运算(当数组维度不是 2 的幂时不好),并且连续的读/写不在相邻的数组位置(当整个数组不适合缓存)。

      反向子数组

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

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

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

      这个算法对每个数组组件进行四次读写,所以它的时间复杂度也是O(numRows * numCols),但是它比使用循环分解的算法对数组的遍历次数更多。但是,它不进行模运算,并且可能有更少的缓存未命中。实现起来也更简单。

      【讨论】:

        猜你喜欢
        • 2020-06-04
        • 1970-01-01
        • 2016-06-25
        • 1970-01-01
        • 1970-01-01
        • 2019-10-27
        • 2021-07-16
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多