二分查找及应用

二分查找针对有序集合,每次查找通过跟中间元素对比大小,将待查找的区间缩小为原来的一半,直到找到要查找的元素或者区间缩小为0.

二分查找是一种时间复杂度为O(logN)的查找算法,有时甚至比 hash 表中的 O(1)算法效率还要高,因为 hash 表计算 hash 值、解决冲突所花的时间不一定比 O(logN)短。

二分查找依赖的是顺序表结构,简单的说就是数组,利用数组支持按下标访问的特性实现高速查找。

二分查找使用场景:

  • 有序数组,且插入、删除不频繁,一次排序多次查找
  • 数据量太小不易使用,顺序遍历即可
  • 数据量太大也不易使用,因为存储这部分数据要耗费大量内存

快速排序

既然二分查找依赖的是有序数组,所以先将数据排序,常见的快速排序如下:

def qsort(arr):
    qsort_p(arr, 0, len(arr)-1)

def qsort_p(arr, p, q):
    if p < q:
        pivot = arr[q]
        i = p
        for j in range(p, q):
            if arr[j] < pivot:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
        arr[i], arr[q] = arr[q], arr[i]
        qsort(arr, p, i-1)
        qsort(arr, i+1, q)

for 循环借助插入排序的思想,变量 i 将数据一分为二:已处理区间和未处理区间,每次从 i..q-1 中取出一个元素arr[j]与 pivot 对比,如果小于 pivot 则将此数据放到已处理区间的末尾也就是 arr[i] 的位置。循环完成将 arr[i] 与 pivot 值互换,arr[i] 左边就是小于 pivot 的,右边是大于 pivot 的数据。

二分查找的应用

查找值等于给定值的元素

代码实现:

def bsearch(arr, val):
    low, high = 0, len(arr) - 1
    while low <= high:
        mid = low + (high - low) // 2
        if arr[mid] > val:
            high = mid - 1
        elif arr[mid] < val:
            low = mid + 1
        else:
            return mid
    return -1

每次都与区间中间值 arr[mid] 比较,缩小比较范围。若大于 arr[mid] 则更新 low = mid + 1;若小于 arr[mid] 则更新 high = mid - 1

计算平方根

精确到小数点后6位

def bsqrt(n):
    low, high = 0, n
    while high - low > 0.000001:
        mid = low + (high - low) / 2
        if mid * mid > n:
            high = mid
        elif mid * mid < n:
            low = mid
        else:
            return mid
    return mid

在数组中下标是整数,所以用了 (high - low) // 2,而这里直接使用的小数乘法,所以是正常的除法

二分查找变形问题

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

在找到给定值后不停下,而是继续往 查找,直到 mid 为 0 或者 mid 的前一个值不等于 val

def first_equal(arr, val):
    low, high = 0, len(arr) - 1
    while low <= high:
        mid = low + (high - low) // 2
        if arr[mid] < val:
            low = mid + 1
        elif arr[mid] > val:
            high = mid - 1
        else:
            if mid == 0 or arr[mid-1] != val:
                return mid
            else:
                high = mid - 1
    return -1

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

在找到给定值后不停下,而是继续往 查找,直到 mid 为 len(arr) - 1 或者 mid 的后一个值不等于 val

def last_equal(arr, val):
    length = len(arr)
    low, high = 0, length - 1
    while low <= high:
        mid = low + (high - low) // 2
        if arr[mid] > val:
            high = mid - 1
        elif arr[mid] < val:
            low = mid + 1
        else:
            if mid == length - 1 or arr[mid+1] != val:
                return mid
            else:
                low = mid + 1
    return -1

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

一直往 查找,直到 mid 为 0 或者 mid 的前一个值小于 val

def first_great(arr, val):
    length = len(arr)
    low, high = 0, length - 1
    while low <= high:
        mid = low + (high - low) // 2
        if arr[mid] < val:
            low = mid + 1
        else:
            if mid == 0 or arr[mid-1] < val:
                return mid
            high = mid - 1
    return -1

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

一直往 查找,直到 mid 为 len(arr)-1 或者 mid 的后一个值大于 val

def last_less(arr, val):
    length = len(arr)
    low, high = 0, length - 1
    while low <= high:
        mid = low + (high - low) // 2
        if arr[mid] <= val:
            if mid == length - 1 or arr[mid+1] > val:
                return mid
            else:
                low = mid + 1
        else:
            high = mid - 1
    return -1

容易出错的地方

  1. 循环退出条件是 low <= high
  2. mid 的取值,如果写成 (low+high) // 2 可能会溢出
  3. low 和 high 的更新,一定是 low = mid + 1 和 high = mid - 1 ,如果写成 low = mid 或 high = mid,当 mid = 3 时,如果 arr[3] 不等于 val 就会发生死循环
posted @ 2020-01-30 17:03  葡萄不吐皮  阅读(315)  评论(0编辑  收藏  举报