【问题标题】:How do you rotate a single layer of a matrix by n places?如何将矩阵的单层旋转 n 个位置?
【发布时间】:2019-06-18 21:37:19
【问题描述】:

我已阅读有关旋转矩阵的堆栈溢出的所有问题 除了在单层或整个矩阵的任一方向上转置或旋转 90 或 180 度之外,它们都没有解决任何问题。

我已经回答了这个问题,但我正在寻找快速和/或“就地”的方法,最好是,但任何方法 除了我给的那个之外,如果只是为了 教育目的。我的示例是在 Perl 5 中,但只要您的意图明确,大多数任何语言都应该是可以接受的。

我的例子中使用的矩阵如下

# 2D array @arr has 3 layers.
# @arr is 
#         0 1 2 3 4 5
#         a b c d e f
#         5 4 3 2 1 0
#         9 8 7 6 5 4
#         f e d c b a
#         4 5 6 7 8 9

Goal->顺时针旋转中间层2个位置得到以下...

请注意中间层的左上角“b c”如何向右移动了 2 个位置,而中间左侧的“8 4”现在是“b c”所在的位置。

# Rotated @arr is 
#         0 1 2 3 4 5
#         a 8 4 b c f
#         5 e 3 2 d 0
#         9 d 7 6 e 4 
#         f c b 5 1 a
#         4 5 6 7 8 9

目前我一直在按照这个思路做一些事情,诚然,这非常慢。尤其是在许多大型阵列上。

#!/usr/bin/env perl
use strict; use warnings;

# Coordinates are $arr[$row][$col]
my @arr;
$arr[0][0]='0'; $arr[0][1]='1'; $arr[0][2]='2'; $arr[0][3]='3'; $arr[0][4]='4'; $arr[0][5]='5';
$arr[1][0]='a'; $arr[1][1]='b'; $arr[1][2]='c'; $arr[1][3]='d'; $arr[1][4]='e'; $arr[1][5]='f';
$arr[2][0]='5'; $arr[2][1]='4'; $arr[2][2]='3'; $arr[2][3]='2'; $arr[2][4]='1'; $arr[2][5]='0';
$arr[3][0]='9'; $arr[3][1]='8'; $arr[3][2]='7'; $arr[3][3]='6'; $arr[3][4]='5'; $arr[3][5]='4';
$arr[4][0]='f'; $arr[4][1]='e'; $arr[4][2]='d'; $arr[4][3]='c'; $arr[4][4]='b'; $arr[4][5]='a';
$arr[5][0]='4'; $arr[5][1]='5'; $arr[5][2]='6'; $arr[5][3]='7'; $arr[5][4]='8'; $arr[5][5]='9';

# Print matrix
print_matrix(@arr);

# Extract layer 2
my $target_layer=2;
my $layer_two=extract_layer($target_layer,@arr);

# From the top left corner of the layer, it is as follows
# bcde15bcde84
print "\n$layer_two\n";

# Rotate layer 2 clockwise 2 places
$layer_two=rotate_layer_cl($layer_two,2);

# 84bcde15bcde
print "$layer_two\n\n";

# Replace layer 2 in the same way
@arr=replace_layer($target_layer,$layer_two,@arr);

# Print again
print_matrix(@arr);

### Sub functions ###
# Extract layer by walking around it's coordinates like so
# [1,1]-[1,4]  Top(left->right)
# [2,4]-[4,4]  Right(top->bottom)
# [4,3]-[4,1]  Bottom(right->left)
# [3,1]-[2,1]  Left(bottom->top)
sub extract_layer {
    my ($layer_cnt,@matrix)=@_;
    my $width=scalar(@matrix);
    my $layer_width=$width-$layer_cnt;
    # layer_cnt=2
    # width=6
    # layer_width=4
    my $layer;
    for my $col ( $layer_cnt-1..$layer_width ) {
        $layer.=$matrix[$layer_cnt-1][$col];
    }
    for my $row ( $layer_cnt..$layer_width ) {
        $layer.=$matrix[$row][$layer_width];
    }
    my $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt-1 ) {
        $layer.=$matrix[$layer_width][$cnt];
        $cnt--;
    }
    $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt ) {
        $layer.=$matrix[$cnt][$layer_cnt-1];
        $cnt--;
    }
    return $layer;
}

