题目描述
思路解析
一句话答案:LeetCode 832 翻转图像:每行先水平翻转再按位取反,用对撞双指针把两步合成一遍——两端相等就各取反、不同就不动、中点单独取反,时间 O(n²)、空间 O(1)。
n×n 的 0/1 图片,翻转加取反要返回什么
给一个 n×n 的二进制矩阵 image,只有 0 和 1。要做两件事:每一行先水平翻转(整行首尾倒过来,[1,1,0] 逆序成 [0,1,1]),再整图取反(0 换 1、1 换 0,[0,1,1] 取反成 [1,0,0]),返回结果。题面 image=[[1,1,0],[1,0,1],[0,0,0]],答案是 [[1,0,0],[0,1,0],[1,1,1]]。
先逆序整行、再整图取反,得多扫一趟多占一行
照题面直译:另开一行,把原行从后往前抄一遍得逆序,再逐格对调 0 和 1。一行 n 个数、扫两趟,还得为逆序结果多占一整行空间。全矩阵 n×n 个格子各碰两回,时间 O(n²)、额外空间 O(n)。这么写能过,可那两趟遍历和那份额外空间都省得掉。
一对位置交换再取反,净效果是什么
水平翻转就是把一行里对称的一对值对调——左端的 row[i] 换到右端 row[j] 的位置;取反是给每格做一次 0/1 开关。把这两件事落在一对位置上算清楚:这一对相等时,对调后仍相等、再各取反,等于两端各翻一次;这一对不同时,对调再取反恰好抵消,两端原样不动。
于是逆序和取反不必分两趟。用一个对撞双指针(左指针 i 从头、右指针 j 从尾,一起朝中间收)扫每一行:i、j 值相等就把两端各取反一次,不同就跳过,全程原地改、不再另建逆序数组。奇数长的行正中间剩一格,它对调后位置不动,单独取反一次即可。
双指针怎么收,中点为什么单独处理
对每一行,起手让 i 指向行首、j 指向行尾(下标 n−1)。i 还在 j 左边时看这一对:row[i] 等于 row[j] 就把两个都异或 1——异或 1 正好是一次 0/1 翻转,不同则原封不动;随后 i 右移、j 左移,继续朝中间收。等 i 和 j 撞上(i 等于 j),落到正中间那一格,它没有配对的另一端,循环外补一次取反。偶数行长时 i 和 j 错身而过、永不相等,全部成对处理,走不到中点这一步。
题面这三行,双指针各扫出什么
第 0 行 [1,1,0]:i 在位置 0(值 1)、j 在位置 2(值 0),两端不同,跳过;i、j 收到位置 1 相遇,这是中点,值 1,单独取反成 0,得 [1,0,0]。
第 1 行 [1,0,1]:两端都是 1,相等,各取反成 0,行成 [0,0,0];中点位置 1 的 0 取反成 1,得 [0,1,0]。第 2 行 [0,0,0]:两端都是 0 相等,各取反成 1,行变 [1,0,1],中点 0 取反成 1,得 [1,1,1]。三行拼起来正是题面答案 [[1,0,0],[0,1,0],[1,1,1]]。
不同的两端别去翻,奇数中点别漏掉
每个格子恰好被一个指针碰一次,n×n 个格子,时间 O(n²);只用 i、j 两个指针原地改,空间 O(1)。真正常写坏的是两端不同那一对:它经过互换加取反本就回到原值,你若顺手也给它翻一下,反倒把对的改成错的。另一处躲在奇数边长的正中间:那格只在 i 和 j 相遇时露面,循环里 i 严格小于 j 够不到它,漏掉循环外补的那次取反,整行中央就少翻一格。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记牢这把尺子:两端相等就各翻一次,两端不同就不动,正中间单独翻一次。下面每一帧都在套它。
输入矩阵 · 3×3 · 绿=1 蓝=0:这是原始图片,绿色格是 1、蓝色格是 0。我们一行一行地处理,每行用左右两个指针向中间收。
聚焦第 0 行 · 摆好双指针:轮到第 0 行,内容是 [1, 1, 0]。把左指针放在头、右指针放在尾,紫色标出这两端,准备向中间收。
比较两端 · 位置 0 与 2:看这一行的两端:位置 0 是 1,位置 2 是 0。它们不同。互换后再取反正好抵消,所以这两端最终保持原样,不用动。
不同 → 两端保持不变:两端不同,什么都不用改,位置 0 还是 1、位置 2 还是 0。两个指针各自向中间挪一格。
正中间一格 · 位置 1:两个指针在位置 1 相遇,这是正中间那一格,值是 1。它水平翻转后位置不变,所以没有「互相抵消」这回事,只需要老老实实取反一次。
中点取反 → 0:中点从 1 取反成 0(红色闪一下)。这一行所有格子都处理完了。
第 0 行完成 · [1, 0, 0]:第 0 行收工,结果是 [1, 0, 0]。已完成行数加一,接着处理下一行,套路完全一样。
聚焦第 1 行 · 摆好双指针:轮到第 1 行,内容是 [1, 0, 1]。把左指针放在头、右指针放在尾,紫色标出这两端,准备向中间收。
比较两端 · 位置 0 与 2:看这一行的两端:位置 0 是 1,位置 2 是 1。它们相等。水平翻转会把它俩互换,换完仍相等,再各取反,等于两端都要翻一次。
相等 → 两端各翻一次:两端相等,各翻一次:位置 0 变 0、位置 2 变 0(红色闪一下表示刚翻)。这一帧就同时做完了这对格子的翻转和取反。
正中间一格 · 位置 1:两个指针在位置 1 相遇,这是正中间那一格,值是 0。它水平翻转后位置不变,所以没有「互相抵消」这回事,只需要老老实实取反一次。
中点取反 → 1:中点从 0 取反成 1(红色闪一下)。这一行所有格子都处理完了。
第 1 行完成 · [0, 1, 0]:第 1 行收工,结果是 [0, 1, 0]。已完成行数加一,接着处理下一行,套路完全一样。
聚焦第 2 行 · 摆好双指针:轮到第 2 行,内容是 [0, 0, 0]。把左指针放在头、右指针放在尾,紫色标出这两端,准备向中间收。
比较两端 · 位置 0 与 2:看这一行的两端:位置 0 是 0,位置 2 是 0。它们相等。水平翻转会把它俩互换,换完仍相等,再各取反,等于两端都要翻一次。
相等 → 两端各翻一次:两端相等,各翻一次:位置 0 变 1、位置 2 变 1(红色闪一下表示刚翻)。这一帧就同时做完了这对格子的翻转和取反。
正中间一格 · 位置 1:两个指针在位置 1 相遇,这是正中间那一格,值是 0。它水平翻转后位置不变,所以没有「互相抵消」这回事,只需要老老实实取反一次。
中点取反 → 1:中点从 0 取反成 1(红色闪一下)。这一行所有格子都处理完了。
第 2 行完成 · [1, 1, 1]:第 2 行收工,结果是 [1, 1, 1]。三行全部处理完毕,下一帧返回结果。
全部完成 · 返回结果矩阵:三行全部处理完,矩阵变成 [[1,0,0],[0,1,0],[1,1,1]]。每一行都是「逆序 + 取反」的结果,而我们只用一遍双指针就一次性做完了两步。
对照验证 · 两步法同样结果:换个老实办法验证一下:把每行先逆序、再把全图 0 和 1 互换,得到的矩阵和上一帧完全相同。可见双指针那套「相等才翻、不同不动、中点单翻」确实等价于规规矩矩的两步法。
边界先想清:1×1 只取反;偶数边长没有中点,所有格子成对处理。
两个高频追问:合并两步靠逐对算净效果;偶数无中点、奇数补中点。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = rightclass ListNode: def __init__(self, val=0, next=None): self.val = val self.next = nextclass Solution: def flipAndInvertImage(self, image: List[List[int]]) -> List[List[int]]: n = len(image) for row in image: i, j = 0, n - 1 while i < j: if row[i] == row[j]: row[i] ^= 1 row[j] ^= 1 i, j = i + 1, j - 1 if i == j: row[i] ^= 1 return image复杂度
- 时间:O(n²),矩阵共 n×n 个格子,每格恰好被一个指针碰一次,无重复无回头
- 空间:O(1),原地修改矩阵,只用 i、j 两个指针变量,不开额外矩阵
易错点
面试追问把动画讲成自己的话
追问为什么一遍双指针就能把「翻转」和「取反」两步合并?
追问如果边长是偶数,会不会漏处理某些格子?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
按奇偶排序数组
LeetCode 905 · 简单 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题