运用面向对象的分析与设计模式巧解面试算法题

来源:互联网 发布:电脑破解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++解法:


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
原创粉丝点击