二分查找原始版
在有序数组中查找某个数,找到返回数的下标,存在多个返回任意一个即可,没有返回-1。所有程序采用左右均为闭区间,即函数中n为最后一个元素下标,而不是元素个数。典型代码如下:
public int binarySearch(int[] a, int n, int key){
//n + 1 个数
int low = 0;
int high = n;
int mid = 0;
while(low <= high) {
mid = low + ((high-low) >> 1);
if(key == a[mid]) {
return mid;
} else if(key < a[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
注意这里用到的是后面要总结的左闭右闭的方式进行的查找,如何看是左闭右开还是左闭右闭的查找方式呢,首先high和low的移动方式是判断的先绝条件,对于左闭右闭的方式low和high移动时一定时mid+1或者mid-1,但是对于左闭右开的方式,high移动时一定是mid而不做加减,这是因为high移动时我们不能确定target的前一个位置的元素一定时不属于查找区间的。顺便说一下:如果时左闭右闭的那么high初始化只能为length-1,但是如果是左闭右开的那么high必须初始化为length。
查找第一个大于等于某个数的下标
例:int[] a = {1,2,2,2,4,8,10},查找2,返回第一个2的下标1;查找3,返回4的下标4;查找4,返回4的下标4。如果没有大于等于key的元素,返回-1。下面是代码,改动只有两处:
public int firstGreatOrEqual(int[] a, int n, int key){
//n + 1 个数
int low = 0;
int high = n;
int mid = 0;
while(low <= high) {
mid = low + ((high-low) >> 1);
if(key <= a[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return low <= n ? low : -1;
}
解释:
1、条件为key<=a[mid],意思是key小于等于中间值,则往左半区域查找。如在 {1,2,2,2,4,8,10}查找2,第一步,low=0, high=6, 得mid=3, key <= a[3],往下标{1,2,2}中继续查找。
2、终止前一步为: low=high,得mid = low,此时如果key <= a[mid],则high会改变,而low指向当前元素,即为满足要求的元素。如果key > a[mid],则low会改变,而low指向mid下一个元素。
3、如果key大于数组最后一个元素,low最后变为n+1,即没有元素大于key,需要返回 -1。
查找第一个大于某个数的下标
例:int[] a = {1,2,2,2,4,8,10},查找2,返回4的下标4;查找3,返回4的下标4;查找4,返回8的下标5。如果没有大于key的元素,返回-1。
如下是代码,与上面大于等于某个数仅判断一个符号不同:
public int firstGreat(int[] a, int n, int key){
//n + 1 个数
int low = 0;
int high = n;
int mid = 0;
while(low <= high) {
mid = low + ((high-low) >> 1);
if(key < a[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return low <= n ? low : -1;
}
上面的基础形式可以有下面的集中扩展:
(1) 查找数组中某个数的位置的最小下标
可以先查找数组中第一个大于等于某个数的位置,然后做一个判断,如果等于这个数直接返回下标,否则就返回-1
(2) 查找数组中某个数的位置的最大下标
可以先查找数组中第一个大于这个数的位置,然后将这个数和这个位置上的前一数进行比较,如果相等返回前一个位置,否则就返回-1。
(3) 查找数组中小于某个数的最大下标
可以先查找第一个大于等于这个数的下标,如果前一个位置有效,返回前一个位置,否则返回-1
(4) 查找数组中某个数的出现次数
首先用(1)求下界,然后用(2) 求上界,如果上界和下界都存在二者的差值+1就是出现的次数
参考 二分查找学习札记
二分查找的边界控制
二分查找算法的边界,一般来说分两种情况,一种是左闭右开区间,类似于[left, right),一种是左闭右闭区间,类似于[left, right].需要注意的是, 循环体外的初始化条件,与循环体内的迭代步骤, 都必须遵守一致的区间规则,也就是说,如果循环体初始化时,是以左闭右开区间为边界的,那么循环体内部的迭代也应该如此.如果两者不一致,会造成程序的错误.
# 两种闭合方式程序的对比
def left_in_right_in(arr, length, target):
lo, hi = 0,length-1
while lo<=hi:
mid = lo + (hi-lo)>>1
if arr[mid] in the left of target interval:
lo = mid+1
elif arr[mid] in the right of target interval:
hi = mid-1
else: # arr[mid] in the target interval
return mid
return # 只能找到或者是找不到
def left_in_right_out(arr, length, target):
lo , hi = 0,length
while lo < hi:
mid = lo + (hi-lo)>>1
if arr[mid] in the left of target intervel:
lo = mid+1
elif arr[mid] in the right of target interval:
hi = mid
else: # arr[mid] in the target interval
hi = mid
return {
lo: 'the left margin of the target interval'
hi: 'it must equal to lo, its meaning equal to lo'
}
现在来总结一条规律:
两种闭合方式需要仔细鉴别,如果使用左闭右闭的方式那么循环跳出的时机一定是lo>hi的时侯,如果使用左闭右开的方式那么循环跳出的时机一定是lo=hi的时侯,这里可以通过数学上当搜索区间中没有元素的时机条件来辅助理解。
理解两种闭合方式的等价性
# 搜索第一个等于target的元素的位置
def left_in_right_in(arr, length, target):
lo, hi = 0,length-1
while lo<=hi:
mid = lo + ((hi-lo)>>1)
if arr[mid] < target:
lo = mid+1
elif arr[mid] > target:
hi = mid-1
else: # arr[mid] in the target interval
hi = mid-1
return lo if lo<length and arr[lo]==target else -1
def left_in_right_out(arr, length, target):
lo , hi = 0,length
while lo < hi:
mid = lo + ((hi-lo)>>1)
if arr[mid] < target:
lo = mid+1
elif arr[mid] > target:
hi = mid
else: # arr[mid] in the target interval
hi = mid
return lo if lo < length and arr[lo]==target else -1
size = 50
import random
for i in range(100000):
arr_length = int(random.random()*(size+1))
arr = []
for i in range(arr_length):
arr.append(random.randint(0,100))
target = random.randint(0,1000)
arr.sort() # 所有二分查找的变体都要求有序性
a=left_in_right_in(arr,arr_length,target)
b=left_in_right_out(arr,arr_length,target)
if a!=b :
print(arr,target)
print(a,b)
break