运用面向对象的分析与设计模式巧解面试算法题
来源:互联网 发布:电脑破解wifi密码软件 编辑:程序博客网 时间:2024/06/16 00:14
IT行业,千变万化,日新月异,身处其中的各位同仁必感同身受,特别是对从事技术开发的朋友们而言,或许能感觉到唯一不变的就是变化。对纷繁复杂的程序人生而言,其实有一个看不见的主旋律,那就是找工作,找房子,找人(另一半)。如果你是心怀梦想,浪迹于上广北深的千万大军中的一员的话,我想这一句话该是你生活的写照了。在三找中,找工作是其他两找的基础,本系列文章主要是就找工作展开而言。
对开发而言,面试的过程,很多时候就是解算法题,给你一块白板,一支笔,你在白板上写下面试者给你的算法题解。在此,我向大家推荐一个面试算法题网站leetcode oj(https://oj.leetcode.com/),该网站收集了个大著名软件公司的算法题,正在找工作的朋友不妨上去看看。算法面试大概分两部分,一是要做出来,二是要说清楚。很多失败的情况,主要是倒在第二条。说不清楚,除了表达能力外,很重要的一个原因就是算法的设计不不清晰,软件是一个逻辑复杂聚合体,如果做的不清楚,想讲清楚,不太可能,接下来,借用leetcode的一道算法题,讨论一下如何利用面向对象的分析和设计模式给出清晰设计方案。
Validate if a given string is numeric.
Some examples:
"0" => true
" 0.1 " => true
"abc" => false
"1 a" => false
"2e10" => true
Note: It is intended for the problem statement to be ambiguous. You should gather all requirements up front before implementing one.
(https://oj.leetcode.com/problems/valid-number/)
这道题目初看上去很简单,如果你拿到后直接动手,恐怕就危险了,如同它提示所说的,题目一个隐含目的就是模糊性,如果你不对题目仔细分析,并对面试官提出一些减少模糊性的问题就直接动手,那你很可能就已经被pass了。题目要求设计算法,判断字符串是否是数字,数字格式合法性的情况太多了,除了题目给定的例子外,还有很多情况需要搞清楚,例如“00123”是否合法, ".123", "123." 是否是合法的浮点数。而且我们不要以为只要判断给定字符串符合例子格式就可以了,同时还要考虑十六进制格式“0Xabc"是否也是合法的数字格式,罗马数字”I, II, IV"等是否也是合理格式。这道题不但语义模糊,而且具备极高的开放性,扩展性,据说在leetcode上,对该题的测试用例就有1400多个,因此题目看起来简单,但隐藏着很多的陷阱,所以它在leetcode里难度被标记为"Hard". 我们先看看leetcode里一位朋友给的c++解法:
这是一段简洁的应该是通过了测试的c++解决方案,这段代码是“对”的,但是可能会有一些问题,
一,它的表述性不强,除了作者外,其他人要容易的看懂,估计不容易(其他人也包括面试官)。
二,代码中有若干注释,大家如果读过Code Complete, Clean Code 等编程大牛写的书籍,则会意识到,好的代码是不需要注释的,当你需要注释才能解释代码的时候,很可能意味着你的设计不清楚,你的代码有code smell。
第三,将算法逻辑通过很多if else语句柔和起来,调试会复杂,修改一处逻辑很容易引起新的bug。
第四,很难扩展,该解法只处理了问题中给的几个例子,很显然,合理数字不止这三种,如果要添加新的判断功能的话,例如判断十六进制数,那将是困难重重,添加后接下来就是找bug了。如果要是这是面试给出的答案,恐怕要通过的可能性会不高。接下来,我们看看,能否通过面向对象的方法和设计模式给出一个清晰的,可容错,可扩展的解决方案。
根据经典设计模式书 《design patterns, elements of reusable oriented software》。它给出了几条面向对象的设计原则,我们先来看看其中三条:
1. Program to interface, no implementation.
2. Close for modification, open for extension.
3. Encapsulate what varies
根据这几条设计原则,我们一起探讨如何设计一个满足规则的方案,以下的代码基于java实现。首先是program to interface,因此我们先针对问题给出一个对象接口:
对实现了该接口的类,只要将字符串输入,它将返回输入字符串是否是合理的数字格式。根据规则3,封装可变的部分,那此处可变的是什么呢,那就是字符串的解析逻辑,整形有自己的解析逻辑,浮点数有自己的解析逻辑,科学计数有自己的解析逻辑,十六进制数有自己的解析逻辑,因此要将这些不同的解析算法封装起来,而不是全部柔和在一起,因此根据这个接口,可以分别派生出以下几个类:
IntegerValidate , HexValidate ,SienceFormatValidate ,FloatValidate
各种类型的解析算法分别实现在相应类里,这样就实现了原则3。这种做法有一个好处就是各司其职,逻辑清晰,同时由于封装导致局部化,某一处的修改不会影响到其他部位。假定上述的几个类都实现了,然后我们根据设计模式 Chain of responsibility, 即责任链模式,将他们用一个队列串起来,当要判断给定字符串时,只要将链表上的对象逐个取出,调用validate接口,只要有一个返回true, 那么字符串就是合法的,要不然字符串就非法。该设计模式又满足了第二条原则, 试想如果我们需要再添加对罗马数字的支持,我们只要再实现一个类, RoamNumberValidate, 然后将其加入链表即可,新功能的添加不会对系统造成负面影响。接下来我们看看相应代码实现.
NumberValidateTemplate 类是一个模板类,对应于书里称之为Template 模式,关于该模式我们以后详谈,该类的主要功能是对输入的字符串做预处理,我想,不需要多说,大家能看懂它的实现逻辑。接下来就是IntegerValidate 等算法实现类的代码:
这两个类的实现简单,无需太多精力可看懂,对于FloatValidate, 我们只要判断"."前后的内容能通过IntegerValidate即可,由此可见,通过面向对象的设计方法,容易实现代码的重用。
同理,对于SienceFormatValidate , 只用判断"e"的前半部分满足IntegerValidate, 或FloatValidate, 后半部分满足IntegerValidate即可,可见又是一次代码重用^_^:
接下来用一个链表将他们串联起来:
最后,我们看看它用起来多简单:
至此,整个方案就结束了,虽然代码量相比上个方案有所增加,但逻辑上更清晰,扩展性更强,要想增加对罗马数字的支持,再添加一个相应类的实现即可,这也就做到了Close for modification, Open for extension。 该方案很好的包容了题目所蕴含的模糊性,使用这个方案,pass的概率想必会大一些吧。
作者:陈屹
转载注明出处,谢谢
对开发而言,面试的过程,很多时候就是解算法题,给你一块白板,一支笔,你在白板上写下面试者给你的算法题解。在此,我向大家推荐一个面试算法题网站leetcode oj(https://oj.leetcode.com/),该网站收集了个大著名软件公司的算法题,正在找工作的朋友不妨上去看看。算法面试大概分两部分,一是要做出来,二是要说清楚。很多失败的情况,主要是倒在第二条。说不清楚,除了表达能力外,很重要的一个原因就是算法的设计不不清晰,软件是一个逻辑复杂聚合体,如果做的不清楚,想讲清楚,不太可能,接下来,借用leetcode的一道算法题,讨论一下如何利用面向对象的分析和设计模式给出清晰设计方案。
Validate if a given string is numeric.
Some examples:
"0" => true
" 0.1 " => true
"abc" => false
"1 a" => false
"2e10" => true
Note: It is intended for the problem statement to be ambiguous. You should gather all requirements up front before implementing one.
(https://oj.leetcode.com/problems/valid-number/)
这道题目初看上去很简单,如果你拿到后直接动手,恐怕就危险了,如同它提示所说的,题目一个隐含目的就是模糊性,如果你不对题目仔细分析,并对面试官提出一些减少模糊性的问题就直接动手,那你很可能就已经被pass了。题目要求设计算法,判断字符串是否是数字,数字格式合法性的情况太多了,除了题目给定的例子外,还有很多情况需要搞清楚,例如“00123”是否合法, ".123", "123." 是否是合法的浮点数。而且我们不要以为只要判断给定字符串符合例子格式就可以了,同时还要考虑十六进制格式“0Xabc"是否也是合法的数字格式,罗马数字”I, II, IV"等是否也是合理格式。这道题不但语义模糊,而且具备极高的开放性,扩展性,据说在leetcode上,对该题的测试用例就有1400多个,因此题目看起来简单,但隐藏着很多的陷阱,所以它在leetcode里难度被标记为"Hard". 我们先看看leetcode里一位朋友给的c++解法:
class Solution {public: // can be also solved by DFA bool isNumber(const char *s) { bool has_dot = false, has_e = false, has_num = false; while (*s == ' ') s++; // filter prefix ' ' if (*s == '-' || *s == '+') s++; // filter operator '+' or '-' while (*s && *s != ' ') { if ((*s == 'e' || *s == 'E') && !has_e) { // filter 'e' has_e = has_dot = true; if (!has_num) return false; // there should be a number before 'e' if (*(s + 1) == '-' || *(s + 1) == '+') s++; if (!isdigit(*(s + 1))) return false; } else if (*s == '.' && !has_dot) has_dot = true; else if (isdigit(*s)) has_num = true; else return false; s++; } while (*s) if (*s++ != ' ') return false; // filter suffix ' ' return has_num; }};
这是一段简洁的应该是通过了测试的c++解决方案,这段代码是“对”的,但是可能会有一些问题,
一,它的表述性不强,除了作者外,其他人要容易的看懂,估计不容易(其他人也包括面试官)。
二,代码中有若干注释,大家如果读过Code Complete, Clean Code 等编程大牛写的书籍,则会意识到,好的代码是不需要注释的,当你需要注释才能解释代码的时候,很可能意味着你的设计不清楚,你的代码有code smell。
第三,将算法逻辑通过很多if else语句柔和起来,调试会复杂,修改一处逻辑很容易引起新的bug。
第四,很难扩展,该解法只处理了问题中给的几个例子,很显然,合理数字不止这三种,如果要添加新的判断功能的话,例如判断十六进制数,那将是困难重重,添加后接下来就是找bug了。如果要是这是面试给出的答案,恐怕要通过的可能性会不高。接下来,我们看看,能否通过面向对象的方法和设计模式给出一个清晰的,可容错,可扩展的解决方案。
根据经典设计模式书 《design patterns, elements of reusable oriented software》。它给出了几条面向对象的设计原则,我们先来看看其中三条:
1. Program to interface, no implementation.
2. Close for modification, open for extension.
3. Encapsulate what varies
根据这几条设计原则,我们一起探讨如何设计一个满足规则的方案,以下的代码基于java实现。首先是program to interface,因此我们先针对问题给出一个对象接口:
interface NumberValidate { boolean validate(String s);}
对实现了该接口的类,只要将字符串输入,它将返回输入字符串是否是合理的数字格式。根据规则3,封装可变的部分,那此处可变的是什么呢,那就是字符串的解析逻辑,整形有自己的解析逻辑,浮点数有自己的解析逻辑,科学计数有自己的解析逻辑,十六进制数有自己的解析逻辑,因此要将这些不同的解析算法封装起来,而不是全部柔和在一起,因此根据这个接口,可以分别派生出以下几个类:
IntegerValidate , HexValidate ,SienceFormatValidate ,FloatValidate
各种类型的解析算法分别实现在相应类里,这样就实现了原则3。这种做法有一个好处就是各司其职,逻辑清晰,同时由于封装导致局部化,某一处的修改不会影响到其他部位。假定上述的几个类都实现了,然后我们根据设计模式 Chain of responsibility, 即责任链模式,将他们用一个队列串起来,当要判断给定字符串时,只要将链表上的对象逐个取出,调用validate接口,只要有一个返回true, 那么字符串就是合法的,要不然字符串就非法。该设计模式又满足了第二条原则, 试想如果我们需要再添加对罗马数字的支持,我们只要再实现一个类, RoamNumberValidate, 然后将其加入链表即可,新功能的添加不会对系统造成负面影响。接下来我们看看相应代码实现.
abstract class NumberValidateTemplate implements NumberValidate{public boolean validate(String s) { if (checkStringEmpty(s)) { return false; } s = checkAndProcessHeader(s); if (s.length() == 0) { return false; } return doValidate(s); } private boolean checkStringEmpty(String s) { if (s.equals("")) { return true; } return false; } private String checkAndProcessHeader(String value) { value = value.trim(); if (value.startsWith("+") || value.startsWith("-")) { value = value.substring(1); } return value; } protected abstract boolean doValidate(String s);}
NumberValidateTemplate 类是一个模板类,对应于书里称之为Template 模式,关于该模式我们以后详谈,该类的主要功能是对输入的字符串做预处理,我想,不需要多说,大家能看懂它的实现逻辑。接下来就是IntegerValidate 等算法实现类的代码:
class IntegerValidate extends NumberValidateTemplate{ protected boolean doValidate(String integer) { for (int i = 0; i < integer.length(); i++) { if(Character.isDigit(integer.charAt(i)) == false) { return false; } } return true; }}class HexValidate extends NumberValidateTemplate{ private char[] valids = new char[] {'a', 'b', 'c', 'd', 'e', 'f'}; protected boolean doValidate(String hex) { hex = hex.toLowerCase(); if (hex.startsWith("0x")) { hex = hex.substring(2); } else { return false; } for (int i = 0; i < hex.length(); i++) { if (Character.isDigit(hex.charAt(i)) != true && isValidChar(hex.charAt(i)) != true) { return false; } } return true; } private boolean isValidChar(char c) { for (int i = 0; i < valids.length; i++) { if (c == valids[i]) { return true; } } return false; }}
这两个类的实现简单,无需太多精力可看懂,对于FloatValidate, 我们只要判断"."前后的内容能通过IntegerValidate即可,由此可见,通过面向对象的设计方法,容易实现代码的重用。
class FloatValidate extends NumberValidateTemplate{ protected boolean doValidate(String floatVal) { int pos = floatVal.indexOf("."); if (pos == -1) { return false; } if (floatVal.length() == 1) { return false; } String first = floatVal.substring(0, pos); String second = floatVal.substring(pos + 1, floatVal.length()); if (checkFirstPart(first) == true && checkFirstPart(second) == true) { return true; } return false; } private boolean checkFirstPart(String first) { if (first.equals("") == false && checkPart(first) == false) { return false; } return true; } private boolean checkPart(String part) { if (Character.isDigit(part.charAt(0)) == false || Character.isDigit(part.charAt(part.length() - 1)) == false) { return false; } NumberValidate nv = new IntegerValidate(); if (nv.validate(part) == false) { return false; } return true; }}
同理,对于SienceFormatValidate , 只用判断"e"的前半部分满足IntegerValidate, 或FloatValidate, 后半部分满足IntegerValidate即可,可见又是一次代码重用^_^:
class SienceFormatValidate extends NumberValidateTemplate{protected boolean doValidate(String s) { s = s.toLowerCase(); int pos = s.indexOf("e"); if (pos == -1) { return false; } if (s.length() == 1) { return false; } String first = s.substring(0, pos); String second = s.substring(pos+1, s.length()); if (validatePartBeforeE(first) == false || validatePartAfterE(second) == false) { return false; } return true; } private boolean validatePartBeforeE(String first) { if (first.equals("") == true) { return false; } if (checkHeadAndEndForSpace(first) == false) { return false; } NumberValidate integerValidate = new IntegerValidate(); NumberValidate floatValidate = new FloatValidate(); if (integerValidate.validate(first) == false && floatValidate.validate(first) == false) { return false; } return true; }private boolean checkHeadAndEndForSpace(String part) { if (part.startsWith(" ") || part.endsWith(" ")) { return false; } return true; } private boolean validatePartAfterE(String second) { if (second.equals("") == true) { return false; } if (checkHeadAndEndForSpace(second) == false) { return false; } NumberValidate integerValidate = new IntegerValidate(); if (integerValidate.validate(second) == false) { return false; } return true; }}
接下来用一个链表将他们串联起来:
class NumberValidator implements NumberValidate { private ArrayList<NumberValidate> validators = new ArrayList<NumberValidate>(); public NumberValidator() { addValidators(); } private void addValidators() { NumberValidate nv = new IntegerValidate(); validators.add(nv); nv = new FloatValidate(); validators.add(nv); nv = new HexValidate(); validators.add(nv); nv = new SienceFormatValidate(); validators.add(nv); } @Override public boolean validate(String s) { for (NumberValidate nv : validators) { if (nv.validate(s) == true) { return true; } } return false; }}
最后,我们看看它用起来多简单:
public class Solution { public boolean isNumber(String s) { NumberValidate nv = new NumberValidator(); return nv.validate(s); }}
至此,整个方案就结束了,虽然代码量相比上个方案有所增加,但逻辑上更清晰,扩展性更强,要想增加对罗马数字的支持,再添加一个相应类的实现即可,这也就做到了Close for modification, Open for extension。 该方案很好的包容了题目所蕴含的模糊性,使用这个方案,pass的概率想必会大一些吧。
作者:陈屹
转载注明出处,谢谢
0 0
- 运用面向对象的分析与设计模式巧解面试算法题
- 运用面向对象的分析与设计模式巧解面试算法(二)
- 基于模式的面向对象分析与设计
- 面向对象的分析与设计
- 面向对象的分析与设计
- ICONIX--面向对象的分析与设计
- 面向对象的分析与设计
- 面向对象的分析与设计
- 面向对象分析与设计的意义
- 面向对象分析与设计的区别
- 面向对象的分析与设计1
- 面向对象的分析与设计OOAD
- 例解基于UML的面向对象分析与设计
- 例解基于UML的面向对象分析与设计
- 例解基于UML的面向对象分析与设计
- 例解基于UML的面向对象分析与设计
- 例解基于UML的面向对象分析与设计
- 例解基于UML的面向对象分析与设计
- MUI-页面初始化
- Boost Asio 网络编程 基本用法
- 黑马程序员——Java基础网络编程
- 通过Wifi调试Android程序
- Android思维导图
- 运用面向对象的分析与设计模式巧解面试算法题
- 从今天开始,把C++primer(第五版)的课后练习答案贴到我的博客里面
- 寒假集训.Multiplying by Rotation
- 初始化iframe数据,判断iframe加载是否完成的问题
- android 鼠标悬停状态下改变背景色(类似Button、listview item 的selector)
- Java4Android学习十三 - 多线程
- 开始写博客
- Web开发中最致命的8个小错误
- android 语言定制