漫漫前端路之数据结构与算法基础VIII——二分查找篇

161 阅读1分钟

时间复杂度

image.png 通过 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;
}

*易错点

  1. 循环退出条件
  2. mid取值 mid=(low+high)/2 在 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low+(high-low)/2。更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low+((high-low)>>1)。
  3. 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 的字符串,如此长的两个字符串之间比对大小,就会非常耗时。需要尽可能地减少比较次数,而比较次数的减少会大大提高性能,这个时候二分查找就比顺序遍历更有优势。
  • 数据量太大也不适合二分查找,二分查找的底层需要依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。

二分变形问题

image.png

查找第一个值等于定值的元素

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] 江苏连云港

问题可以转化为:二分变形问题的第四种——“在有序数组中,查找最后一个小于等于某个给定值的元素”

如何在循环数组中查找给定值的元素

leetcode 33

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…