翻转图像 图解题解
这道题到底在问什么
- 输入
- image=[[1,1,0],[1,0,1],[0,0,0]]
- 输出
- [[1,0,0],[0,1,0],[1,1,1]]
- 输入
- 一行 [1,1,0]
- 输出
- 先逆序成 [0,1,1],再取反成 [1,0,0]
最优解:为什么这么做
一句话答案: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 够不到它,漏掉循环外补的那次取反,整行中央就少翻一格。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这把尺子:两端相等就各翻一次,两端不同就不动,正中间单独翻一次。下面每一帧都在套它。
- 4原图共 3 行 3 列,每格非 0 即 1这是原始图片,绿色格是 1、蓝色格是 0。我们一行一行地处理,每行用左右两个指针向中间收。
- 5第 0 行 = [1, 1, 0],左指针 i = 0,右指针 j = 2轮到第 0 行,内容是 [1, 1, 0]。把左指针放在头、右指针放在尾,紫色标出这两端,准备向中间收。
- 6左端值 1,右端值 0 → 不同看这一行的两端:位置 0 是 1,位置 2 是 0。它们不同。互换后再取反正好抵消,所以这两端最终保持原样,不用动。
- 7位置 0 与 2 维持 1 和 0两端不同,什么都不用改,位置 0 还是 1、位置 2 还是 0。两个指针各自向中间挪一格。
- 8左右指针在位置 1 相遇,中点值 1两个指针在位置 1 相遇,这是正中间那一格,值是 1。它水平翻转后位置不变,所以没有「互相抵消」这回事,只需要老老实实取反一次。
- 9中点位置 1 取反为 0中点从 1 取反成 0(红色闪一下)。这一行所有格子都处理完了。
- 10第 0 行最终 = [1, 0, 0]第 0 行收工,结果是 [1, 0, 0]。已完成行数加一,接着处理下一行,套路完全一样。
- 11第 1 行 = [1, 0, 1],左指针 i = 0,右指针 j = 2轮到第 1 行,内容是 [1, 0, 1]。把左指针放在头、右指针放在尾,紫色标出这两端,准备向中间收。
- 12左端值 1,右端值 1 → 相等看这一行的两端:位置 0 是 1,位置 2 是 1。它们相等。水平翻转会把它俩互换,换完仍相等,再各取反,等于两端都要翻一次。
- 13位置 0 与 2 翻转为 0 和 0两端相等,各翻一次:位置 0 变 0、位置 2 变 0(红色闪一下表示刚翻)。这一帧就同时做完了这对格子的翻转和取反。
- 14左右指针在位置 1 相遇,中点值 0两个指针在位置 1 相遇,这是正中间那一格,值是 0。它水平翻转后位置不变,所以没有「互相抵消」这回事,只需要老老实实取反一次。
- 15中点位置 1 取反为 1中点从 0 取反成 1(红色闪一下)。这一行所有格子都处理完了。
- 16第 1 行最终 = [0, 1, 0]第 1 行收工,结果是 [0, 1, 0]。已完成行数加一,接着处理下一行,套路完全一样。
- 17第 2 行 = [0, 0, 0],左指针 i = 0,右指针 j = 2轮到第 2 行,内容是 [0, 0, 0]。把左指针放在头、右指针放在尾,紫色标出这两端,准备向中间收。
- 18左端值 0,右端值 0 → 相等看这一行的两端:位置 0 是 0,位置 2 是 0。它们相等。水平翻转会把它俩互换,换完仍相等,再各取反,等于两端都要翻一次。
- 19位置 0 与 2 翻转为 1 和 1两端相等,各翻一次:位置 0 变 1、位置 2 变 1(红色闪一下表示刚翻)。这一帧就同时做完了这对格子的翻转和取反。
- 20左右指针在位置 1 相遇,中点值 0两个指针在位置 1 相遇,这是正中间那一格,值是 0。它水平翻转后位置不变,所以没有「互相抵消」这回事,只需要老老实实取反一次。
- 21中点位置 1 取反为 1中点从 0 取反成 1(红色闪一下)。这一行所有格子都处理完了。
- 22第 2 行最终 = [1, 1, 1]第 2 行收工,结果是 [1, 1, 1]。三行全部处理完毕,下一帧返回结果。
- 23三行都做完,返回 [[1,0,0],[0,1,0],[1,1,1]]三行全部处理完,矩阵变成 [[1,0,0],[0,1,0],[1,1,1]]。每一行都是「逆序 + 取反」的结果,而我们只用一遍双指针就一次性做完了两步。
- 24先逐行逆序、再整张取反,得到一模一样的矩阵换个老实办法验证一下:把每行先逆序、再把全图 0 和 1 互换,得到的矩阵和上一帧完全相同。可见双指针那套「相等才翻、不同不动、中点单翻」确实等价于规规矩矩的两步法。
⚠️ 容易写错的地方
✗ 错:把两端不同的情况也去翻转
✓ 对:只有两端相等才各翻一次,两端不同保持不动
不同的两端经过「互换 + 取反」正好抵消回原值,强行改反而出错
✗ 错:奇数边长漏掉正中间那一格
✓ 对:循环结束后判断 i 是否等于 j,相等则单独把中点取反一次
中点没有配对的另一端,只在 i 和 j 相遇时处理,忘了它会少翻一格
✗ 错:只背两步顺序,没推清双指针一对位置的净效果
✓ 对:双指针条件要按「一对位置交换 + 各取反」的净效果推导
逐格取反与逐行逆序本就可交换、顺序不影响结果;真正的坑是不去推一对位置的净效果,只死记两步,遇到「相等才翻、不同不动、中点单翻」就说不清为什么
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from 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 = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class 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 imageC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
vector<vector<int>> flipAndInvertImage(vector<vector<int>>& image) {
for (auto& row : image) {
int i = 0, j = row.size() - 1;
for (; i < j; ++i, --j) {
if (row[i] == row[j]) {
row[i] ^= 1;
row[j] ^= 1;
}
}
if (i == j) {
row[i] ^= 1;
}
}
return image;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int[][] flipAndInvertImage(int[][] image) {
for (int[] row : image) {
int i = 0, j = row.length - 1;
for (; i < j; ++i, --j) {
if (row[i] == row[j]) {
row[i] ^= 1;
row[j] ^= 1;
}
}
if (i == j) {
row[i] ^= 1;
}
}
return image;
}
}复杂度
时间
O(n²)
矩阵共 n×n 个格子,每格恰好被一个指针碰一次,无重复无回头
空间
O(1)
原地修改矩阵,只用 i、j 两个指针变量,不开额外矩阵
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 翻转图像 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
一遍双指针凭什么能把水平翻转和取反两步并成一步?+
因为水平翻转就是把对称的一对位置 row[i] 和 row[j] 的值对调,取反是给每格做一次开关。把这两件事落到一对位置上算净效果:相等的一对,对调后不变、再各取反,所以各翻一次;不同的一对,对调再取反正好抵消,所以不动;中点对调后位置没变,只取反一次。每一对的最终值直接算得出,就不必先生成逆序数组再扫第二趟,省了一趟遍历和一份额外空间。
边长是偶数时,会不会有格子被漏掉?+
不会。偶数边长时左指针 i 和右指针 j 一路错身而过、永远不会落在同一格,所有格子都成对处理,循环自然覆盖整行。只有奇数边长才会出现 i 等于 j 的正中间一格,那时在循环外单独补一次取反即可。代码用 i 小于 j 控制成对处理、再用 i 等于 j 兜住中点,奇偶都不漏。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 翻转图像 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。