针对有序数据集合的查找算法:二分查找(Binary Search)算法,也叫折半查找算法。
假设有10个数据:8,11,19,23,27,33,45,55,67,98,快速找到19这个值是否在数据中。
利用二分思想,每次都与区间的中间数据比对大小,缩小查找区间的范围。过程如下图所示,其中,low
和 high
表示待查找区间的下标,mid
表示待查找区间的中间元素下标。
假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。
\(O(logn)\) 这种对数时间复杂度,是一种极其高效的时间复杂度,有时甚至比时间复杂度是常量级 \(O(1)\) 的算法还要高效。
最简单的情况就是有序数组中不存在重复元素。
low
、high
、mid
都是数组下标,low
和 high
表示当前查找的区间范围,初始 low=0
, high=n-1
。mid
表示\([low, high]\)的中间位置。value
的大小,来更新接下来要查找的区间范围,直到找到或者区间缩小为 0
,就退出。注意:
low>=high
,而不是 low>igh
mid
的取值:\(mid=\frac{low+high}{2}\)在数据较大时可能溢出,改成\(low+\frac{(high-low)}{2}\),优化性能可以将除法运算改为位运算low+((high-low)>>1)
low
和high
的更新:low=mid+1
,high=mid-1
,如果直接写成 low=mid
或者 high=mid
,就可能会发生死循环也可以使用递归实现:
binarySearch(p...r,value)=binarySearch(p...mid-1,value) or binarySearch(mid+1...r,value)
A[mid]=value or low >= high
有序数据集合中不存在重复的数据。
a[mid]
跟要查找的 value
的大小关系有三种情况:
a[mid]>value
的情况,更新 high=mid-1
a[mid]<value
的情况,更新 low=mid+1
a[mid]=value
的情况a[mid]
等于要查找的值时,a[mid]
就是要找的元素。a[mid]
等于要查找的值时,就需要确认一下这个 a[mid]
是不是第一个值等于给定值的元素。func BinarySearchFirst(list []int, value int) int {
low := 0
high := len(list) - 1
for low <= high {
mid := low + ((high - low) >> 1)
if list[mid] < value {
low = mid + 1
} else if list[mid] > value {
high = mid - 1
} else {
if mid == 0 || list[mid-1] != value {
return mid
} else {
high = mid - 1
}
}
}
return -1
}
看第 12 行代码。
a[mid]
的前一个元素 a[mid-1]
不等于 value,那也说明 a[mid]
就是要找的第一个值等于给定值的元素。a[mid]
前面的一个元素 a[mid-1]
也等于 value,那说明此时的 a[mid]
肯定不是要查找的第一个值等于给定值的元素。更新 high=mid-1
,要找的元素肯定出现在[low, mid-1]
之间。基于上面的分析:
if
的判定条件修改为mid == len(a)-1 || list[mid+1] != value
low=mid+1
func BinarySearchGE(list []int, value int) int {
length := len(list)
low := 0
high := length - 1
for low <= high {
mid := low + ((high - low) >> 1)
if list[mid] >= value {
if mid == 0 || list[mid-1] < value {
return mid
} else {
high = mid - 1
}
} else {
low = mid + 1
}
}
return -1
}
看第7、8行代码:
list[mid]>=value
,且mid==0
,那就是要查找的值list[mid]
的前一个值小于value
,那就是要查找的值[low,mid-1]
之间list[mid]<value
,说明要查找的之在[mid+1,high]
之间func BinarySearchLE(list []int, value int) int {
length := len(list)
low := 0
high := length - 1
for low <= high {
mid := low + ((high - low) >> 1)
if list[mid] > value {
high = mid - 1
} else {
if mid == len(list)-1 || list[mid+1] > value {
return mid
} else {
low = mid + 1
}
}
}
return -1
}
实际上,求“值等于给定值”的二分查找确实不怎么会被用到,二分查找更适合用在“近似”查找问题,在这类问题上,二分查找的优势更加明显。
上述这些变体的二分查找算法很容易因为细节处理不好而产生 Bug,这些容易出错的细节有: