二分法

88 阅读3分钟

二分法基础

技巧1: 找到二分时mid的对比值

技巧2: 闭区间和开区间转换(部分题目开区间更好写)

low bound

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出第一个不小于target的元素位置。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

解法分析:

寻找目标值在数组中的开始位置,即找出target对应的lower bound。

假设每次分析基于闭区间[left, right],在每次循环迭代时:

  • nums[mid] < target,此时从mid - 1往前,肯定是小于target的,left = mid + 1

  • nums[mid] > target,此时从mid + 1往后,肯定是大于target的,right = mid - 1

  • nums[mid] = target, 此时需要收缩right,而不是left,如[8, 8, 8], target=8时,收缩left的话,会找不到下界。收缩right的话,

    • nums[mid - 1] = target,迭代的下一个循环继续处理
    • nums[mid - 1] < target,在此之后,left会不断收缩,直到left > right
  • 循环结束条件:由于是闭区间,left <= right

循环结束时,会有三种情况:

  • 数组中存在正常下界,此时left的位置为目标值的下界(right + 1也为目标值下界)
  • 数组元素均小于target,此时left为nums.length
  • 数组元素均大于target,此时left为0
// 闭区间循环
export function low_bound(nums, target) {
  if (nums.length === 0) return -1
  let left = 0, right = nums.length - 1
  while (left <= right) { // [left, right]
    const mid = Math.floor(left + (right - left) / 2)
    if (nums[mid] < target) {
      left = mid + 1 // [mid + 1, right]
    } else {
      // num[mid] = target时,需要收缩右端点
      // 收缩左端点的话,会找不到下界,如[8, 8, 8, 8], target=8
      right = mid - 1 // [left, mid - 1]
    }
  }
  // 1. 数组中存在正常下界,此时left的位置为目标值的下界
  // 2. 数组元素均小于target,此时left为nums.length
  // 3. 数组元素均大于target,此时left为0
  return left
}

解法分析:

假设每次分析基于左闭右开[left, right)

此时在循环时需要注意:

  • 循环条件: left < right
  • 右端点收缩时,right = mid
// 左闭右开循环
export function low_bound2(nums, target) {
  if (nums.length === 0) return -1
  let left = 0, right = nums.length
  while (left < right) { // [left, right)
    const mid = Math.floor(left + (right - left) / 2)
    if (nums[mid] < target) {
      left = mid + 1 // [mid + 1, right)
    } else {
      right = mid // [left, mid)
    }
  }
  // 1. 数组中存在正常下界,此时left的位置为目标值的下界
  // 2. 数组元素均小于target,此时left为nums.length
  // 3. 数组元素均大于target,此时left为0
  return left
}

解法分析:

假设每次分析基于左开右开(left, right)

此时在循环时需要注意:

  • 循环条件: left + 1 < right
  • 左端点收缩时,left = mid
  • 右端点收缩时,right = mid
// 左开右开循环
export function low_bound3(nums, target) {
  if (nums.length === 0) return -1
  let left = -1, right = nums.length
  while (left + 1 < right) { // (left, right)
    const mid = Math.floor(left + (right - left) / 2)
    if (nums[mid] < target) {
      left = mid // (mid, right)
    } else {
      right = mid // (left, mid)
    }
  }
  // 1. 数组中存在正常下界,此时left的位置为目标值的下界
  // 2. 数组元素均小于target,此时left为nums.length
  // 3. 数组元素均大于target,此时left为0
  return right
}

其他情况

整数数组时,以下四种情况,均可以转化成low bound形式

  • >=target>= target, 对应low bound
  • >target> target,转化成 >=target+1>= target + 1
  • <=target<= target, 转化成 >=target+1 >= target + 1 的前一个元素
  • <target< target, 转化成 >=target>= target 的前一个元素

在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题

function low_bound(nums, target) {
  if (nums.length === 0) return -1
  let left = 0, right = nums.length - 1
  while (left <= right) { // [left, right]
    const mid = Math.floor(left + (right - left) / 2)
    if (nums[mid] < target) {
      left = mid + 1 // [mid + 1, right]
    } else {
      // num[mid] = target时,需要收缩右端点
      // 收缩左端点的话,会找不到下界,如[8, 8, 8, 8], target=8
      right = mid - 1 // [left, mid - 1]
    }
  }
  // 1. 数组中存在正常下界,此时left的位置为目标值的下界
  // 2. 数组元素均小于target,此时left为nums.length
  // 3. 数组元素均大于target,此时left为0
  return left
}

var searchRange = function(nums, target) {
  let start = low_bound(nums, target)
  if (start === - 1 || start === nums.length || nums[start] > target) return [-1, -1]
  return [start, low_bound(nums, target + 1) - 1]
};

寻找峰值

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。

你必须实现时间复杂度为 O(log n) **的算法来解决此问题

解法分析: 复杂度为 O(log n),已经在提示要使用二分法。另外需要注意,题目只要求找到其中一个峰值即可。

在每次循环时,比较mid和mid+1的关系

  • nums[mid] < nums[mid+1], 此时从mid向前,都属于上坡,峰值肯定在mid + 1后,left = mid + 1

  • nums[mid] > nums[mid+1],此时从mid + 1向后,都属于下坡,峰值肯定在mid之前。但是需要额外的判断right的下一个位置

    • right不能直接赋值为mid-1,因为mid可能是峰顶
    • 如果mid是峰顶, right = mid
    • 如果mid不是封顶,right = mid - 1
  • 循环结束条件:闭区间判断,left <= right

返回结果:

  • 数组元素递增时,left=nums.length
  • 其他情况,left为封顶位置
var findPeakElement = function(nums) {
  if (nums.length < 2) return 0
  let left = 0, right = nums.length - 2
  while (left <= right) {
    const mid = Math.floor((left + right) / 2)
    if (nums[mid] < nums[mid + 1]) {
      left = mid + 1
    } else {
      if (nums[mid] > nums[right]) {
        right = mid
      } else {
        right = mid - 1
      }
    }
  }
  return left === nums.length ? left - 1 : left
};

搜索旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题

解法分析:

对比条件:nums[nums.length - 1]

比较区间:左开右开

var findMin = function(nums) {
  if (nums.length < 2) return nums[0]
  let left  = -1, right = nums.length - 1
  while (left + 1 < right) {
    const mid = Math.floor((left + right) / 2)
    if (nums[mid] > nums[nums.length - 1]) {
      left = mid
    } else {
      right = mid
    }
  }
  return nums[right]
};