二分查找的前提条件是:数组是有序的。
一、基础版(左闭右闭):
待查找值target在[left , right]区间中,所以左右指针不仅表示查找边界,也表示指向的元素可能参与运算。
1)确定左右指针初始值:i=0,j=arr.length-1。
2)while(i<=j):“=”是多考虑一次,即当指针i=j=m指向同一元素
3)中间索引:m=(i+j)/2,此处Java是默认向下取整;但当j取值很大,即Interger.Max_Value-1时,第二次运算可能出错;因此,我们采用无符号运算:右移一位>>>1,这样既可以避免索引为负值,也可以保证向下取整。
3)判断:①arr[m]<target,则i=m+1;表示查找值在右半区域,故左指针后移
②target<arr[m],则j=m-1;表示查找值在左半区域,故右指针前移
③target=arr[m],则return m;返回查找值的索引
4)当i>j时,跳出循环,返回-1;
java中的binarySearch采用基础版,返回值:-(插入点-+1)
二、改动版(左闭右开):
待查找值target在[left , right)区间中,左指针i与基础版的含义一样;不过右指针j此时仅仅表示查找边界,指向的元素一定不是查找目标。
1)确定左右指针初始值:i=0,j=arr.length。
2)while(i<j):此时不能考虑“=”的情况,否则可能陷入死循环
3)中间索引:m=(i+j)>>>1;
3)判断:①arr[m]<target,则i=m+1;表示查找值在右半区域,故左指针后移
②target<arr[m],则j=m;//表示查找值在左半区域,故右指针前移,其指向的m索引在上一次循环已经判断过,不需要再次判断
③target=arr[m],则return m;返回查找值的索引
4)当i>j时,跳出循环,返回-1;
时间复杂度:O(log(n));空间复杂度:O(n)
上面两种方法:当目标值在左、右两边进行比较的次数不均一,最好情况和最坏情况的所需时间差距较大;因此基于此,在改动版的基础上进行改进。
三、平衡版(左闭右开):
特点:①不在循环内找target,循环只是缩小范围,当范围内只剩下i时,退出循环,在循环外比较arr[i]与target
②循环内的平均比较次数减少了
缺点:原本二分法最好情况时,时间复杂度为O(1);而平衡板最好和最坏情况的时间复杂度都是log(n)
1)确定左右指针初始值:i=0,j=arr.length。
2)while(1<j-i):此时不能考虑“=”的情况,否则可能陷入死循环
3)中间索引:m=(i+j)>>>1;
3)判断:if(target<arr[m]){j=m;}//表示查找值在左半区域,故右指针前移,其指向的m索引在上一次循环已经判断过,不需要再次判断
else{i=m;}//表示查找值在右半区域,故左指针后移
4)跳出循环后,if(arr[m]==target) {return i;}else{return -1;},则return m;返回查找值的索引
以上版本适合数组中没有重复元素的情况,如果有重复元素,那么返回的索引就不是唯一的。当出现重复元素时,我们就可以在基础版上进行改动分别找到最左侧或是最右侧的值。
四、LeftMost:找到重复元素中最左侧的值
1)在while循环前,定义一个变量临时记录目标值
2)当target=arr[m]时,用变量candidate=m,然后继续向左找,缩小边界j=m-1;
3)循环结束后,返回candidate,当没找到时返回初始值-1。
五、RightMost:找到重复元素中最右侧的值
1)在while循环前,定义一个变量临时记录目标值
2)当target=arr[m]时,用变量candidate=m,然后继续向右找,缩小边界i=m+1;
3)循环结束后,返回candidate,当没找到时返回初始值-1。
利用LeftMost和RightMost可以找到前任--LeftMost(i)-1、后任--RightMost(i)+1、最近邻居、排名、范围查询。