写在前面
学习很难,克服惰性。每天学一点,不会的就少一点。
养成习惯很重要,先从点赞开始吧!关注[程序员之道],程序员之路不再迷茫
大厂光环,闪耀万里,谁不想拥有BAT的一段光环。
最近有位朋友参加阿里的视频面试,道哥觉得他的面试过程很神奇,因为全程都是围绕一个排序算法?而且这位同学获得了面试官的充分认可。我觉得很有趣,有必要分享出来。
下面请搬好板凳做好,中途不要走神离开哦,好,我们的故事开始了。
自我介绍开头
面试官:你好,请问是xxx同学吗?
候选人:是的。您好。(要礼貌一些,但也不能话太多)。
面试官:您好,我是阿里天猫事业部的,目前我们业务急需一个后端工程师,请问有兴趣吗?
候选人:我对阿里一直都很向往,平时使用淘宝天猫也比较多,对天猫的高并发等后台技术很感兴趣。(拍马屁的套路)。
面试官:好,那你先自我介绍一下吧。
候选人:好的,我是xxx。(此处省略50字…)
寒暄的地方,我们就写这么多吧,下面的才是正题。
排序算法
面试官:现在有这么个需求,有一个文件,里面都是整型数字,以分号分割,要求你写一个程序?你如何实现?
候选人:(内心状态:这个问题很简单啊,就是一个排序算法,我可以使用冒泡算法,快排、归并排序,特殊情况下也可以用桶排序,但一般来说比较典型的都是使用快速排序,外表表现的很淡定)额,首先将内容从文件中读出来,按照分号分割成一个一个的数字,放到内存中,然后可以根据数字规律,进行排序(典型的就是快排算法)。现在很多编程语言如java都提供了现成的排序算法,直接使用编程语言已有的排序算法对数组进行排序,然后再讲数据写回到文件中就行了。
面试官:(基本的思路和表达能力还是不错)那如果文件比较大呢?
候选人:如果文件比较大的话,那就很难一次性将文件全部加载到内存,就无法使用快排或其他排序算法。这个时候就要使用外部排序了。
面试官:如果文件特别大了,比如说100G,或者更大的情况怎么做呢?
候选人:如果文件比较大,一两百G的话,如果是简单的外部排序,处理可能有点慢,这时候为了利用 CPU 多核的优势,可以在外部排序的基础之上进行优化,使用多线程并发排序,有点像“单机版”的 MapReduce。
面试官:(哈哈,这个小伙子竟然还知道MapReduce,跟我正要招的大数据后端处理还是挺沾边的),为什么要使用MapReduce呢?
候选人:如果文件特别大,例如2TB 大小,即便是单机多线程排序,也比较慢。这个时候,可以使用真正的 MapReduce 框架,利用多机的处理能力,提高排序的效率。
光说不练可不行
面试官:听你前面的描述,基本思路是对的,那我们现在就简单写下代码吧,假设我们的计算机内存是16G的,你按你前面说的几种情况写一下大概得逻辑代码,能写出来吗?
候选人:额,可以的,等我十分钟。
在十分钟左右的过程中,候选人指尖如飞,键盘噼里啪啦的,面试官一直看着候选人共享的屏幕(额,这个小伙子还是不错的,思路清晰,写的代码也相对规范)
候选人:我基本写好了一个框架,具体的代码还没写完。
talk is cheap,show me the code!
public class SortRecord {private static final long GB_SIZE = 1024 * 1024 * 1024;public void sortFile(String path) {// 省略校验逻辑File file = new File(path);long fileSize = file.length();//0-14G,使用内存排序,使用快排if (fileSize < 14 * GB_SIZE) {quickSort(path);} else if (fileSize < 20 * GB_SIZE) { // 14GB-20GB,使用外部排序externalSort(path);} else if (fileSize < 100 * GB_SIZE) { // 20GB-100GB,使用并发的外部排序concurrentExternalSort(path);} else { // 100GB往上,使用并行处理的mapReduceSortmapReduceSort(path);}}private void quickSort(String filePath) {// 快速排序}private void externalSort(String filePath) {// 外部排序}private void concurrentExternalSort(String filePath) {// 多线程外部排序}private void mapReduceSort(String filePath) {// 利用MapReduce多机排序}public static void main(String[] args) {SortRecord sorter = new SortRecord();sorter.sortFile(args[0]);}
}
面试官:额额,基本的架子搭的还不错,完整的代码可以写出来吗?
候选人:(内心很为难啊,这个确实不太好写)可以的,但是需要一点时间。
面试官:你可以上网搜索,毕竟平时编程时也是可以上网的嘛,充分调动和利用身边的一切资源,去完成你要做的事情。最后把事情做成,这是我们很看重的一个能力。你稍后以附件的形式把完整代码分给我吧!
候选人:面带微笑,好的。
面试官:接下来,我们围绕你写的这段代码,再做一些深入的思考吧。作为大厂人,我们不能满足只把一件事做成,还要考虑做好。平时写代码也是一样,要考虑扩展性,如果这几个算法都比较简单,都写在一个类里没啥问题。考虑到代码的可读性,可扩展性,可维护性,你可以使用你平时用到或者学过的一些设计模式对上面的代码进行优化。
候选人:(你怎么说都是对喽,谁让你是面试官呢,虽然我内心觉得写的不错了呢),额,考虑到设计模式,可以针对接口编程,给我几分钟改一改。
简单的面对接口编程,这个可难不倒我!!!
过了几分钟,共享桌面的代码变成了这样:
public interface ISortRecord {void sort(String path);
}public class QuickSort implements ISortRecord {@Overridepublic void sort(String path) {}
}public class ExternalSort implements ISortRecord {@Overridepublic void sort(String path) {}
}public class ConcurrentExternalSort implements ISortRecord {@Overridepublic void sort(String path) {}
}public class MapReduceSort implements ISortRecord {@Overridepublic void sort(String path) {}
}public class SortRecord1 {private static final long GB_SIZE = 1024 * 1024 * 1024;public void sortFile(String path) {// 省略校验逻辑File file = new File(path);long fileSize = file.length();ISortRecord sortAlg;//0-14G,使用内存排序,使用快排if (fileSize < 14 * GB_SIZE) {sortAlg = new QuickSort();} else if (fileSize < 20 * GB_SIZE) { // 14GB-20GB,使用外部排序sortAlg = new ExternalSort();} else if (fileSize < 100 * GB_SIZE) { // 20GB-100GB,使用并发的外部排序sortAlg = new ConcurrentExternalSort();} else { // 100GB往上,使用并行处理的mapReduceSortsortAlg = new MapReduceSort();}sortAlg.sort(path);}public static void main(String[] args) {SortRecord1 sorter = new SortRecord1();sorter.sortFile(args[0]);}
}
面试官:面向接口编程,额,不错,可读性和可维护性都比之前提高了不少。但是针对排序算法,我们没有必要每次都新建一个对象吧,这部分,还可以优化么,举个例子,是不是可以使用工厂模式呢?你再思考思考。
候选人:(还要思考啊,倒也是,可以通过工厂模式对对象进行封装,这个面试官也太挑剔了点,但我还不能表现出来)嗯,说的很有道理,可以通过工厂模式再封装一下,我再改一下。
又过了五分钟。
候选人:可以增加一个创建工厂,然后修改一下主类SortRecord。新的代码变成了这样:
public class SortRecordFactory {private static final Map<SortTypeEnum, ISortRecord> map = new HashMap<>();static {map.put(SortTypeEnum.quick_sort, new QuickSort());map.put(SortTypeEnum.external_sort, new ExternalSort());map.put(SortTypeEnum.concurrent_external_sort, new ConcurrentExternalSort());map.put(SortTypeEnum.map_reduce_sort, new MapReduceSort());}public static ISortRecord getSortAlg(SortTypeEnum type) {if (type == null) {throw new IllegalArgumentException("type can not be null");}return map.get(type);}
}public class SortRecord2 {private static final long GB_SIZE = 1024 * 1024 * 1024;public void sortFile(String path) {// 省略校验逻辑File file = new File(path);long fileSize = file.length();ISortRecord sortAlg;//0-14G,使用内存排序,使用快排if (fileSize < 14 * GB_SIZE) {sortAlg = SortRecordFactory.getSortAlg(SortTypeEnum.quick_sort);} else if (fileSize < 20 * GB_SIZE) { // 14GB-20GB,使用外部排序sortAlg = SortRecordFactory.getSortAlg(SortTypeEnum.external_sort);} else if (fileSize < 100 * GB_SIZE) { // 20GB-100GB,使用并发的外部排序sortAlg = SortRecordFactory.getSortAlg(SortTypeEnum.concurrent_external_sort);} else { // 100GB往上,使用并行处理的mapReduceSortsortAlg = SortRecordFactory.getSortAlg(SortTypeEnum.map_reduce_sort);}sortAlg.sort(path);}public static void main(String[] args) {SortRecord2 sorter = new SortRecord2();sorter.sortFile(args[0]);}
}
面试官:(露出了赞许的目光,这个小伙子很不错,反应比较快,我再来考考他)这么写吧,我看着还是有点不太爽,if-else分支太多了,如果以后我又增加了其他的排序方式,还得增加,这个能继续优化吗?
候选人:(还好系统的准备过设计模式相关的知识,不然今天真是歇菜了,这个面试官也是醉了,竟然抓住一道题不放了,照这么下去,不得面好几个小时啊,内心一万个崩溃…硬着头皮,也得改啊,想了一两分钟,哈,有了个思路)这个可以借助一个辅助类,初始化一个区间,及对应的算法。然后判断文件大小在哪个区间内,则使用哪个算法。
面试官:嘿嘿,既然你有了思路,那也可以写出来吧。
候选人:(⊙o⊙)…,应该可以的,我试试。
键盘响的飞起。
又过了几分钟
面试官一直在注视着候选人共享的屏幕,通过屏幕键盘输入的字符,以及候选人是否有切换过窗口,判断候选人真正的是在自己思考,还是在上网查找答案。
候选人:写好了,您再看看。
public class SortRecord3 {private static final long GB_SIZE = 1024 * 1024 * 1024;private List<SortRange> algList = new ArrayList<>();public SortRecord3() {algList.add(new SortRange(0, 14 * GB_SIZE, SortRecordFactory.getSortAlg(SortTypeEnum.quick_sort)));algList.add(new SortRange(14 * GB_SIZE, 20 * GB_SIZE, SortRecordFactory.getSortAlg(SortTypeEnum.external_sort)));algList.add(new SortRange(20 * GB_SIZE, 100 * GB_SIZE, SortRecordFactory.getSortAlg(SortTypeEnum.concurrent_external_sort)));algList.add(new SortRange(100 * GB_SIZE, Long.MAX_VALUE, SortRecordFactory.getSortAlg(SortTypeEnum.map_reduce_sort)));}public void sortFile(String path) {// 省略校验逻辑File file = new File(path);long fileSize = file.length();ISortRecord sortAlg = null;for (SortRange sortRange : algList) {if (sortRange.inRange(fileSize)) {sortAlg = sortRange.getSortAlg();break;}}if (sortAlg != null) {sortAlg.sort(path);} else {throw new IllegalArgumentException("according to file size ,can not find sort method");}}private class SortRange {private long start;private long end;private ISortRecord sortRecord;public SortRange(long start, long end, ISortRecord sortRecord) {this.start = start;this.end = end;this.sortRecord = sortRecord;}public ISortRecord getSortAlg() {return this.sortRecord;}public boolean inRange(long fileSize) {return fileSize >= start && fileSize < end;}}public static void main(String[] args) {SortRecord3 sorter = new SortRecord3();sorter.sortFile(args[0]);}}
面试官:(内心有些赞许,这个小伙子基础比较扎实,皮实,反应比较快,具有一定自省精神,符合我们的招聘原则,虽然都是围绕一道题目,但能看出他挺深的功底,再简单聊聊,我这里基本是可以过关了)额嗯,可以,这样写,基本上看起来就比较优雅了,这叫什么设计模式呢?
候选人:(设计模式,哪一种呢,单例、工厂模式、责任链模式、模板模式,各种设计模式及定义在脑中飞速飞过,但又都对不上)这一说,我还真有点忘了,额,想起来了,应该是策略模式。
面试官:嗯嗯,是的,当我们再增加代码时,基本上只有修改策略工厂类及SortRecord的构造函数。但这还谈不上完美,因为我还需要修改代码,不符合开闭原则,你有什么思路,可以更加灵活吗?
候选人:(求放过啊,还有完没完了,要说到更加灵活,那基本上就是配置大于约定,读取配置文件,还有通过java反射的一些机制了,这些再让我写代码,可基本就露馅了)可以通过配置文件,类动态读取配置文件,然后通过反射动态加载对应的类,创建策略对象。当新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件即可。将文件大小区间和算法之间的对应关系放到配置文件中。当添加新的排序算法时,我们只需要改动配置文件即可,不需要改动代码。
面试官:嗯,基本回答到了要点,这个就不要求你写代码了。在我们这就是这样的,一定要皮实,而且要懂得不断思考,思考业务的发展,思考代码的不足,思考架构的缺陷,思考自身有何可努力改进的。时间差不多了,你有什么问我的吗?
候选人:(已经满头大汗,汗流浃背了。总算到了“你有什么问我的这一步了”,不要问些天马星空的东西,要问一些跟业务,跟自身发展有关系,要显得自己很主动,很想去)请问我们这个组具体做什么,在里面可以得到哪些锻炼。
面试官:(此处略过,没有太大意义)&&^%$$#@!@!!#@@@@。好,耽误了你不少时间,今天就到这吧,谢谢你。等待下一位面试官通知面试吧。
候选人:(面试官很有礼貌啊,这意思,这一面应该是过了)好的,谢谢面试官。
写在后面
关于具体的快排,外部排序的完整代码,及面试后记,正在整理中。会在博客和公众号更新,公众号更新的可能更及时一些。关注公众号疯狂催更吧!
坚持和习惯是学习中的两大绊脚石,先从养成点赞和评论开始吧,😁!