二分查找及应用
二分查找针对有序集合,每次查找通过跟中间元素对比大小,将待查找的区间缩小为原来的一半,直到找到要查找的元素或者区间缩小为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
容易出错的地方
- 循环退出条件是 low <= high
- mid 的取值,如果写成 (low+high) // 2 可能会溢出
- low 和 high 的更新,一定是 low = mid + 1 和 high = mid - 1 ,如果写成 low = mid 或 high = mid,当 mid = 3 时,如果 arr[3] 不等于 val 就会发生死循环