Algorithm
题目:832-翻转图像
给定一个二进制矩阵 A
,我们想先水平翻转图像,然后反转图像并返回结果。
水平翻转图片就是将图片的每一行都进行翻转,即逆序。例如,水平翻转 [1, 1, 0]
的结果是 [0, 1, 1]
。
反转图片的意思是图片中的 0
全部被 1
替换, 1
全部被 0
替换。例如,反转 [0, 1, 1]
的结果是 [1, 0, 0]
。
示例 1:
输入: [[1,1,0],[1,0,1],[0,0,0]]
输出: [[1,0,0],[0,1,0],[1,1,1]]
解释: 首先翻转每一行: [[0,1,1],[1,0,1],[0,0,0]];
然后反转图片: [[1,0,0],[0,1,0],[1,1,1]]
示例 2:
输入: [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
输出: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
解释: 首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
说明:
1 <= A.length = A[0].length <= 20
0 <= A[i][j] <= 1
思路:初始矩阵和最终矩阵的规律是,以列对称的方式来观察矩阵,可以发现:
- 如果初始矩阵对称列相等,则最终矩阵的对应元素是初始元素的反转
- 如果初始矩阵对称列相反,则最终矩阵的对应元素保持初始元素不变
如果矩阵的列数为奇数,则中间列(对称列)取反。代码如下:
class Solution {
public int[][] flipAndInvertImage(int[][] A) {
int row = A.length;
int column = A[0].length;
int p1;
int p2;
for(int i=0;i<row;i++){
p1 = 0;
p2 = column - 1;
for(;p1<p2;p1++,p2--){
if(A[i][p1] == A[i][p2]){
A[i][p1] ^= 1;
A[i][p2] ^= 1;
}
}
if(p1 == p2){
A[i][p1] ^= 1;
}
}
return A;
}
}
注意:不能直接使用~
来取反,因为~
是对实际存储的二进制操作的,所以~1!=0
,可通过元素与 1 的异或来完成反转操作。
PS:这道题怎么有种找规律的错觉…
Review
原文链接:《Introduction to Java Bytecode》
一篇 Java 字节码入门的文章,图文并茂地讲了 Java 字节码的几个简单指令以及变量在栈中位置的变化。这里列出了里面出现的一些指令(详见字节码指令官方文档):
-
int
型变量的部分指令:以下指令都是int
型变量的操作指令iconst_<i>
:向操作数栈中压入int
型常量 iistore_<n>
:将操作数栈顶的int
型 value 弹出并存储到当前栈帧的局部变量数组的索引 n 处iload_<n>
:获取局部变量数组索引 n 处的变量(必须是int
型),将其压入操作数栈中iadd
:从操作数栈的栈顶弹出两个int
值,相加后结果压入操作数栈顶i2d
:将操作数栈顶的int
值扩展长度,转为double
类型。d2i
与i2d
是相反的过程
-
方法调用:
-
invokestatic
:调用类的静态方法,后面会跟一个当前类的运行时常量池中某个类或接口的静态方法的符号引用- 不太理解符号引用和直接引用的区别,只知道在类加载的解析阶段,JVM会将常量池内的符号引用替换为直接引用。
-
invokespecial
:调用实例方法;对父类方法、私有方法、实例初始化方法调用的特殊处理。这里是查看的官方文档,可能有理解不到位的地方,所以下面给出了原文内容:Invoke instance method; special handling for superclass, private, and instance initialization method invocations
- 注意:使用构造函数创建对象实例时,会通过
dup
指令复制一份对象的引用并压入操作数栈,所以创建实例时,在操作数栈中会有两个相同的值指向同一个实例化的对象,其中复制出来的值(用于找到对象)会和构造函数的参数一起被“消费”从而创建实例,另一个则用于赋值给局部变量,便于用户调用,如果没有赋值操作的话则直接从栈中弹出。
- 注意:使用构造函数创建对象实例时,会通过
-
invokevirtual
:调用实例方法(不能是初始化方法),而且可以根据类的实际类型来调用合适的方法。class Father{ public void say() { System.out.println("I'm father"); } } public class Son extends Father{ @Override public void say() { //查看该方法的字节码可以发现,调用父类方法用的是 invokespecial 指令 super.say(); System.out.println("I'm son"); } public static void main(String[] args) { //无论用父类接收还是子类接收,调用 say 方法的字节码都是 invokevirtual final Son sonn = new Son(); //初始化时用的是 invokespecial 指令 //invokevirtual #7 // Method say:()V son.say(); } }
-
invokeinterface
:调用接口方法。在文章中并没有出现,这里是为了对比invokevirtual
列出的interface Shape{ void area(); } public class Square implements Shape { int a,b; public Square(int a, int b) { this.a = a; this.b = b; } @Override public void area() { System.out.println(a*b); } public static void main(String[] args) { //使用实际类型接收,调用 area 方法的字节码: // invokevirtual #8 // Method area:()V Square square = new Square(2, 3) square.area(); //而如果使用接口 Shape 来接收,调用 area 方法的字节码: // invokeinterface #8, 1 // InterfaceMethod feature/Shape.area:()V Shape shape = new Square(2, 3) shape.area(); } }
-
还有一个调用方法的指令
invokedynamic
,是用于调用动态方法,由于自己对底层的一些知识了解太少,所以看的有点不理解,这里没有说明,会等到以后再分享。
-
Share
最近看了一个TED演讲,大致内容是每个人都有学习区和表现区两个认知领域,在学习区我们会为了进步而设计自己的行为,更关注自己没有掌握地东西,从犯错地地方吸取教训,而在表现区,我们则会聚焦于自己掌握的东西,并尽力减少犯错。然而,过多的将实践花费在表现区,长此以往就很难有所提高。
比如,学校本应是一个学习成长的地方,但由于家长、老师对于优异成绩的青睐,使得学生们误以为学校是一个表现区,他们更多的关注于如何提高成绩,而很少进行开拓性的思考,所以在走向社会后反而出现了曾经的学霸同学给当初的差等生打工的故事。
在工作中也类似,当我们可以很好的完成任务,而缺少压力时,如果长期处于这样一种舒适的环境,就会发现自己的进步变得越来越慢,所以我们需要转换自己的思考,不要让自己的认知长期停留在表现区,而应在两者之间来回转换,不断提高自己,正如在《The Key To Accelerating Your Coding Skills》一文中说到的:“For the rest of your life,go outside your limits every single day.”,在空余时间去面对更高的挑战,不要害怕失败,乘上与平时相反的列车,去看未曾见过的风景,并在最后做出“当时那么做了真好”的感想,而不是后悔当初为什么安于现状。
PS:这里的分享和技术关系不太大,仅仅是自己的一点总结,如果有不同的看法,欢迎交流讨论。
Tips
这周读了一篇 Java 字节码入门的文章,在实践里面的示例时发现,对于复杂的代码,通过javap -v xxx.class命令得到的输出有些不易看懂,可以将其与 IDE 对 class 文件反编译的结果对比来看,会相对容易一些。