前言
一个优秀的程序员具备挺多特质的,比如好奇心,学习能力等,但在我看来一个优秀的程序员必须具备四项核心能力,哪四项,先卖个关子,程序员最喜欢说的话是「TalkisCheap,showmeyourcode」,那我们先来看一道很常见的面试题
如何快速定位IP对应的省份地址?
我们知道,每个省市都分配了一个ip段,如下
[...0,...]山东东营市[...0,...]山东烟台[...34,...]山东青岛[..48.0,..48.]江苏宿迁[..49.15,..51.]江苏泰州[..56.0,..56.]江苏连云港
输入一个ip地址怎么做到秒级定位此ip所在的省市呢?
如图示:在百度上输入一个ip地址,能做到秒级展示其所属地,怎么做到的呢,背后用到了什么原理
这就引入了我们要谈的程序员需要具备的第一项能力:抽象问题或者说数据建模的能力
抽象问题的能力
所谓抽象问题或者说数据建模的能力,既能把一个问题抽象或归类为某种方案来解决,比如要实现负载均衡,会想到一致性哈希算法,要实现最短路径,想到使用动态规划,微服务下要保证服务可用引入降级机制等等,一句话就是把具体的问题抽象成到解决此问题背后的方法论,进而用相关的技术方案得以解决。
回归到如何快速定位IP对应的省份地址这道题来看,如果我们不具备抽象问题的能力,硬着头皮从头到尾把输入的ip与所有区间段的ip都遍历对比一遍,然后判断它落到哪个区间,那么ip地址有32位,共有2^32个,约有42.9亿个,用暴力遍历法每查找一个ip最坏情况下要遍历约42亿次,这种方法显然是不可行的。
所以我们必须得把这个问题抽象为另一种可行的方法,即:二分查找,ip地址查找怎么就跟二分查找扯上关系了,背后的逻辑是什么,我们一起来看看。
ip地址不容易比较,那我们首先把ip地址转成整数,于是每个省市对应的ip地址区间就变成了整数区间,假设为如下区间
[1,5][11,15][16,20][6,10]....
再以每个整数区间的起始数字对这些区间进行排序,排序后的区间如下
看到这些排序后的区间,想到了啥,二分查找就是在一组有序的数字中进行查找!是不是找到相似点了?
这里给没听过二分查找的读者简单普及下啥是二分查找,小时候可能我们都玩过猜字游戏,在纸面上写一个1到的数字,比如70,让对方猜,怎样猜才能猜最快。
首先猜1和的中间数字(1+)/2=50(取整),于是我们继续猜50和的中间数字(50+)/2=,于是我们继续猜50和75的中间数字(50+75)/2=62依次持续类似以上的步骤,不断地缩小范围,直至找到70
总共只猜了7次,比起我们从1猜到效率高了十几倍,如果被猜字的范围从一扩大到成百上千万,提升的效率是指数级的!二分查找也叫折半查找(注意上文中加粗的中间数字),仔细看上图,每查找一次,问题规模缩小一半,整体时间复杂度是O(logn),即使我们要在42亿的数字中查找数字,最多也只要查32次,所以采用二分查找对查找性能的提升无疑是巨大的!
二分查找是要在一堆有序的数字中精准地查找所要查找的数是否存在,而回过头来看已经排序好的以下ip段
我们要查找的是某个整数是否在一个有序数组的相邻两个数字的区间里,例如:取这些ip区间的起始地址组成一个数组(1,6,11,16,....)(有序数组),如果我们要找的ip对应的整形为14,由于它在[11,16)(11是闭区间,16是开区间)之间,所以这个ip就落在[11,15]这个ip区间,这样就找到了这个ip对应的省市了。
所以就由二分查找某个值是否存在转变成了查找某个值是否在有序数组中相邻的两个值之间了,这就引入了程序员要具备的第二层能力:举一反三或者说修改模型的能力
修改模型的能力
就像机器学习,现在其实有很多现成的模型可用,比如识别物的模型等等,我们需要的话可以直接拿来用,但是现有模型的准确率可能不是那么理想(比如只有80%),如果我们需要进一步地提升识别准确率,可能就需要对其参数进行进一步的调优,以进一步地优化模型,达到我们预期的值。
再比如当当网基于Dubbo的扩展版本开发的Dubbox也是由于原来的Dubbo功能不满足其团队需求而在其基础上修改扩展的。
回过头来看以上说的原来二分查找只是查找某个值是否存在,而我们现在要解决的问题是查找某个值是否在相邻的两个值之间,这本质是也是对模型的调优或修改,以进一步满足我们的要求。于是我们写下了如下代码
publicstaticintbsearch(int[]a,intlength,intvalue){intlow=0;inthigh=length-1;while(low=high){intmid=(low+high)/2;if(a[mid]value){if(mid==0){return-1;}if(a[mid-1]=value){returnmid-1;}else{high=mid-1;}}else{low=mid+1;}}return-1;}
那这段代码有啥问题吗,或者说有哪些可以优化的空间,这就引入了程序员需要具备的第三项能力:代码要有足够的健壮性
代码要有足够的健壮性
仔细看上文的代码,有两个地方有潜在隐患,一个是length可能是负数,而显然数组的长度不可能是负数,也就是说对这种异常数据应该抛异常。另外(low+higth)/2这段代码中的low+high如果在数组很大的情况下比较容易造成溢出,所以可以改造成low+(high-low)/2,另外为了提升性能可以把除以2改成位运算,即low+((high-low)1),于是代码变成了
publicstaticintbsearch(int[]a,intlength,intvalue)throwsException{if(length0){//实际应该抛出一个继续自Exception的异常,这里为了方便直接抛出ExceptionthrownewException("数据长度不合法");}intlow=0;int1;whileintmid=low+((high-low)1);ifif(mid==0){return-1;}if(a[mid-1returnmid-1;}else{high=mid-1;}}else{low=mid+1;}}return-1;}
有人可能觉得判断数组长度小于0过于严苛了,但是是人就会犯错误,这里也是为了强调我们对异常情况的处理要到位,说到代码的健壮性,这里再多说几句,在创业初期我司主要用的是php,主要是创业团队追求快,用PHP这种弱类型语言开发确实效率高,不过不安全,线上多次出现因为变量可以随意赋值造成的多次线上故障,而Java这种强类型语言虽然开发效率上比PHP慢了不少,但强类型语言的特征保证了它的稳定,足够安全,所以后期随着人员的扩充,为了保证线上足够安全,我司去年把大部分的服务都Java化了,近年来有不少人唱衰Java,但Java的安全,稳定性以及强大的生态能力注定了它的长久生命力。
代码写成这样看起来确实完美了,还能再优化吗,注意上文中的代码只适用于int的数组,如果用二分查找法进行区间查找具有通用性,比如我们想针对short或long型等类型的数组进行查找就无能为力了,所以这就引入了程序员需要具备的第四项能力:代码要有足够的可扩展性
代码要有足够的可扩展性
怎么让bsearch这个二分查找也支持long型或short型数组呢,Java支持重载,再针对bsearch进行多个函数的重载是一种方式,不过会造成代码的大量冗余,所以另一种更合适的方式是利用Java语言中的泛型,于是我们的代码改造如下
publicstaticTextendsComparableintbsearch(T[]a,intlength,Tvalue)throwsException{if0){//实际应该抛出一个继承自Exception的异常,这里为了方便直接抛出ExceptionthrownewException();}intlow=0;int1;whileint1);if(a[mid].