时间复杂度
通过 n/2k=1,我们可以求得 k=log2n,所以时间复杂度就是 O(logn)。
具体实现
非递归
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
*易错点
- 循环退出条件
- mid取值 mid=(low+high)/2 在 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low+(high-low)/2。更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low+((high-low)>>1)。
- low与high的更新 low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。
递归
// 二分查找的递归实现
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);
}
}
局限性
- 二分查找依赖的是顺序表结构,简单点说就是数组。
- 二分查找针对的是有序数据,二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。针对动态变化的数据集合,二分查找将不再适用。
- 数据量太小不适合二分查找,遍历跟二分查找的速度差不多。例外:如果数据之间的比较操作非常耗时,不管数据量大小,推荐使用二分查找。比如,数组中存储的都是长度超过 300 的字符串,如此长的两个字符串之间比对大小,就会非常耗时。需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。
- 数据量太大也不适合二分查找,二分查找的底层需要依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。
二分变形问题
查找第一个值等于定值的元素
function bsearch(a, n){
let low = 0;
let high = n-1;
while(low <= high){
let mid = low + ((high-low) >> 1);
if(a[mid]>n){
high = mid -1;
}else if (a[mid] < n){
low = mid + 1;
}else{
if(mid === 0 || a[mid-1]!=n) return mid;
else high = mid -1;
}
}
return -1
}
const a =[1,3,4,5,6,8,8,8,11,18];
let n = 8;
console.log(bsearch(a,n))
查找最后一个值等于定值的元素
function bsearch(a, n){
let low = 0;
let high = n-1;
while(low <= high){
let mid = low + ((high-low) >> 1);
if(a[mid]>n){
high = mid -1;
}else if (a[mid] < n){
low = mid + 1;
}else{
if(mid === n-1 || a[mid+1]!=n) return mid;
else low = mid + 1;
}
}
return -1
}
const a =[1,3,4,5,6,8,8,8,11,18];
let n = 8;
console.log(bsearch(a,n))
给定第一个大等于定值的元素
function bsearch(a, n){
let low = 0;
let high = n-1;
while(low <= high){
let mid = low + ((high-low) >> 1);
if(a[mid] >= n){
if(mid === 0 || a[mid-1] < n) return mid
else high = mid -1;
}else {
low = mid + 1;
}
}
return -1
}const a =[1,3,4,5,6,8,8,8,11,18];
let n = 7;
console.log(bsearch(a,n))
查找最后一个小等于定值的元素
function bsearch(a, n){
let low = 0;
let high = n-1;
while(low <= high){
let mid = low + ((high-low) >> 1);
if(a[mid] <= n){
if(mid === n-1 || a[mid+1] > n) return mid
else low = low + 1;
}else {
high = mid - 1;
}
}
return -1
}const a =[1,3,4,5,6,8,8,8,11,18];
let n = 7;
console.log(bsearch(a,n))
实例
想要查询 202.102.133.13 这个 IP 地址的归属地时,可在地址库中搜索,发现这个 IP 地址落在[202.102.133.0, 202.102.133.255]这个地址范围内,就可以将这个 IP 地址范围对应的归属地“山东东营市”显示给用户了。
[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] 江苏连云港
问题可以转化为:二分变形问题的第四种——“在有序数组中,查找最后一个小于等于某个给定值的元素”
如何在循环数组中查找给定值的元素
var search = function(nums, target) {
let low = 0;
let high = nums.length-1;
while(low <= high){
let mid = low +((high - low)>>1);
if(target === nums[mid]) return mid
if(nums[low] <= nums[mid]){
if(target < nums[mid] && target >= nums[low]){
high = mid - 1;
}
else{
low = mid + 1;
}
}else {
if(target > nums[mid] && target <= nums[high]){
low = mid + 1;
}
else{
high = mid - 1;
}
}
}
return -1
};
资料来源
time.geekbang.org/column/arti… time.geekbang.org/column/arti…