# Shift input to the right by $n places, wrapping around.
sub rotate_layer_cl {
    my $n=$_[1];
    my $buf=substr($_[0],length($_[0])-$n,$n);
    return $buf.substr($_[0],0,length($_[0])-$n);
}

# Replace each char from the rotated layer.
sub replace_layer {
    my ($layer_cnt,$layer,@matrix)=@_;
    my $width=scalar(@matrix);
    my $layer_width=$width-$layer_cnt;
    # layer_cnt=2
    # width=6
    # layer_width=4
    my $slot=0;
    for my $col ( $layer_cnt-1..$layer_width ) {
        $matrix[$layer_cnt-1][$col]=substr($layer,$slot,1);
        $slot++;
    }
    for my $row ( $layer_cnt..$layer_width ) {
        $matrix[$row][$layer_width]=substr($layer,$slot,1);
        $slot++;
    }
    my $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt-1 ) {
        $matrix[$layer_width][$cnt]=substr($layer,$slot,1);
        $slot++;
        $cnt--;
    }
    $cnt=$layer_width-1;
    while ( $cnt >= $layer_cnt ) {
        $matrix[$cnt][$layer_cnt-1]=substr($layer,$slot,1);
        $slot++;
        $cnt--;
    }
    return @matrix;
}

# Prints given matrix
sub print_matrix {
    foreach my $row (@_) {
        my $cnt=0;
        foreach my $char (@$row) {
            print $char; $cnt++;
            if ( $cnt == scalar(@_) ) {
                print "\n";
            } else {
                print " ";
            }
        }
    }
}

以上代码输出如下,

0 1 2 3 4 5
a b c d e f
5 4 3 2 1 0
9 8 7 6 5 4
f e d c b a
4 5 6 7 8 9

bcde15bcde84
84bcde15bcde

0 1 2 3 4 5
a 8 4 b c f
5 e 3 2 d 0
9 d 7 6 e 4
f c b 5 1 a
4 5 6 7 8 9

其中显示了旋转中间层之前的数组,中间层为字符串,中间层移位为字符串,中间层向右旋转 2 位后的最终结果数组。

-----编辑-----

到目前为止,我发现最快的方法是不实际使用完整的二维数组,而是将块放入常规数组中,例如...

my @hex_ary=('1234', '5678', '90ab', 'cdef');

然后创建图层字符串并按照我原始示例显示的方式移动它。

但是,因为它们是字符串,我只需要在两侧“走”上和下。顶部和底部被简单地引用为...

my $top=substr($hex_ary[0],0,3);

my $bot=reverse(substr($hex_ary[3],0,3));

对于这个小 4x4 数组中的外层,而边是迭代的

my $right_side_char=substr($hex_ary[$top_row-$i],-1);
my $left_side_char=substr($hex_ary[$bot_row+$i],0,1);

这样做可以提高性能大约。 100% 来自 2D 方法,因为从数组中取出的切片数是 half+2。

也许对 C 有更好理解的人可以创造出更高效的东西。我觉得 Perl 有时只能做这么多。

【问题讨论】:

  • 一个想法:你能不能把旋转表达为一个函数f(x,y),即输入(x,y)是中间层每个成员的行/列索引,输出是旋转位置@987654331 @?。那么方法是(1)制作@arr的副本,(2)循环中间层成员并根据f()的输出将每个成员放在副本中。
  • 我猜你还需要一个函数members(ring_identifier),它返回一个数组,其中包含环ring_identifier的所有成员的[x,y]条目。这将是for my($x, $y) (@{ $members }) { ... } 循环的来源。
  • 我在想这样的事情,但不完全确定如何实现它。非常感谢。
  • 很高兴我们能提供帮助,我非常喜欢在这方面工作。请记住对答案进行投票并接受您最喜欢的答案。

标签: arrays perl matrix multidimensional-array 2d


【解决方案1】:

简而言之方法:

  • 复制数组
  • 计算环的成员列表(行、列)
  • 遍历列表索引

    • 从原始数组中的成员[索引]读取
    • 写入副本数组中的成员[(index + shift) % ring length]
  • 右移 N 位:shift == N

  • 左移 N 位:shift == -N
#!/usr/bin/perl
use strict;
use warnings;

sub dump_array($$) {
    my($label, $array) = @_;
    print "${label}:\n";
    foreach my $row (@{ $array }) {
        print join(" ", @{ $row }), "\n";
    }
    print "\n";
}

