最近在 javaeye 上看到一道面试题。
要求打印出:
int i = 5;
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
int i = 6;
1 2 3 4 5 6
20 21 22 23 24 7
19 32 33 34 25 8
18 31 36 35 26 9
17 30 29 28 27 10
16 15 14 13 12 11
从发贴人的意图来看,这应该是一道 java 面试题( http://www.iteye.com/topic/545378?page=1 );
看到很多热心的网友给出各种各样的答案,其中有 java 、 c 、 ruby 的。看到很多人这么有兴趣,我也给出了自己的答案。
不过我还想到另外两个问题:
1 、什么语言的题目?
2 、考官的目的?
如果是 c 语言,那么第二个问题一定是考查面试者的算法。如果这是一道 java 面试题,那就很难说了。第一,跟考官的水平和思维很大的关系;
第二,确实考查面试者的算法能力;
第三,考查的是面向对象分析和设计的能力。
本人更倾向于第三种,为什么呢?因为主要是 java 本身是面向对象的语言;其次算法已经很成熟了,平时工作可参考资料就可以实现。
很多网友给出的答案大多都是基于面向过程的算法实现。如果这些实现放在其他面向过程的语言中可能非常适合,如果在 java 中就不合适了。 java 是一个面向对象的语言,面向对象的出现无非是为了解决面向过程中出现的一些缺点:可维护性、可扩展性、代码可复用性差等。所以在 java 语言中应该是考查我们的面向对象分析和设计的能力,而不是算法实现 !
如果我是考官,那么让我看一个人的算法实现是很痛苦的事情。首先我没有那么多脑力计算出这个算法实现是否正确,其次让我把代码一行一行敲入计算机,那肯定没有这个功夫。
所以,大家以后碰到这类题目不要一上来就冥思苦想,然后写出让人难于看懂的算法实现。当然也有例外,那就是有些考官本身水平和思维有限,就是想看到你是否有代码。
3 年前我在面试中也碰到类似的题目,我的做法仅仅是说明为了实现这个功能我有那些类和关键方法。结果很不利,所以这个跟考官的水平和思维有很大的关系。
像这道面试题,我第一次看到就联想到这个是一个贪吃蛇游戏的简化版。如果联想不到呢,那就没辙啦。那也不会。
首先,我们从上面可以看出这是一个矩阵,它的高度和宽度是一样的,矩阵中有一些格子存放数字,这个格子就是一个点。
其次,这些数字的增长是有规律的,即顺时针而且不断内旋。
所以我们可以得到两个关键的类:点( Point )和方向( Direction )。既然存在方向这个概念,那么一定存在它的主体,比如车、火箭等,这里我们就用蛇( Snake )吧。
既然有了类,那么他们有那些属性呢?
想到点我们必定也能想到坐标中用 x 、 y 变量来表示。另外跟我们问题域有关的是在这个点上要记录一些信息。所以点有以下定义:
class Point {
private int x ;
private int y ;
private String val ;
}
方向这个概念比较抽象,在我们要研究的问题域中方向有向左、向右、向下、向上,而且有一定的范围。所以在给定范围的方向上,类 Direction 应该知道一个点是否出界,能够计算本方向上下一个点是什么。另外从题目上看,方向的变化规律是顺时针的,所以类 Direction 应该也能够知道它下一个方向是什么。所以方向有以下定义:
abstract class Direction {
int size ;
abstract public Point nextPoint(Point p); // 下个点
abstract public Direction next(Point p); // 下一个方向
abstract public boolean isOut(Point point); // 是否在本方向上出界
}
类 Direction 有四个子类,分别是右、左、下、上。这四个子类通过 next 方法返回另一个 Direction 构成类一个顺时针。
再来看看另外一个核心类 Snake ,它有那些属性呢?这个 Snake 会在一个方向和指定范围内进行游走,碰到壁什么的会掉头,不走已经走过的点。所以它能记住自己走过的点,并且在那里做些标记。最后还会打印出自己走过的路径。
public class Snake {
int size ;
Direction direct ;
Point next ;
Map<String, Point> map = new HashMap<String, Point>(); // 记录走过的点
public Snake( int size, Direction direct, Point point) {
this . size = size;
this . direct = direct;
next = point;
map .put(key( next ), next );
}
// 游走
public void run() {
for ( int i = 2; i < size * size + 1; i++) {
Point tempNext = direct .nextPoint( next );
// 如果在一个方向上出界或者已经走过的点要掉头
if ( direct .isOut(tempNext) || containsPoint(tempNext)) {
direct = direct .next(tempNext);
next = direct .nextPoint( next );
} else {
next = tempNext;
}
next .setVal(String. valueOf (i));
map .put(key( next ), next ); // 记录走过的点
}
}
// 打印走过的点
public void print() {
for ( int y = 1; y < size + 1; y++) {
for ( int x = 1; x < size + 1; x++) {
String temp = " "
+ (containsPoint( new Point(x, y)) ? map .get(key(x, y))
.getVal() : " " );
System. out .print(temp.substring(temp.length() - 5));
}
System. out .println( "" );
}
}
private String key(Point point) {
return key(point.getX(), point.getY());
}
private String key( int x, int y) {
String xVal = "00000" + x;
String yVal = "00000" + y;
return xVal.substring(xVal.length() - 3)
+ yVal.substring(yVal.length() - 3);
}
private boolean containsPoint(Point point) {
return map .containsKey(key(point));
}
public static void main(String[] args) {
// 指定范围、起始点、起始方向
Snake snake = new Snake(6, new Right(6), new Point(1, 1, "1" ));
snake.run();
snake.print();
}
}
到此,如果只给出 Snake 的属性和关键方法( run,print )的实现就可以表达你的设计意图了,其他的只不过是实现细节而已。
为什么这么实现,首先我觉得是自然,至于有什么好处当时还没有想过。看看这句就非常达意:
Snake snake = new Snake(6, new Right(6), new Point(1, 1, "1" ));
对蛇指定范围、起始方向、起始点才能让蛇开始游走。从这里我们也可以知道,对于蛇来讲只要你告诉它这几个输入,它就知道怎么做了,其他的你不用管。
这样设计有什么好处呢?从题目来看,用户只不过想在一个矩阵上打印一些数字序列。我们知道用户的想法最不可靠了。更多的想法如下:
1 、还可以从四个角开始顺时针内旋;
2 、分别可以从四个角逆时针内旋;
3 、还可以这样:
1 2 3 4 5 6
12 11 10 9 8 7
13 14 15 16 17 18
24 23 22 21 20 19
25 26 27 28 29 30
36 35 34 33 32 31
4 、在单数的序列上用红色字体:
1 2 3 4 5 6
20 21 22 23 24 7
19 32 33 34 25 8
18 31 36 35 26 9
17 30 29 28 27 10
16 15 14 13 12 11 ;
5 、 ...........
其中:
关于第一个想法,我们一开始就认为是顺时针内旋的,只要正确的初始化起始点和起始方向就可以轻松满足用户需求:
Snake snake = new Snake(6, new Down(6), new Point(6,1, "1" ));
Snake snake = new Snake(6, new Left(6), new Point(6,6, "1" ));
Snake snake = new Snake(6, new Up(6), new Point(1, 6, "1" ));
关于第二、三个想法,因为我们有 Direction 为抽象类,根据需要重写子类即可。
关于第四个想法,我想这个工作应该由蛇来做,你(蛇)到那个点上爱干嘛就干嘛,不关方向什么事。
所以好处是:
1 、第一、二、三个需求的变化是通过继承和扩展而不是修改来达到增加功能的目的,最大限度地保护 Snake 类的代码不受冲击。
2 、现在我们有很多各种各样“走法”的功能了,现在有点点变化就是想给数字加上颜色。我们不可能针对每种走法都进行修改吧。因为这个职责很自然地分配给了 Snake ,所以我们就只要修改 Snake 即可,同样其他代码得到保护。
下面是 Direction 子类实现示例:
class Right extends Direction {
public Right( int size) {
this . size = size;
}
public Direction next(Point point) {
return new Down( size );
}
public Point nextPoint(Point p) {
return new Point(p.getX() + 1, p.getY());
}
public boolean isOut(Point point) {
return point.getX() > size ;
}
}
class Down extends Direction {
public Down( int size) {
this . size = size;
}
public Direction next(Point point) {
return new Left( size );
}
public Point nextPoint(Point p) {
return new Point(p.getX(), p.getY() + 1);
}
public boolean isOut(Point point) {
return point.getY() > size ;
}
}
class Left extends Direction {
public Left( int size) {
this . size = size;
}
public Direction next(Point point) {
return new Up( size );
}
public Point nextPoint(Point p) {
return new Point(p.getX() - 1, p.getY());
}
public boolean