用JavaScript带你通杀二分查找模板

589 阅读3分钟

前言

二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好,需要注意到很多细节,经常整体框架写出来了,还是bug频出。例如判断条件到底是 while(left < right) 还是 while(left <= right),左右查找到底是right = middle呢,还是要right = middle - 1呢?相信阅读完这篇文章,写二分查找再也不会一看就会,一写就废了。

1. 模板一

最经典的二分查找模板,教科书指定二分查找模板

初始条件:left = 0, right = n - 1

循环条件:left <= right

向左查找:right = mid-1

向右查找:left = mid+1

mid指针计算:左右指针和折半向下取整

  • 查找等于 target 的第一个元素下标,即查找 target 左边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left <= right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] < target) {
      left = mid + 1
    } else {
      right = mid - 1
    }
  }
  if(left !== nums.length && nums[left] === target) return left
  return -1
}
  • 查找等于 target 的最后一个元素下标,即查找 target 右边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left <= right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] <= target) {
      left = mid + 1
    } else {
      right = mid - 1
    }
  }
  if(right !== -1 && nums[right] === target) return right
  return -1
}

2. 模板二

左闭右开的区间分割方式在数学和计算机中都极为普遍。

初始条件:left = 0, right = n

循环条件:left < right

向左查找:right = mid

向右查找:left = mid+1

mid指针计算:左右指针和折半向下取整

  • 查找等于 target 的第一个元素下标,即查找 target 左边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length
  while (left < right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] < target) {
      left = mid + 1
    } else {
      right = mid
    }
  }
  if(left !== nums.length && nums[left] === target) return left
  return -1
}
  • 查找等于 target 的最后一个元素下标,即查找 target 右边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length
  while (left <= right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] <= target) {
      left = mid + 1
    } else {
      right = mid
    }
  }
  if(left != nums.length && nums[left] == target) return left;
  if(left > 0 && nums[left - 1] == target) return left - 1;
  return -1
}

3. 模板三

用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件。

初始条件:left = 0, right = n - 1

循环条件:left + 1 < right

向左查找:right = mid

向右查找:left = mid

mid指针计算:左右指针和折半向下取整

这个模板自己刚入门的时候比较喜欢, 提前一步退出,最后再作判断,不用考虑 mid 加一减一的情况


let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left + 1 < right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] < target) {  // 查找target左边界, <= 查找target右边界
      left = mid
    } else {
      right = mid
    }
  }
  if(nums[left] === target) return left
  if(nums[right] === target) return right
  return -1
}
  • 注释版
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left + 1 < right) { // 保证循环一定会退出
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] < target) {
      left = mid   // 不 +1,不影响结果,有时候 +1 反而会出错
    } else {
      right = mid  // 不 -1,不影响结果,有时候 -1 反而会出错
    }
  }
  // 因为提前一步退出了,最后一步自己判断
  if(nums[left] === target) return left
  if(nums[right] === target) return right
  return -1
}

4. 模板四

ACM大佬推荐查询目标元素左边界模板

初始条件:left = 0, right = n - 1

循环条件:left < right

向左查找:right = mid

向右查找:left = mid + 1

mid指针计算:左右指针和折半向下取整

  • 查找等于 target 的第一个元素下标,即查找 target 左边界。(熟练刷题专用)
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left < right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] < target) {
      left = mid + 1
    } else {
      right = mid
    }
  }
  if(nums[left] === target) return left
  return -1
}
  • 查找等于 target 的最后一个元素下标,即查找 target 右边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left < right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] <= target) {
      left = mid + 1
    } else {
      right = mid
    }
  }
  if(nums[left] === target) return left
  if(left > 0 && nums[left - 1] === target) return left - 1
  return -1
}

5. 模板五

ACM大佬推荐查询目标元素右边界模板

初始条件:left = 0, right = n - 1

循环条件:left < right

向左查找:right = mid - 1

向右查找:left = mid

mid指针计算:左右指针和折半向上取整

  • 查找等于 target 的第一个元素下标,即查找 target 左边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left < right) {
    let mid = Math.floor((right - left) / 2 + 1) + left
    if (nums[mid] < target) {
      left = mid
    } else {
      right = mid - 1
    }
  }
  if(nums[left] === target) return left
  if(left + 1 < nums.length && nums[left + 1] == target) return left + 1;
  return -1
}
  • 查找等于 target 的最后一个元素下标,即查找 target 右边界。(熟练刷题专用)
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = 0, right = nums.length - 1
  while (left <= right) {
    let mid = Math.floor((right - left) / 2 + 1) + left
    if (nums[mid] <= target) {
      left = mid
    } else {
      right = mid - 1
    }
  }
  if(nums[left] === target) return left
  return -1
}

6. 模板六

构造左右边界,蓝红二分,生动形象,界限分明。

初始条件:left = -1, right = n

循环条件:left + 1 < right

向左查找:right = mid

向右查找:left = mid

mid指针计算:左右指针和折半向下取整

  • 查找等于 target 的第一个元素下标,即查找 target 左边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = -1, right = nums.length 
  while (left + 1 < right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] < target) {
      left = mid
    } else {
      right = mid
    }
  }
  if(right != nums.length && nums[right] == target) return right;
  return -1
}
  • 查找等于 target 的最后一个元素下标,即查找 target 右边界。
let binarySearch = (nums, target) => {
  if (!nums || !nums.length) return -1
  let left = -1, right = nums.length 
  while (left + 1 < right) {
    let mid = Math.floor((right - left) / 2) + left
    if (nums[mid] <= target) {
      left = mid
    } else {
      right = mid
    }
  }
  if(left != -1 && nums[left] == target) return left;
  return -1
}

7. 模板总结

模板四和模板五是模板二的变体,模板六是模板三的变体,本质上都是二分思想的运用。

  • 左右指针关系

image.png

  • 初始区间选择

image.png