数据结构——二分查找(python实现)

最近开始学习王争老师的《数据结构与算法之美》,通过总结再加上自己的思考的形式记录这门课程,文章主要作为学习历程的记录。

课前问题:假设有1000万个整数数据,每个数据占8个字节,如何设计数据结构与算法,快速判断某个整数是否出现在这1000万数据中,同时又希望这个功能内存不要超过100M?

这个问题就引入了二分查找,举个例子来理解二分查找:假设一个数组为0-99,要查找元素23在数组中的位置

次数 猜测范围 中间数 对比大小
1 0-99 49 49>23
2 0-48 24 24>23
3 0-23 11 11<23
4 12-23 17 17<23
5 18-23 20 20<23
6 21-23 22 22<23
7 23

利用二分思想,每次都与区间中间数据比对大小,缩小查找区间的范围,二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想,每次都通过跟区间的中间元素对比,将待查找的区间缩小为原来的一半,直到找到要查找的元素,或者区间被缩小为0。

二分查找查找效率非常高。假设数据大小为n,每次查找后数据都会缩小为原来的一半,也就是会除以2,最坏的情况下,直到查找区间被缩小为空才停止。

被查找区间的大小变化:

n n n , n / 2 n/2 n/2 , n / 4 n/4 n/4 , n / 8 n/8 n/8 , … , n / 2 k n/2^k n/2k n / 2 k n/2^k n/2k=1时,k的值就是缩小的次数。

​ 时间复杂度为O(k) = O( l o g 2 n log_2{n} log2n),故时间复杂度为O(logn)

最简单的二分查找:

最简单的二分查找就是有序数组中不存在重复元素,代码:

def binary_search(l,n):
    low = 0
    high = len(l)-1
    while(low<=high):
        mid = low+((high-low)>>1) 
        if n == l[mid]:
            return mid
        elif nl[mid]:
            low = mid+1

下面有几点需要注意:

1.循环退出条件:

​ 注意是low<=high,而不是low

2.mid的取值:

​ mid = (low+high)/2的写法是有问题的,low+high可能会发生溢出,改进的方法是将mid的计算方式写出low+(high-low)/2.需要进一步优化的话,可将除以2的操作转化为位运算,即low+((high-low)>>1),速度要快得多。

3.low和high的更新

​ low = mid+1, high = mid -1,如果直接写成low = mid或者high = mid就会发生死循环。

实际上,二分查找除了可以用循环,还可以采用递归实现。

def binary_search(l,n,low,high):   
    while(high>=low):
        mid = ((high-low)>>1)+low
        if l[mid]==n:
            return mid
        elif l[mid]>n:
            return binary_search(l,n,low,mid-1)
        elif l[mid]

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

一、二分查找依赖的是顺序表结构,即数组

​ 二分查找不适合用于链表,主要原因在于二分查找算法需要按照下标随机访问元素。数组按照下标随机访问数据的时间复杂度为O(1),而链表是O(n)。因此数据使用链表存储的话,二分查找时间复杂度就会很高。

二、二分查找针对的是有序数组

​ 二分查找要求数据必须是有序的。排序的时间复杂度最低是O(nlogn)。因此,针对的是一组静态的数据,没有频繁地插入、删除,我们可以进行一次排序,多次二次查找,这样排序的成本可以被均摊,二次查找的边际成本就会比较低。

三、数据量太小不适合采用二分查找

​ 数据量很小,直接进行顺序遍历。只有数据量大时,二分查找的优势才比较明显。但若是数据之间的比较操作非常耗时,不管数据量大小,都更推荐使用二分查找。

四、数据量太大也不适合二分查找

​ 二分查找的底层需要依赖数组,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。因此太大的数据用数组存储就比较吃力,不能用二分查找。

补充一个思考题,编程实现“求一个数的平方根,保留6位有效小数”

def sqrt(t):
    low = 0
    high = t
    mid = t/2
    while(abs(t-mid**2)>0.00001): 
        if mid**2>t:
            high = mid
        elif mid**2

四种常见的二分查找变形问题

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

这个问题的难度在于当查找到对应的值时,不一定是第一个值,需要进行判断。若mid=0,则元素已经是第一个值了,即我们查找的。若mid不等于0,但s[mid]的前一个元素s[mid-1]不等于t,那么s[mid]就是我们要找的第一个等于给定值的元素。如果发生s[mid-1]也等于t,则令high = mid - 1,因为要找的元素肯定在[low,mid-1]中。

def bin_find(s,t): #查找第一个等于给定值的元素
    low = 0
    high = len(l)-1
    while(high>=low):
        mid = low + ((high-low)>>1)
        if s[mid]t:
            high = mid-1
        elif s[mid]==t:
            if mid==0 or s[mid-1]!=t:
                return mid
            else:
                high = mid-1

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

与变体一类似

def bin_find(s,t): #查找最后一个等于给定值的元素
    low = 0
    high = len(l)-1
    while(high>=low):
        mid = low + ((high-low)>>1)
        if s[mid]t:
            high = mid-1
        elif s[mid]==t:
            if mid==0 or s[mid+1]!=t:
                return mid
            else:
                low = mid+1

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

难度在于s[mid]>=t。我们要首先看一下这个s[mid]是不是我们要找的第一个大于等于给定值的元素。如果s[mid]前面无元素或前一个元素小于要查找的t,那s[mid]即为要找的元素。如果s[mid-1]也大于等于要找的t,说明要查找的元素在[low,mid-1]之间,所以要将high更新为mid-1.

def bin_find(s,t): #查找第一个大于等于给定值的元素
    low = 0
    high = len(l)-1
    while(high>=low):
        mid = low + ((high-low)>>1)
        if s[mid]=t:
            if mid==0 or s[mid-1]

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

与变体三类似

def bin_find(s,t): #查找最后一个小于等于给定值的元素
    low = 0
    high = len(l)-1
    while(high>=low):
        mid = low + ((high-low)>>1)
        if s[mid]<=t:
            if mid == 0 or s[mid+1]>t:
                return mid
            else:
                low = mid+1
        elif s[mid]>t:
            high = mid-1

参考资料:王争《数据结构与算法之美》

你可能感兴趣的:(数据结构与算法)