sub copy_array($) {
    my($array) = @_;
    my @copy;
    foreach my $row (@{ $array }) {
        push(@copy, [ @{ $row } ]);
    }
    return(\@copy);
}

sub ring_index_to_row_col($$$$$) {
    my($N, $ring, $total, $length, $index) = @_;
    my($row, $col);

    if ($index < $length) {
        # top row, left to right
        $row = 0;
        $col = $index;
    } elsif ($index < 2 * $length - 2) {
        # top to bottom, right row
        $row = $index - $length + 1;
        $col = $N - 1 - 2 * $ring;
    } elsif ($index < 3 * $length - 2) {
        # bottom row, right to left
        $row = $N - 1 - 2 * $ring;
        #$col = $length - 1 - ($index - 2 * $length + 2);
        $col = -$index + 3 * $length - 3;
    } else {
        # bottom to top, left row
        #$row = $total - 1 - $index + 1;
        $row = $total - $index;
        $col = 0;
    }

    #print "${index}\t of ${total}\t-> ${row}, ${col}\n";

    # shift $length x length array to offset ($ring, $ring)
    return([$row + $ring, $col + $ring]);
}

sub ring_members($$) {
    my($N, $ring) = @_;
    my @list;

    # @TODO: odd N?
    #
    # Examples for N == 6
    #   0   -> 2*6 + 2*4 = 20
    #   1   -> 2*4 + 2*2 = 12
    #   2   -> 2*2 + 2*0 =  4
    #
    # Examples for N == 5
    #   0   -> 2*5 + 2*3
    #   1   -> 2*3 + 2*1
    #   2   -> 1
    #
    # Examples for N == 4
    #   0   -> 2*4 + 2*2 = 12
    #   1   -> 2*2 + 2*0 =  4
    my $length  = $N - 2 * $ring;
    my $members = 4 *  $N - 8 * $ring - 4;

    foreach my $index (0..$members-1) {
        push(@list, ring_index_to_row_col($N, $ring, $members, $length, $index));
    }

    return(\@list);
}

sub rotate_array_ring(\@$$) {
    my($source, $ring, $shift) = @_;

    # Sanity check. @TODO is the check correct for odd N?
    my $N              = @{ $source };
    die "ERROR: invalid ring '${ring}' for 2D array of size $N!\n"
        unless $ring < ($N / 2);

    my $copy   = copy_array($source);

    my $list   = ring_members($N, $ring);
    my $length = @{ $list };

    foreach my $index (0..$length-1) {
        my($row,   $col)   = @{ $list->[ $index                     ] };
        my($s_row, $s_col) = @{ $list->[($index + $shift) % $length ] };

        $copy->[$s_row]->[$s_col] = $source->[$row]->[$col];
    }

    return($copy);
}

my @source;
while (<DATA>) {
    chomp;
    push(@source, [ split(/\s+/, $_) ]);
}
dump_array('SOURCE', \@source);

dump_array('SHIFT 1 RING 0', rotate_array_ring(@source, 0, 1));
dump_array('SHIFT 1 RING 1', rotate_array_ring(@source, 1, 1));
dump_array('SHIFT 1 RING 2', rotate_array_ring(@source, 2, 1));
dump_array('SHIFT 2 RING 0', rotate_array_ring(@source, 0, 2));
dump_array('SHIFT 2 RING 1', rotate_array_ring(@source, 1, 2));
dump_array('SHIFT 2 RING 2', rotate_array_ring(@source, 2, 2));

exit 0;

__DATA__
0 1 2 3 4 5
a b c d e f
5 4 3 2 1 0
9 8 7 6 5 4
f e d c b a
4 5 6 7 8 9

示例输出:

$ perl dummy.pl
SOURCE:
0 1 2 3 4 5
a b c d e f
5 4 3 2 1 0
9 8 7 6 5 4
f e d c b a
4 5 6 7 8 9

SHIFT 1 RING 0:
a 0 1 2 3 4
5 b c d e 5
9 4 3 2 1 f
f 8 7 6 5 0
4 e d c b 4
5 6 7 8 9 a

SHIFT 1 RING 1:
0 1 2 3 4 5
a 4 b c d f
5 8 3 2 e 0
9 e 7 6 1 4
f d c b 5 a
4 5 6 7 8 9

