数据结构与算法系列13--二分查找

什么是二分查找?

二分查找算法是一种针对有序集合的查找算法,也叫折半查找。

实现原理

每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0。(特别注意前提是针对有序的数据集合)

时间复杂度

二分查找是一种非常高效的查找算法,他的时间复杂度是O(logn),我们可以假设数据大小是n,每次查找后数据都会缩小为原来的一半,也就是会除以2。最坏情况下,直到查找区间被缩小为空,才停止。那么被查找的区间大小变化为:n,n/2,n/4,n/8…n/2^k,可以看出这是一个等比数列,其中n/2k=1时,k的值就是总共缩小的次数,而每一次缩小操作只涉及两个数据的大小比较,所以,经过了k次区间缩小操作,时间复杂度就是O(k)。通过n/2k=1,我们可以求得
在这里插入图片描述所以时间复杂度就是O(logn)。另外,logn是一个非常“恐怖“的数量级,即便n非常大,对应的logn也很小。比如n等于2的32次方,也就是42亿,而logn才32。

二分查找的非递归实现

def bsearch(a,value):
    low=0
    high=len(a)-1

    while(low<=high):
        mid=(low+high)//2
        if a[mid]==value:
            return mid
        elif a[mid]>value:
            high=mid-1
        else:
            low=mid+1

    return -1


a=[1,2,3,4,5,6]
print(bsearch(a,6))

注意:
1.循环退出条件
注意是 low<=high,而不是 low<high。
2.mid的取值
mid的取值,使用mid=low + (high - low) / 2,而不用mid=(low + high)/2,因为如果low和high比较大的话,求和可能会发生int类型的值超出最大范围。为了把性能优化到极致,可以将除以2转换成位运算,即low + ((high - low) >> 1),因为相比除法运算来说,计算机处理位运算要快得多。
3.low和high的更新
low = mid - 1,high = mid + 1,若直接写成low = mid,high=mid,就可能会发生死循环。比如,我们假设low=5,high=5,此时如果a[5]不等于value,那么此时就会陷入死循环。

二分查找的递归实现

def bsearch(a,value):
    low=0
    high=len(a)-1
    return bsearchInternally(a,low,high,value)


def bsearchInternally(a,low,high,value):

    if low>high:
        return -1

    mid=(low+high)//2
    if a[mid]==value:
        return mid
    elif a[mid]>value:
        high=mid-1
        return bsearchInternally(a,low,high,value)
    else:
        low=mid+1
        return bsearchInternally(a,low,high,value)

a=[1,2,3,4,5,6]
print(bsearch(a,5))

二分查找应用场景的局限性

虽然二分查找速度很快,时间复杂度为O(logn),但是并不是什么情况下都可以使用二分查找。
1.二分查找依赖的是顺序表结构,即数组。
那二分查找能否依赖其他数据结构呢?比如链表。答案是不可以的,主要原因是二分查找算法需要按照下标随机访问元素,数组按照下标随机访问数据的时间复杂度是O(1),而链表随机访问的时间复杂度是O(n)。所以,如果数据使用链表存储,二分查找的时间复杂就会变得很高。
2.二分查找针对的数据集合必须是有序的
二分查找针对的是有序数据,因此只能用在插入、删除操作不频繁,一次排序多次查找的场景中。针对动态变化的数据集合,二分查找将不再适用。那针对动态数据集合,如何在其中快速查找某个数据呢?可以使用二叉树,后续我们也会讲到。
3.数据量太小不适合二分查找
因为如果要处理的数据量很小,不管用二分查找还是顺序遍历,查找速度都差不多。优势并不明显。不过,这里有一个例外。如果数据之间的比较操作非常耗时,不管数据量大小,我都推荐使用二分查找。因为假如数组中存储的都是长度超过300的字符串ao,如此长的两个字符串之间比对大小,就会非常耗时。我们需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。
4.数据量太大也不适合二分查找
前面我们说到二分查找需要依赖顺序表结构,也即是数组,而数组是需要连续的储存空间,如果要查找的数据量非常大,往往找不到存储如此大规模数据的连续内存空间。

二分查找变形问题(重要):

前面我们讲的是最简单的一种二分查找算法。看起来还是比较简单的,但是它的变形问题就比较复杂了。二分查找的变形问题很多,这里我们只讲解几个典型的例子:

  1. 查找第一个值等于给定值的元素
  2. 查找最后一个值等于给定值的元素
  3. 查找第一个大于等于给定值的元素
  4. 查找最后一个小于等于给定值的元素

查找第一个值等于给定值的元素

def bsearch(a,value):
	low=0
	high=len(a)-1
	while(low<=high):
		mid=low+((high-low)>>1)
		if a[mid]<value:
			low=mid+1
		elif a[mid]>value:
			high=mid-1
		else:
			#这里先判断mid是否等于0,如果是说明是第一个了,另外如果mid不等于0,但a[mid]的前一个元素a[mid-1]不等于value,说明也是a[mid]就是我们要查找的第一个等于value的元素
			if (mid==0||a[mid-1]!=value):
				return mid
			else:
			#如果经过检查之后发现a[mid]前面的一个元素a[mid-1]也等于value,那就说明a[mid]肯定不是我们要查找的第一个值等于value的元素。那我们就更新high的值,因为要查找的元素肯定会出现在[low,mid-1]之间。
				high=mid-1
				
	return -1

a=[1,2,3,3,4,4,4,5,5,5,5]
print(bsearch(a,4))

同样的思路,我们写下查找最后一个值等于给定值的元素

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

            if ((mid==len(a)-1)or(a[mid+1]!=value)):
                return mid
            else:
                low=mid+1

    return -1

a=[1,2,3,3,4,4,4,5,5,5,5]
print(bsearch3(a,4))

查找第一个大于等于给定值的元素:

def bsearch4(a,value):
    low=0
    high=len(a)-1
    while(low<=high):
        mid=low+((high-low)>>1)
        if a[mid]>=value:
        	#注意这里
            if (a[mid]==0)or(a[mid-1]<value):
                return mid
            else:
                high=mid-1
        else:
            low=mid+1

    return -1


a=[1,2,5,6]
print(bsearch4(a,3))

查找最后一个小于等于给定值的元素

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


    return -1

a=[1,2,5,9]
print(bsearch5(a,8))

二分查找应用场景:
1.凡事能用二分查找解决的,绝大部分我们更倾向于用散列表或者二叉查找树,即便二分查找在内存上更节省,但是毕竟内存如此紧缺的情况并不多。
2.二分查找适用于近似查找,如第一个≥给定值的元素,第一个≤给定值的元素,都是二分查找的变体。而一般情况下查找某个给定值,二叉查找树和散列表更适合。

猜你喜欢

转载自blog.csdn.net/qq_34493908/article/details/83794081