面试手写二分法查找,十个有八个死在边界上:while条件写l小于r还是小于等于r,返回-1还是返回left。先把规则定死,代码一次就能写对。
前提和原理
二分法查找要求数组有序,这是前提。每次取中间元素和目标比较,中间值大了丢掉右半区,小了丢掉左半区,查找范围一次砍一半。100万个有序元素最多比较20次,时间复杂度O(log n),这就是它比顺序查找快的原因。数据基本有序、偶尔插入删除的场合实际表现也说得过去;完全乱序就先排序再查,或者换哈希表。
一份能直接跑的写法,按闭区间约定:
function bsearch(a,t){var l=0,r=a.length-1;while(l<=r){var m=l+((r-l)>>1);if(a[m]===t){return m;}if(a[m]<t){l=m+1;}else{r=m-1;}}return -1;}两个经典的坑
一个是中点计算:(l+r)/2在Java和C里两个int相加可能溢出,写成l+(r-l)/2就安全;JavaScript的数字是浮点实现,日常数组到不了那个量级,但养成习惯没坏处。另一个是区间约定混用:闭区间配while(l<=r)和r=m-1,左闭右开配while(l<r)和r=m,两套约定挑一套用到底,混着写必出错。还有个隐蔽坑是重复元素:标准二分命中就返回,数组里有重复值时返回哪个位置说不好,业务上要稳定就得用边界变体。
变体和适用场景
找目标首次出现和末次出现的位置,命中后别急着return,继续往两边收缩边界,这是高频面试变体;再进一步是找大于等于目标的最小位置,把相等判断改成大于等于就行。链表没法随机访问,用不上二分。数据就几十个元素,顺序遍历更省事,别为了炫技过度设计。
写完拿三个用例测一遍:空数组、单元素数组、目标值在头尾。这几个过了,边界基本就稳了。