我想再加一点细节。在这个答案中,关键概念被重复强调,节奏缓慢并且有意地反复。虽然提供的解决方案不是语法最紧凑的,但它旨在帮助那些想要学习矩阵旋转及其实现结果的人。
首先,什么是矩阵?对于本答案而言,矩阵只是一个宽度和高度相同的网格。请注意,矩阵的宽度和高度可以不同,但为了简单起见,本教程仅考虑宽度和高度相等的矩阵(方阵)。是的,矩阵是matrix的复数形式。
示例矩阵包括:2×2、3×3或5×5。或者更一般地说,N×N。一个2×2的矩阵将有4个正方形,因为2×2=4。一个5×5的矩阵将有25个正方形,因为5×5=25。每个正方形都称为元素或条目。我们将在下面的图表中用句点(.
)表示每个元素:
2×2矩阵
. .
. .
3×3 矩阵
. . .
. . .
. . .
4x4矩阵
. . . .
. . . .
. . . .
. . . .
那么,旋转矩阵是什么意思呢?让我们拿一个 2×2 的矩阵,并在每个元素中放一些数字,以便观察旋转:
0 1
2 3
将此旋转90度可得:
2 0
3 1
我们实际上把整个矩阵向右旋转了一次,就像转动汽车的方向盘一样。可以将矩阵“倾斜”到右侧进行思考。我们想要编写一个Python函数,将矩阵向右旋转一次。函数签名如下:
def rotate(matrix):
矩阵将使用二维数组来定义:
matrix = [
[0,1],
[2,3]
]
因此,第一个索引位置访问行,第二个索引位置访问列:
matrix[row][column]
我们将定义一个实用函数来打印矩阵。
def print_matrix(matrix):
for row in matrix:
print row
一种矩阵旋转的方法是逐层进行。但是什么是层呢?可以把洋葱作为类比,就像剥洋葱的层,每一层被移除后,我们就靠近中心了。另一个类比可以是 保卫女孩游戏 或者是俄罗斯套娃。
矩阵的宽度和高度决定该矩阵中的层数。让我们使用不同的符号来表示每个层:
一个2×2的矩阵有1层。
. .
. .
一个3×3矩阵有两层
. . .
. x .
. . .
一个4×4矩阵有两个层
. . . .
. x x .
. x x .
. . . .
一个5×5的矩阵有3层
. . . . .
. x x x .
. x O x .
. x x x .
. . . . .
一个6×6的矩阵有3层
. . . . . .
. x x x x .
. x O O x .
. x O O x .
. x x x x .
. . . . . .
一个7×7的矩阵有4个层
. . . . . . .
. x x x x x .
. x O O O x .
. x O - O x .
. x O O O x .
. x x x x x .
. . . . . . .
你可能会注意到,将矩阵的宽度和高度增加1,并不总是增加层数。通过上述矩阵并制表计算出层数和维度,我们可以看到每增加一次宽度和高度,层数增加一次:
+
| N×N | Layers |
+
| 1×1 | 1 |
| 2×2 | 1 |
| 3×3 | 2 |
| 4×4 | 2 |
| 5×5 | 3 |
| 6×6 | 3 |
| 7×7 | 4 |
+
然而,并不是所有的层都需要旋转。一个 1×1 的矩阵在旋转前后是相同的。无论整个矩阵有多大,中心的 1×1 层在旋转前后始终保持相同:
+
| N×N | Layers | Rotatable Layers |
+
| 1×1 | 1 | 0 |
| 2×2 | 1 | 1 |
| 3×3 | 2 | 1 |
| 4×4 | 2 | 2 |
| 5×5 | 3 | 2 |
| 6×6 | 3 | 3 |
| 7×7 | 4 | 3 |
+
给定一个 N×N 的矩阵,我们如何编程确定需要旋转的层数?如果将宽度或高度除以二并忽略余数,我们将得到以下结果。
+
| N×N | Layers | Rotatable Layers | N/2 |
+
| 1×1 | 1 | 0 | 1/2 = 0 |
| 2×2 | 1 | 1 | 2/2 = 1 |
| 3×3 | 2 | 1 | 3/2 = 1 |
| 4×4 | 2 | 2 | 4/2 = 2 |
| 5×5 | 3 | 2 | 5/2 = 2 |
| 6×6 | 3 | 3 | 6/2 = 3 |
| 7×7 | 4 | 3 | 7/2 = 3 |
+
注意到 N/2
与需要旋转的层数相对应吗?有时可旋转的层数比矩阵中总层数少一层。当最内层仅由一个元素(即1×1矩阵)构成时,会出现这种情况,因此不需要旋转它。只需忽略它。
我们在编写矩阵旋转函数时无疑需要这些信息,因此让我们现在添加它:
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
现在我们知道了层是什么,以及如何确定实际需要旋转的层数,那么我们如何隔离单个层以便旋转它呢?首先,我们从最外层向内部检查矩阵。一个5×5的矩阵总共有三层,其中两层需要旋转:
. . . . .
. x x x .
. x O x .
. x x x .
. . . . .
首先让我们看一下列。假设我们从0开始计数,定义最外层的列的位置是0和4:
+--------+-----------+
| Column | 0 1 2 3 4 |
+--------+-----------+
| | . . . . . |
| | . x x x . |
| | . x O x . |
| | . x x x . |
| | . . . . . |
+--------+-----------+
0和4也是最外层行的位置。
+-----+-----------+
| Row | |
+-----+-----------+
| 0 | . . . . . |
| 1 | . x x x . |
| 2 | . x O x . |
| 3 | . x x x . |
| 4 | . . . . . |
+-----+-----------+
由于宽度和高度相同,这种情况将始终存在。因此,我们只需使用两个值(而不是四个)即可定义图层的列和行位置。
向第二层内移动时,列的位置为1和3。是的,你猜对了,行也是如此。重要的是要理解,当向下一层移动时,我们必须同时增加和减少行和列位置。
+
| Layer | Rows | Columns | Rotate? |
+
| Outermost | 0 and 4 | 0 and 4 | Yes |
| Inner | 1 and 3 | 1 and 3 | Yes |
| Innermost | 2 | 2 | No |
+
因此,为了检查每一层,我们需要一个循环,其中包括递增和递减的计数器,表示从外向内移动,从最外层开始。我们将称其为“层循环”。
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
for layer in range(0, layer_count):
first = layer
last = size - first - 1
print 'Layer %d: first: %d, last: %d' % (layer, first, last)
matrix = [
[ 0, 1, 2, 3, 4],
[ 5, 6, 6, 8, 9],
[10,11,12,13,14],
[15,16,17,18,19],
[20,21,22,23,24]
]
rotate(matrix)
代码以上循环遍历需要旋转的任何图层的(行和列)位置。
Layer 0: first: 0, last: 4
Layer 1: first: 1, last: 3
现在我们有一个循环,提供每个层的行和列的位置。变量first
和last
标识第一行和列的索引位置以及最后一行和列的索引位置。回顾我们的行和列表格:
+--------+-----------+
| Column | 0 1 2 3 4 |
+--------+-----------+
| | . . . . . |
| | . x x x . |
| | . x O x . |
| | . x x x . |
| | . . . . . |
+--------+-----------+
+-----+-----------+
| Row | |
+-----+-----------+
| 0 | . . . . . |
| 1 | . x x x . |
| 2 | . x O x . |
| 3 | . x x x . |
| 4 | . . . . . |
+-----+-----------+
现在我们可以穿过一个矩阵的层。现在我们需要一种方式来在层内导航,以便我们可以移动该层中的元素。请注意,元素永远不会从一层跳到另一层,但它们确实在各自的层内移动。
旋转层中的每个元素会旋转整个层。旋转矩阵中的所有层将旋转整个矩阵。这句话非常重要,请在继续之前努力理解它。
现在,我们需要一种实际移动元素的方法,即旋转每个元素,随后是层,最终是矩阵。为简单起见,我们将恢复为一个3x3的矩阵——它具有一个可旋转的层。
0 1 2
3 4 5
6 7 8
我们的层循环提供第一列和最后一列的索引,以及第一行和最后一行的索引:
我们的层循环提供第一列和最后一列的索引,以及第一行和最后一行的索引:
+-----+-------+
| Col | 0 1 2 |
+-----+-------+
| | 0 1 2 |
| | 3 4 5 |
| | 6 7 8 |
+-----+-------+
+-----+-------+
| Row | |
+-----+-------+
| 0 | 0 1 2 |
| 1 | 3 4 5 |
| 2 | 6 7 8 |
+-----+-------+
由于我们的矩阵总是正方形,因此我们只需要两个变量first
和last
,因为行和列的索引位置相同。
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
for layer in range(0, layer_count):
first = layer
last = size - first - 1
变量 first
和 last
可以轻松用来引用矩阵的四个角落。这是因为角落本身可以使用各种排列方式来定义,其中没有对这些变量进行减法、加法或偏移:
+
| Corner | Position | 3x3 Values |
+
| top left | (first, first) | (0,0) |
| top right | (first, last) | (0,2) |
| bottom right | (last, last) | (2,2) |
| bottom left | (last, first) | (2,0) |
+
出于这个原因,我们从外部的四个角开始旋转——我们首先旋转它们。让我们用*
突出显示它们。
* 1 *
3 4 5
* 7 *
我们想要将每个
*
与其右边的
*
进行交换。 因此,让我们继续打印出仅使用
first
和
last
的各种排列定义的角:
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
for layer in range(0, layer_count):
first = layer
last = size - first - 1
top_left = (first, first)
top_right = (first, last)
bottom_right = (last, last)
bottom_left = (last, first)
print 'top_left: %s' % (top_left)
print 'top_right: %s' % (top_right)
print 'bottom_right: %s' % (bottom_right)
print 'bottom_left: %s' % (bottom_left)
matrix = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8]
]
rotate(matrix)
输出结果应为:
top_left: (0, 0)
top_right: (0, 2)
bottom_right: (2, 2)
bottom_left: (2, 0)
现在我们可以在层循环中轻松地交换每个角落:
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
for layer in range(0, layer_count):
first = layer
last = size - first - 1
top_left = matrix[first][first]
top_right = matrix[first][last]
bottom_right = matrix[last][last]
bottom_left = matrix[last][first]
matrix[first][first] = bottom_left
matrix[first][last] = top_left
matrix[last][last] = top_right
matrix[last][first] = bottom_right
print_matrix(matrix)
print '---------'
rotate(matrix)
print_matrix(matrix)
旋转角落之前的矩阵:
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
旋转角落后的矩阵:
[6, 1, 0]
[3, 4, 5]
[8, 7, 2]
太好了!我们已经成功地旋转了矩阵的每个角落。但是,我们还没有旋转每个层的中间元素。很明显,我们需要一种在层内进行迭代的方法。
问题在于,到目前为止,我们函数中唯一的循环(即我们的层循环)在每次迭代时都会移动到下一层。由于我们的矩阵只有一个可旋转的层,因此层循环在仅旋转角落后退出。让我们看一下在较大的5×5矩阵中会发生什么情况(其中两层需要旋转)。函数代码已被省略,但与上面相同:
matrix = [
[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19],
[20, 21, 22, 23, 24]
]
print_matrix(matrix)
print '--------------------'
rotate(matrix)
print_matrix(matrix)
输出结果为:
[20, 1, 2, 3, 0]
[ 5, 16, 7, 6, 9]
[10, 11, 12, 13, 14]
[15, 18, 17, 8, 19]
[24, 21, 22, 23, 4]
外层的角落被旋转不应该让人感到惊讶,但你可能也会注意到内层(向内)的角落也被旋转了。这是有道理的。我们编写了代码来浏览各个图层并旋转每个图层的角落。这感觉像是进步,但不幸的是我们必须退一步。在之前(外部)层的每个元素都被旋转之前,不能继续进入下一层。仅旋转角落是不够的!
深呼吸。我们需要另一个循环。嵌套循环更甚。新的嵌套循环将使用 first
和 last
变量以及偏移量在层内进行导航。我们将称此新循环为“元素循环”。元素循环将访问顶部行中的每个元素,右侧的每个元素,底部行中的每个元素和左侧上方的每个元素。
- 沿着顶行向前移动需要增加列索引。
- 向下移动右侧需要增加行索引。
- 向后沿底部移动需要减少列索引。
- 向上移动左侧需要减少行索引。
听起来很复杂,但因为我们在矩阵的所有四个侧面上增加和减少的次数是相同的,所以这变得很容易。例如:
- 将1个元素移动到顶行。
- 将1个元素向下移动右侧。
- 将1个元素向后沿底部行移动。
- 将1个元素向左方向上移动。
这意味着我们可以使用单个变量与 first
和 last
变量相结合来在层内移动。需要注意的是,沿着顶行移动和向下移动右侧都需要增加。而向后沿着底部和向上移动左侧则需要减少。
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
for layer in range(0, layer_count):
first = layer
last = size - first - 1
for element in range(first, last):
offset = element - first
top = (first, element)
right_side = (element, last)
bottom = (last, last-offset)
left_side = (last-offset, first)
print 'top: %s' % (top)
print 'right_side: %s' % (right_side)
print 'bottom: %s' % (bottom)
print 'left_side: %s' % (left_side)
现在我们只需要将顶部分配给右侧,右侧分配给底部,底部分配给左侧,左侧分配给顶部。将所有这些组合起来,我们得到:
def rotate(matrix):
size = len(matrix)
layer_count = size / 2
for layer in range(0, layer_count):
first = layer
last = size - first - 1
for element in range(first, last):
offset = element - first
top = matrix[first][element]
right_side = matrix[element][last]
bottom = matrix[last][last-offset]
left_side = matrix[last-offset][first]
matrix[first][element] = left_side
matrix[element][last] = top
matrix[last][last-offset] = right_side
matrix[last-offset][first] = bottom
考虑以下矩阵:
0, 1, 2
3, 4, 5
6, 7, 8
我们的rotate
函数会产生以下结果:
6, 3, 0
7, 4, 1
8, 5, 2