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

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的概率想必会大一些吧。




 作者:陈屹


转载注明出处,谢谢

你可能感兴趣的:(设计模式,算法,面向对象,面试)