SHIFT 1 RING 2:
0 1 2 3 4 5
a b c d e f
5 4 7 3 1 0
9 8 6 2 5 4
f e d c b a
4 5 6 7 8 9

SHIFT 2 RING 0:
5 a 0 1 2 3
9 b c d e 4
f 4 3 2 1 5
4 8 7 6 5 f
5 e d c b 0
6 7 8 9 a 4

SHIFT 2 RING 1:
0 1 2 3 4 5
a 8 4 b c f
5 e 3 2 d 0
9 d 7 6 e 4
f c b 5 1 a
4 5 6 7 8 9

SHIFT 2 RING 2:
0 1 2 3 4 5
a b c d e f
5 4 6 7 1 0
9 8 2 3 5 4
f e d c b a
4 5 6 7 8 9

优化:不要复制整个数组,但是

  1. 使用$list 将环成员复制到列表中,然后
  2. 循环遍历该列表以将它们放置到移位后的位置返回到数组中。

即两个循环而不是一个:

my $list   = ring_members($N, $ring);
my $length = @{ $list };

# in-place approach
my @members;
foreach my $index (0..$length-1) {
    my($row, $col) = @{ $list->[ $index ] };
    push(@members, $source->[$row]->[$col]);
}
foreach my $index (0..$length-1) {
    my($row, $col) = @{ $list->[ ($index + $shift) % $length ] };
    $source->[$row]->[$col] = $members[$index];
}

return($source);

优化 2:如果您需要将一系列操作应用于一组相同大小的 2D 数组,那么您可以预先生成它们并反复处理它们使用函数:

my $N     = 6; # all arrays are of same size
my @rings = (
    ring_members($N, 0),
    ring_members($N, 1),
    ring_members($N, 2),
);
my @operations = (
    { ring => $rings[0], shift =>  1 },
    { ring => $rings[1], shift => -1 },
    { ring => $rings[2], shift =>  2 },
    # ...
)

# apply one operation in-place on array
sub apply_operation($$) {
    my($array, $operation) = @_;
    my $list               = $operation->{ring};
    my $shift              = $operation->{shift};
    my $length             = @{ $list };

    my @members;
    foreach my $index (0..$length-1) {
        my($row, $col) = @{ $list->[ $index ] };
        push(@members, $array->[$row]->[$col]);
    }
    foreach my $index (0..$length-1) {
        my($row, $col) = @{ $list->[ ($index + $shift) % $length ] };
        $array->[$row]->[$col] = $members[$index];
    }
}

# apply operation sequence in-place on array
sub apply_operations($$) {
    my($array, $operations) = @_;

    foreach my $operation (@{ $operations }) {
        apply_operation($array, $operation);    
    }
}

apply_operations(\@array1, \@operations);
apply_operations(\@array2, \@operations);
apply_operations(\@array3, \@operations);
...

优化 3:不要在原地方法中复制整个组,而只复制那些将被轮换覆盖的成员。 IE。使用 3 个循环:

  1. 将第一个或最后一个$shift 成员(取决于旋转方向)复制到@cache 数组。
  2. 为其他 $length - abs($shift) 成员直接复制。
  3. @cache 成员复制到他们轮换的位置。

优化 4: 不要使用二维数组 (AoA),而是使用带有 $N * $N 条目的一维数组。即

  • 而不是 ring_index_to_row_col() 返回 [$row, $col] 您将有 ring_index_to_array_index() 返回 $row * $N + $col
  • 数组访问-&gt;[$row]-&gt;[$col] 将替换为-&gt;[$array_index]

【讨论】:

  • 感谢您的宝贵时间。这绝对是最复杂的答案和最彻底的回应。在 1KB 数组测试中,它的速度只有我示例的 1/3。
  • 如果您在大型数组上使用就地方法,则在第一个循环$#members = $length - 1 之前将push(@members, ... 替换为(a)可能会提高性能,即预分配数组, (b) 在第一个循环中$members[$index] = ...
  • 添加了第三和第四个优化思路。
【解决方案2】:

我的方法的想法是使用包含n 元素(其中@ 987654324@ 是您要旋转层的位置数),并遍历层的每个元素,每次,push 将其值添加到队列中,并将其替换为队列的第一个元素。

