餐后我们经常会玩儿的猜数字游戏(庄家规定一个数字区间,然后写下一个数,大家猜这个数,每次猜完之后如果没有猜中,庄家更新数字区间),为了能够尽快结束游戏,一般都会使用二分的方式进行报数,这里面的思想就是二分查找。
二分查找是从有序数字序列中找到某个值的一种快速查找方式,至多logn(n代表数字个数)次就可以找到这个数字,查找速度很快。在解决相关算法题目时,如果需要从一个有序区间中寻找到一个满足约束条件的值,那么就可以考虑使用二分查找的思想,例如求一个数的平方根。
实现方式包括递归方式和非递归方式,非递归方式用的多,因为递归方式需要生成递归调用栈,空间开销比较大。
实现过程中有几个地方需要注意:
// 普通:非递归
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
//加上等号因为要找的数可能在low或high位置
while (low <= high) {
// int mid= low +((high-low)>>1);
// int mid = (low + high) / 2;
// 这里需要注意:采用减法操作可以避免大数溢出
// 另外如果采用移位运算一定要加括号,因为右移的优先级低
int mid = low + ((high - low) >> 1);
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
// 如果是面试需要事先跟面试官确认好应该返回什么
return -1;
}
// 递归方式:递归一定注意要有终止条件
// 二分查找的递归实现
public int bsearch(int[] a, int n, int val) {
return bsearchInternally(a, 0, n - 1, val);
}
private int bsearchInternally(int[] a, int low, int high, int value) {
// 注意递归终止的条件,与迭代方式刚好相反
if (low > high) return -1;
int mid = low + ((high - low) >> 1);
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
return bsearchInternally(a, mid+1, high, value);
} else {
return bsearchInternally(a, low, mid-1, value);
}
}
// 求平方根:
Public float sqrt(x){
low=0;
high=x;
mid=x/2;
// 这个地方也有可能存在溢出问题,可以采用每次记录上一次的值,判断两次值的差值的方式
// 对于某些苛刻的面试,一般需要考虑到极端情况
// 溢出处理一般可以采用加法变减法比较,乘法变除法比较
while(abs(mid**2-x)>0.000001){
// 判断条件可以改为下面防止溢出
// if(mid < x / mid)
if(mid**2<x) low=mid;
else high=mid;
mid=(low+high)/2;
}
Return mid;
}
一般情况下数组中保存的数字可能会存在重复的情况,并且在实际应用中,存储的都是对象,只是根据对象的某些值进行排序查找的,所以在这种情况下这样的变形就发挥作用了。
另外有一道这样的算法题,给定一个排好序的数组,找出某个数字出现的次数。这个题目首先想到的肯定是遍历一遍进行统计,这样时间复杂度是O(n)。一般对于比较简单的面试题,面试官想要的一定不是平常的解,对于这个题就可以利用二分的变体找出第一个值等于给定值的元素位置,再找出最后一个值等于给定值的位置,两个值做差+1就可以得到个数,时间复杂度是O(logn),如果数据量很大的情况下,这个提高是非常明显的。
对于这种变体,着重需要注意的就是当找到一个值等于给定值的时候不能直接返回,因为不确定找到的值是不是位于序列(所有相等值构成的序列)的头部或尾部。这个时候如果mid值为0可以直接返回,因为已经是整个序列的最前面了。如果mid-1或者mid+1的值不等于给定值,直接返回,因为该值已经位于序列的最前方或者最后方。其他情况都让mid -1 或者mid +1,因为此时mid的前后还存在相同的值。
按照代码很容易理解其中的逻辑。
// 查找第一个值等于给定值的元素:
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
//mid等于0说明前面没有数了,如果前面还有数且等于value就继续向前找
if ((mid == 0) || (a[mid - 1] != value)) return mid;
else high = mid - 1;
}
}
return -1;
}
// 查找最后一个值等于给定值的元素:
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
// mid等于n-1说明后面没有数了,如果后面还有数且等于value就继续向后找
if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
else low = mid + 1;
}
}
return -1;
}
该种变体也是针对某些特殊场景而使用的,例如根据手机号码查找归属地的问题等,实现的思路与前面第二小节相似。拿找出第一个大于等于给定值为例,如果a[mid]
// 找出第一个大于等于
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] >= value) {
// 处理的关键,不符合条件继续往前找
if ((mid == 0) || (a[mid - 1] < value)) return mid;
else high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
//查找最后一个小于等于给定值的元素
public int bsearch7(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else {
// 不符合条件继续往后找
if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
else low = mid + 1;
}
}
return -1;
}