JavaScript 二分查找的一些注意细节

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
const target = 3

const binarySearch = (arr, target) => {
  let l = 0, r = arr.length - 1
  while (l <= r) {	// 为何要 = ? 	如 [1] 中找 1, 无等号都进不去
    // mid 表示当前查找的索引
    const mid = (l + r) >>> 1		// 位运算, 性能好; 一开始, 一定有	left <= mid <= right
    if (arr[mid] === target) return mid
    // 为下一次循环做准备, 相当于根据索引削去数组, 如代码下方所示
    // 当前查找的数 < 实际的 target 	=>  说明 target 较大, 在右边, 因此 left = mid + 1
    // 箭头所指 < target    =>    l = mid + 1	(见下方解释)
    else if (arr[mid] < target) l = mid + 1		// 如果 left = mid = right, 将会导致 left > right, 跳出循环
    else r = mid - 1		// 如果 left = mid = right, 将会导致 right < left, 跳出循环
  }
  return false
}

console.log(binarySearch(arr, target));

每次循环, 数组的范围 (可看成根据索引削去数组中的一部分)
[ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
[ 1, 2, 3, 4 ]
[ 3, 4 ]

else if (array[mid] < target) l = mid + 1
else r = mid - 1

这块可能有点绕, 我写反过几次, -.-

然后, 每当看到 arr[mid], 就将其等价于箭头所指

当你看到 arr[mid] < target, 脑中想到类似这个画面, 然后就知道 left = 箭头所指 + 1
[1, 2, 3, 4, 5, 6, 7, 8, 9]
             ↑   target

当你看到 arr[mid] > target, 脑中想到类似这个画面, 然后就知道 right = 箭头所指 - 1
[1, 2, 3, 4, 5, 6, 7, 8, 9]
     target  ↑

删除注释版本

const binarySearch = (arr, target) => {
  let l = 0, r = arr.length - 1
  while (l <= r) {
    const mid = (l + r) >>> 2
    if (arr[mid] === target) return mid
    else if (arr[mid] < target) l = mid + 1
    else r = mid - 1
  }
  return false
}

注意如下注释

else if (arr[mid] < target) l = mid + 1		// 如果 left = mid = right, 将会导致 left > right, 跳出循环
else r = mid - 1		// 如果 left = mid = right, 将会导致 right < left, 跳出循环

当其跳出循环时, 意味着 left = right + 1, 只有符合这个条件, 才能跳出循环

看如下例子, 在只有一个数字的数组[3]中寻找 1 和 5 , 最后跳出循环时的索引情况

  [3]    target = 1
r  l 	 跳出循环时的索引
-1 0	 索引值

[3]   	 target = 5
 r  l 	 跳出循环时的索引
 0  1	 索引值

关于这个的一道算法题
300. 最长递增子序列

方法一:动态规划

nums[j] < nums[i] => dp[i] = max(dp[i], dp[j] + 1)

var lengthOfLIS = function (nums) {
  let dp = new Array(nums.length).fill(1)
  let res = 1
  // 从 dp[1] 开始, 因为 dp[0] 必然为 1
  for (let i = 1; i < nums.length; i++) {
    // 遍历 i 之前所有元素
    for (let j = 0; j < i; j++) {
      // 如果当前元素 > 遍历的元素大, 比较 dp[i] (当前元素) 与 dp[j] + 1 (遍历元素)的值, 将较大值赋给 dp[i]
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    res = Math.max(res, dp[i])
  }
  return res
};

console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]))

方法二:贪心 + 二分查找

维护一个最长子序列的数组

  • dp[len - 1] < nums[i] 直接添加
  • 否则 找到 dp[i - 1] < nums[j] < dp[i] 改变 dp[i] = nums[j]

以输入序列 [0, 8, 4, 12, 2] 为例

  • 第一步插入 0,d = [0]
  • 第二步插入 8,d = [0, 8]
  • 第三步插入 4,d = [0, 4]
  • 第四步插入 12,d = [0, 4, 12]
  • 第五步插入 2,d = [0, 2, 12]

虽然最长子序列的数组 [0,4,12] 但之后有了可以使其扩充的更小的数,即使更改了数组,却未改变数组的长度

思考, 使用二分查找法, 从一个数组中, 找到一个刚好比 target 要大的数

从数组 [1, 5, 8] 中找到一个刚好比 3 大的数

[1, 5, 8]    target = 3
 r  l 		   跳出循环时的索引

从数组 [1, 3, 8] 中找到一个刚好比 3 大的数

[1, 3, 8]    target = 3
 r  l 		   跳出循环时的索引

可以看到, 当跳出循环时, left 的值就是要找的索引
并且, 箭头所指 > target箭头所指 = target 都要将 right = mid - 1, 即

if (arr[mid] < target) l = mid + 1
else r = mid - 1
var lengthOfLIS = function (nums) {
  let arr = [nums[0]]
  for (let i = 1; i < nums.length; i++) {
    const target = nums[i];
    if (arr[arr.length - 1] < target) {
      arr.push(target)
    } else {  // 找到一个比 target 大的数, 然后替换它
      let l = 0, r = arr.length - 1
      while (l <= r) {
        let mid = (l + r) >>> 1
        if (arr[mid] < target) l = mid + 1
        else r = mid - 1
      }
      arr[l] = target
    }
  }
  return arr.length
};

console.log(lengthOfLIS([18, 55, 66, 2, 3, 54]))
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]))
console.log(lengthOfLIS([3, 5, 6, 2, 5, 4, 19, 5, 6, 7, 12]))
console.log(lengthOfLIS([7, 7, 7, 7, 7]))

同版写法

var lengthOfLIS = function (nums) {
  const arr = [nums[0]]
  nums.forEach(target => {
    if (arr[arr.length - 1] < target) {
      arr.push(target)
    } else {
      let l = 0, r = arr.length - 1
      while (l <= r) {
        const mid = (l + r) >>> 1
        if (arr[mid] < target) l = mid + 1
        else r = mid - 1
      }
      arr[l] = target
    }
  })
  return arr.length
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值