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
};