1.什么是二分査找?
二分查找是一种查询效率非常高的查找算法。又称折半查找。
生活中二分查找的思想无处不在。一个最常见的就是猜数游戏,我随机写一个 0 到 99 的数,然后你来猜我写的是什么。猜的过程中,我会告诉你每次是猜大了还是猜小了,直到猜中为止。假如我写的数是 23,猜数过程如下所示。
最多只需要 7 次就猜出来了,这个过程是很快的。同理,要查找某个数据是否在给定的数组中,我们同样也可以利用这个思想。
二分查找针对的是一个有序的数据集合,查找思想有点类似于分治,每次都通过和中间元素进行比较,将待查找区间缩小为之前的一半,直到找到要查找的元素或者区间缩小为 0 为止。
2. 二分查找的时间复杂度?
我们假设数据大小为 n,每次查找数据大小都会缩小为原来的一半,最坏情况下,直到查找区间缩小为空时停止查找。
若经过 k 次区间缩小最后变为空,则 n2k=1,k=log2nn2k=1,k=log2n,所以二分查找的时间复杂度为 O(logn)。
这种对数时间复杂度的算法是一种非常高效的算法,有时候甚至比时间复杂度为常量级的算法还要高效。因为常量级的时间复杂度对应的常数可能非常大,比如 O(100), O(1000),因此这些算法有时候可能还没有 O(logn)O(logn) 的算法执行效率高。
3.如何实观二分查找?
可以使用循环法和递归法分别实现:
public class BinarySearch {
/**
* 使用循环实现二分查找算法(不使用递归实现二分查找算法)
* 注意事项:
• 循环退出条件是:starte<=end,而不是 start<mid
• min的取值,使用mid=start+(end-start)/2,而不用 mid=(start+end)/2,因为如果 start和end比较大的话,求和可能会发生int类型的值超出最大范围.,为了把性能优化到极致,可以将除以2转换成位运算,即 start+(end-start)>>1),因为相比除法运算来说,计算机处理位运算要快得。
• start和end 的更新:start=mid-1,end=mid+1,若直接写成start=mid,end=md,就可能会发生死循环.
* @param arr
* @param x
* @return
*/
public static int binarySearch(int[] arr, int x) {
int low = 0;
int high = arr.length-1;
while(low <= high) {
int middle = (low + high)/2;
if(x == arr[middle]) {
return middle;
}else if(x <arr[middle]) {
high = middle - 1;
}else {
low = middle + 1;
}
}
return -1;
}
/**
* 使用递归实现二分查找
* 二分查找递归实现的局限性:
• 二分查找依赖的是顺序表结构,即数组.
• 二分查找针对的是有序数据,因此只能用在插入,删除操作不频繁,一次排序多次查找的场景
• 数据量太小不适合二分查找,与直接遍历相比效卒提升不明显,但有一个例外,就是数据之间的比较操作非常费时,比如数组中存储的部是长度超过300的字符串,那这是还是尽量减少比较操作使用二分查找吧.
• 数据量太大也不是适合用二分查找,因为数组需要连续的空间,若数据量太大,往往找不到存储如此大规模数据的连续内存空间
* @param srcArray
* @param start
* @param end
* @param key
* @return
*/
public static int binSearch(int srcArray[], int start, int end, int key) {
int mid = (end - start) / 2 + start;
if (srcArray[mid] == key) {
return mid;
}
if (start >= end) {
return -1;
} else if (key > srcArray[mid]) {
return binSearch(srcArray, mid + 1, end, key);
} else if (key < srcArray[mid]) {
return binSearch(srcArray, start, mid - 1, key);
}
return -1;
}
public static void main(String[] args) {
int[] arr = { 6, 12, 33, 87, 90, 97, 108, 561 };
System.out.println("循环查找:" + (binarySearch(arr, 87) + 1));
int srcArray[] = {3,5,11,17,21,23,28,30,32,50,64,78,81,95,101};
System.out.println("递归查找:" + binSearch(srcArray, 0, srcArray.length - 1, 81));
}
}
注意事项
• 循环退出条件 left <= right
• mid = left + ((right-left) >> 1),用移位运算优化计算性能
• left 和 right 的更新分别是 mid+1 和 mid-1
4. 二分查找的应用场景?
• 二分查 找依赖的是顺序表结构,也就是数组,需要能够按照下标随机访问元素。
• 二分查找针对的是有序数据,如果数据无序,需要先进行排序。而如果有频繁的插入、删除操作,则每次查找前都需要再次排序,这时候,二分查找将不再适用。
• 数据量太小可以直接遍历查找,没有必要用二分查找。但如果数据之间的比较操作非常耗时,比如数据为长度超过 300 的字符串,则不管数据量大小,都推荐使用二分查找。
• 而如果数据量过大,则意味着我们需要用非常大的连续内存空间来存储这些数据,内存开销可能无法满足。
5.思考
最简单的二分查找情况下,我们假设数组中没有重复元素,因此很容易实现。如果数组中存在重复元素,二分查找就没有想象中那么容易了。
5.1 查找第一个值等于给定值的元素?
如果数据中存在相同的元素,但我们要查找第一个值等于给定值的元素,如果直接用最简单的二分查找,显然是不满足的。看下面的例子,有 3 个等于 8 的元素,简单二分查找会返回 7,但第一个值等于 8 的元素应该是 a[5]。
其实,只要我们在简单二分查找的基础上再多加上一点限制即可。
当我们发现 a[mid] = val 时,我们需要继续确认 a[mid] 左边还有没有等于 val 的元素。若 mid 左边没有和 a[mid] 相等的元素,则此时 a[mid] 就是我们要找的第一个值等于给定值的元素。另外,若 mid 到了第一个元素的位置,说明左边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向左边查找。
float Binary_Search(float data[], int left, int right, float value)
{
int begin = left;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (value == data[mid])
{
if (mid == begin || a[mid - 1] != a[mid]) return mid;
else right = mid - 1;
}
else if (value < data[mid])
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return -1;
}
5.2 查找最后一个值等于给定值的元素?
这个问题和上面要查找第一个值等于给定值的问题思路一样,只不过是查找的方向改变了。
当我们发现 a[mid] = val 时,我们需要继续确认 a[mid] 右边还有没有等于 val 的元素。若 mid 右边没有和 a[mid] 相等的元素,则此时 a[mid] 就是我们要找的最后一个值等于给定值的元素。另外,若 mid 到达了最后一个元素的位置,说明右边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向右边查找。
float Binary_Search(float data[], int left, int right, float value)
{
int end = right;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (value == data[mid])
{
if (mid == end || a[mid + 1] != a[mid]) return mid;
else left = mid + 1;
}
else if (value < data[mid])
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
return -1;
}
5.3 查找第一个大于等于给定值的元素?
当我们发现 a[mid] >= val 时,我们需要继续确认 a[mid] 左边还有没有大于等于 val 的元素。若 mid 左边的值小于 a[mid],则此时 a[mid] 就是我们要找的第一个大于等于给定值的元素。另外,若 mid 到达了第一个元素的位置,说明左边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向左边查找。
float Binary_Search(float data[], int left, int right, float value)
{
int begin = left;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (data[mid] >= value)
{
if (mid == begin || a[mid - 1] < a[mid]) return mid;
else right = mid - 1;
}
else
{
left = mid + 1;
}
}
return -1;
}
5.4 查找最后一个小于等于给定值的元素?
当我们发现 a[mid] <= val 时,我们需要继续确认 a[mid] 右边还有没有小于等于 val 的元素。若 mid 右边的值大于 a[mid],则此时 a[mid] 就是我们要找的最后一个小于等于给定值的元素。另外,若 mid 到达了最后一个元素的位置,说明右边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向右边查找。
float Binary_Search(float data[], int left, int right, float value)
{
int end = right;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (data[mid] <= value)
{
if (mid == end || a[mid + 1] > a[mid]) return mid;
else left = mid + 1;
}
else
{
right = mid - 1;
}
}
return -1;
}
5.5 如何快速定位 IP 对应的省份?
当我们要查找 202.102.133.13 这个 IP 地址的归属地时,我们就在地址库中搜索,发现这个 IP 位于 [202.102.133.0, 202.102.133.255] 这个范围内,我们就可以找到对应的归属地——山东东营。
[202.102.133.0, 202.102.133.255] 山东东营市
[202.102.135.0, 202.102.136.255] 山东烟台
[202.102.156.34, 202.102.157.255] 山东青岛
[202.102.48.0, 202.102.48.255] 江苏宿迁
[202.102.49.15, 202.102.51.251] 江苏泰州
[202.102.56.0, 202.102.56.255] 江苏连云港
因此,我们可以按照 IP 地址库的起始地址对所有区间进行排序,然后问题就转化为了找到最后一个起始地址小于等于给定 IP 地址的区间。