这就是子 rotate_matrix 所做的。
- $i$j 表示图层当前元素的索引。它们的值介于$target_layer$max_i/$max_j 之间;子next_ij 负责计算层的下一个元素的索引。
- 有趣的事情发生在while(1) 循环内(实际上是一个do...while;但do while 循环在Perl 中是a bit special):当前元素的值被推送到队列中,如果它们是队列中超过$n 个元素(意味着我们已经推送了$n 元素,表示我们要旋转$n),我们将其值替换为队列的第一个元素。 continue 块负责递增 $i$j,并在我们回到层的第一个元素时停止循环。
- 一旦这个循环完成,层的第一个n 元素还没有更新(因为这个next unless @queue &gt; $n),所以我们需要注意这一点:这是while (@queue) 循环)。

就复杂性而言,无需复制数组。内存使用量为O(n)(队列大小)。在时间上,你在层的每个元素上循环一次,每次,你从队列中得到pushshift(使用更好的数据结构,如链表,这将是O(1),但有一个Perl 数组,它可能在 O(n) 的行中;但可能摊销到 O(1)),并计算下一个索引 (O(1))。所以你最终会得到一个时间复杂度,你最终会得到O(size of the layer to rotate)

一些注意事项:如果$n 大于层中元素的数量,这将不起作用(但可以通过简单的模数来修复)。如果您要求矩阵中不存在的层(例如 2x2 矩阵的第 4 层),就会发生奇怪的事情(但再一次,它很容易修复)。

#!/usr/bin/perl

use strict;
use warnings;
use v5.14;


my @arr = ( [qw(0 1 2 3 4 5)],
            [qw(a b c d e f)],
            [qw(5 4 3 2 1 0)],
            [qw(9 8 7 6 5 4)],
            [qw(f e d c b a)],
            [qw(4 5 6 7 8 9)] );

print_matrix(@arr);

rotate_matrix(\@arr, 2, 2);
say "";

print_matrix(@arr);


sub rotate_matrix {
    my ($matrix, $target_layer, $n) = @_;
    --$target_layer; # I prefer a 0-indexed value
    # TODO: check that $target_layer < @$matrix/2
    # TODO: do something like $n = $n % (num of elements in layer)

    my @queue;
    my ($i, $j) = ($target_layer, $target_layer);
    my ($max_i, $max_j) = (@{$matrix->[0]}-$target_layer-1, @$matrix-$target_layer-1);

    while (1) { # Actually a do...while loop (see 'continue' block)
        push @queue, $matrix->[$i][$j];
        next unless @queue > $n; # Waiting to reach n-th element
        $matrix->[$i][$j] = shift @queue;
    } continue {
        ($i, $j) = next_ij($target_layer,$max_i,$max_j,$i,$j);
        # Stopping if we are back at the begining
        last if $i == $target_layer && $j == $target_layer;
    }

    # Emptying queue
    while (@queue) {
        $matrix->[$i][$j] = shift @queue;
        ($i, $j) = next_ij($target_layer,$max_i,$max_j,$i,$j);
    }


}

# Computes the index of the next element of the layer
sub next_ij {
    my ($target_layer, $max_i, $max_j, $i, $j) = @_;
    if ($j == $max_j) { # Last column
        if ($i == $max_i) { # Last row (bottom right -> going left)
            return ($i, $j-1);
        } else { # Not last row (somewhere on the right col -> going down)
            return ($i+1, $j);
        }
    } elsif ($j == $target_layer) { # First column
        if ($i == $target_layer) { # First row (top left -> going right)
            return ($i, $j+1);
        } else { # Not top row (somewhere on the left col -> going up)
            return ($i-1, $j);
        }
    } else { # Neither first nor last column
        if ($i == $target_layer) { # First row (somewhere on the top row -> going right)
            return ($i, $j+1);
        } else { # Last row (somewhere on the last row -> going left)
            return ($i, $j-1);
        }
    }
}



# Prints given matrix
sub print_matrix {
    foreach my $row (@_) {
        my $cnt=0;
        foreach my $char (@$row) {
            print $char; $cnt++;
            if ( $cnt == scalar(@_) ) {
                print "\n";
            } else {
                print " ";
            }
        }
    }
}

【讨论】:

  • 这很好。我喜欢它 :) 实际上,在 1KB 矩阵上,它的性能几乎与我原来的方法相同。
猜你喜欢
  • 2019-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多