数据结构与算法(十四):二分查找问题之变形

二分查找问题的简单形式是:直接查找元素值等于指定值的元素。

然而,实际应用中,会有各种各样的变形问题:

  1. 存在重复元素值,查找第一个值等于指定值的元素;
  2. 存在重复元素值,查找最后一个值等于指定值的元素;
  3. 查找第一个大于等于指定值的元素;
  4. 查找最后一个小于等于指定值的元素;

对于变形的二分查找问题,基本思路与简单情况类似,所不同的是需要考虑更复杂的边界情况的处理。

变形1:存在重复元素值,查找第一个值等于指定值的元素

比如下面这样一个有序数组,其中,a[5],a[6],a[7]的值都等于 8,是重复的数据。我们希望查找第一个等于 8 的数据,也就是下标是 5 的元素。

解题思路:

1、和一般二分查找相同,先设定low/high指针,分别指向下标0和9;

2、求解mid指针的下标:mid=low+((high-low)>>1),

3、根据a[mid]与指定值value的关系,选择更新high指针或者low指针,直到low>high停止。

先上码,再解释:

def bisearch_var1_1(A, value):
    low = 0
    high = len(A)-1
    
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] > value:
            high = mid - 1
        elif A[mid] < value:
            low = mid + 1
        elif A[mid] == value:
            if mid == 0 or A[mid-1] != value:
                return mid
            else:
                high = mid - 1
    
    return -1

def bisearch_var1_2(A, value):
    low = 0
    high = len(A)-1
    
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] >= value:
            high = mid - 1
        elif A[mid] < value:
            low = mid + 1
    
    if low < len(A) and A[low]==value:
        return low
    else:
        return -1

这里给出了两种方案:

第一种方案——bisearch_var1_1:将a[mid]与指定值value的关系分为三个:大于、小于、等于。大于和小于的情况比较清晰,就不多说了。对于等于的情况,需要再作判断:当mid为0时说明在A[mid] == value情况下,mid是A中第一个元素,那当然也是以第一个等于value的元素;当A[mid-1] != value时,这个时候说明了mid位置的元素等于value但它前一个元素不等于value,由于A有序,那A[mid-1]一定是小于value的,此时mid也就是满足要求的第一个值等于value的元素的下标;因此,满足这两个条件之一即可返回结果,若都不满足,则说明了这样一种情况:A[mid]不是第一个等于value的元素,它前面还有其他元素也等于value,所以应该更新high指针,将搜索范围向low的方向缩小。

第二种方案——bisearch_var1_2:将a[mid]与指定值value的关系分为两个个:大于等于、小于。大于等于的时候,说明当前程序还不能确定当前元素A[mid]是第一个等于value的元素,所以更新high指针;小于的时候,说明还没找到等于value的元素,所以向high的方向更新low指针。这样经过while循环之后,low、high指针的情况就变成了这样几种情况:A中有值等于value的情况——A[low]==value,high=low-1,A[high]<value;A中没有值等于value的元素,且A[0]<value<A[n-1]的情况(n为A的长度)——A[low]>value, high = low-1, A[high]<value;A中没有值等于value的元素,且value>A[n-1]的情况(n为A的长度)—— high = n-1, low = n,  A[high]<value; A中没有值等于value的元素,且value<A[0]的情况(n为A的长度)—— high = -1, low = 0,  A[low]<value。可见,经过while后,几种情况都有一个共性:low=high+1。然后,再进行一次判断:low < len(A)且A[low]==value,说明找到值为value的元素且为第一个元素,否则就说明没找到,直接返回-1 。

针对这两种方案,简单测试下:

A = [1,2,3,3,3,4,4,5,6]
print(bisearch_var1_1(A,4)) # result: 5
print(bisearch_var1_2(A,0)) # result: -1

变形2:存在重复元素值,查找最后一个值等于指定值的元素

解题思路与变形1相同,只是在边界条件的判断上有所不同,对于各个情况的分析也类似变形1。废话少说,直接上码:

def bisearch_var2_1(A, value):
    low = 0
    high = len(A)-1
    while low <= high:
        mid = low +((high-low)>>1)
        if A[mid] > value:
            high = mid -1
        elif A[mid] < value:
            low = mid + 1
        elif A[mid] == value:
            if mid == len(A)-1 or A[mid+1] != value:
                return mid
            else:
                low = mid + 1
    return -1

def bisearch_var2_2(A, value):
    low = 0
    high = len(A)-1
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] > value:
            high = mid - 1
        elif A[mid] <= value:
            low = mid + 1
    if high >=0 and A[high]==value:
        return high
    else:
        return -1

A = [1,2,3,3,3,4,4,5,6]
print(bisearch_var2_1(A,3)) # result: 4
print(bisearch_var2_2(A,0)) # result: -1

变形3:查找第一个大于等于指定值的元素

前面的两种情况,都是希望能找到等于指定值的元素,而这里则希望找到第一个大于指定值的元素,所以在边界判断时有所不同,先给出两种解决方案的代码:

def bisearch_var3_1(A, value):
    low = 0
    high = len(A)-1
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] >= value:
            if mid == 0 or A[mid-1] < value:
                return mid
            else:
                high = mid - 1
        elif A[mid] < value:
            low = mid + 1
    return -1

def bisearch_var3_2(A, value):
    low = 0
    high = len(A)-1
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] >= value:
            high = mid - 1
        elif A[mid] < value:
            low = mid + 1
    if low < len(A):
        return low
    else:
        return -1

A = [1,2,3,3,3,4,4,5,6]
print(bisearch_var3_1(A,0.5)) # result: 0
print(bisearch_var3_2(A,0.5)) # result: 0

对于第一种方案——bisearch_var3_1:将A[mid]与value值的关系分两种情况:大于等于、小于。当大于等于时,需要判断如果A[mid]为第一个元素(也即mid==0),或者mid的前一个元素A[mid-1]小于value,说明要么这是第一个元素且大于value满足条件,要么不是第一个元素但它前一个元素小于value,也满足条件,这时就直接返回mid即可;否则就更新high。当小于时,直接更新low即可。

对于第二种方案——bisearch_var3_2:将A[mid]与value值的关系分两种情况:大于等于、小于,与方案bisearch_var3_1所不同的时,bisearch_var3_2将判断放在了while之后。经过while循环,同样满足一个共同条件:low = high + 1,也分三种情况:value<a[0]、a[0]<value<a[n-1]、a[n-1]<value。前两种情况都属于能找到满足条件的元素,第三种情况则属于找不到满足条件的元素,因为第三种情况下A中最大元素都小于value。所以,只有当low小于n-1时,low的值才是所求。

变形4:查找最后一个小于等于指定值的元素

思路类似于变形3,是其反过来的情况,废话少说,直接上码:

def bisearch_var4_1(A, value):
    low = 0
    high = len(A)-1
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] > value:
            high = mid - 1
        elif A[mid] <= value:
            if mid == len(A)-1 or A[mid+1] > value:
                return mid
            else:
                low = mid + 1
    return -1

def bisearch_var4_2(A, value):
    low = 0
    high = len(A)-1
    while low <= high:
        mid = low + ((high-low)>>1)
        if A[mid] > value:
            high = mid - 1
        elif A[mid] <= value:
            low = mid + 1
    if high >= 0:
        return high
    else:
        return -1

A = [1,2,3,3,3,4,4,5,6]
print(bisearch_var4_1(A,3)) # result: 4
print(bisearch_var4_2(A,5.5)) # result: 7

猜你喜欢

转载自blog.csdn.net/oYeZhou/article/details/106418067