二分查找相关问题总结

参考二分查找各种情况大总结

二分查找原始版

在有序数组中查找某个数,找到返回数的下标,存在多个返回任意一个即可,没有返回-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

posted on 2019-03-18 10:17  Frank_Allen  阅读(316)  评论(0编辑  收藏  举报

导航