二分查找
使用二分查找的前提
模板
常见的二分查找应用比如猜数字游戏。
// 二分查找适用于有序的数组
// 这个一个最简单的二分查找算法,前提是数组中不存在重复元素
function binarySearch(arr, target) {
if (arr && Array.isArray(arr)) {
if (!arr.length) return -1
let left = 0
let right = arr.length - 1
while (left <= right) {
// let mid = Math.floor((left + right) / 2)
let mid = Math.floor(left + (right - left)/2) // 防止left, right过大相加导致数值溢出
if (arr[mid] === target) {
return mid
}
if (arr[mid] > target) {
right = mid - 1
} else {
left = mid + 1
}
}
return -1
}
}
// 二分查找除了使用循环实现以外还可以使用递归实现
function bsearch(arr, target) {
if (arr && Array.isArray(arr)) {
return bsearchInternally(arr, 0, arr.length - 1, target)
}
}
function bsearchInternally(arr, left, right, target) {
if (left > right) return -1
let mid = Math.floor(left + (right - left) / 2)
if (arr[mid] === target) {
return mid
}
if (arr[mid] > target) {
return bsearchInternally(arr, left, mid - 1, target)
} else {
return bsearchInternally(arr, mid + 1, right, target)
}
}
console.log(binarySearch([8, 11, 19, 23, 27, 33, 45, 55, 67, 98], 19)) // 2
console.log(binarySearch([8, 11, 19, 23, 27, 33, 45, 55, 67, 98], 20)) // -1
console.log(binarySearch([8, 11, 19, 23, 27, 33, 45, 55, 67, 98], 33)) // 5
console.log(bsearch([8, 11, 19, 23, 27, 33, 45, 55, 67, 98], 33)) // 5
这里关于二分查找算法的时间复杂度分析也很好理解:
我们假设数据规模为n,每一次查找数据规模会缩减1/2,最坏情况下即缩减空间为0才停止。
可以看出来,这是一个等比数列。其中 n/2 =1 时,k 的值就是总共缩小的次数。而每一次 缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。通过 n/2 =1,我们可以求得 k=log n,所以时间复杂度就是 O(logn)。
总结: 二分查找算法针对的是一个有序的数据集合,查找思想类似分治思想,每次都通过跟中间数进行对比来缩小数据规模,将待查找的数据规模缩微之前的一半,直到找到目标元素,或者查找区间被缩减为0。
二分查找的应用场景
- 首先二分查找依赖的是顺序表结构,简单说就是数组,因为二分查找需要按照下标索引随机访问元素,数组随机访问元素时间复杂度是O(1)。
- 其次二分查找针对的是有序数据,如果数据不是有序的,那么我们首先要先排序,我们知道排序最好情况时间复杂度是O(nlogn)。如果我们针对的是一组静态数据,不需要频繁的删除,插入等操作,那么我们只需要进行一次排序,多次二分查找即可,这样排序的效率可以被均摊,性能尚能接受。如果我们的数据集合需要频繁的插入,删除操作,要想用二分查找,那么我们需要保证每次删除,插入操作后数据有序,要么在每次二分查找之前先排序,针对这样的动态数据,无论哪种方式维护成本都太高。
所以二分查找更适合于静态数据场景或者少插入,删除操作,一次排序多次查找的场景。
- 数据量太小也不适合二分查找,如果数据量太少其实用顺序查找与二分查找效率相差无几,不过有一种例外的情况,如果数据之间的比较操作非常耗时,不管数据量大小都推荐使用二分查找。因为我们需要尽可能的减少比较次数,这样可以提高性能。
- 数据量太多也不适合二分查找算法,因为二分查找底层依赖于数组这样的数据结构,而数组在内存中是连续存储的,比较耗内存。
变形的二分查找
我们前面学习的都是非常简单的二分查找,在不存在重复元素的有序数组中,查找值等于给定值的元素。
- 查找第一个值等于给定值的元素
const binaryFirst = (arr, target) => {
if (Array.isArray(arr)) {
const len = arr.length
if (!len) return -1
let low = 0
let high = len - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (arr[mid] < target) {
low = mid + 1
} else if (arr[mid] > target) {
high = mid - 1
} else {
// 需要判断当前值是否是第一个相等的值
if (mid === 0 || arr[mid - 1] < target) return mid
high = mid - 1
}
}
}
return -1
};
// test case
console.log(binaryFirst([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 8)) // 5
console.log(binaryFirst([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 11)) // 8
console.log(binaryFirst([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 19)) // -1
console.log(binaryFirst([1, 4, 4, 5, 6, 8, 8, 8, 11, 18], 4)) // 1
console.log(binaryFirst([1], 1)) // 0
console.log(binaryFirst([1, 1], 1)) // 0
console.log(binaryFirst([1], 0)) // -1
这里主要难点在于判断当前值是否是第一个相等的值。
1. 如果mid === 0 说明这个元素是数组第一个元素 那么肯定是第一个
2. 如果 mid 不等于 0,但 arr[mid] 的前一个元素 arr[mid-1] 不等于value,那也说明 arr[mid] 就是我们要找的第一个值等于给定值的元素。
3. 如果经过检查之后发现 a[mid] 前面的一个元素 a[mid-1] 也等于 value,那说明此时的a[mid] 肯定不是我们要查找的第一个值等于给定值的元素。那我们就更新 high=mid-1,因为要找的元素肯定出现在 [low, mid-1] 之间。
- 查找最后一个值等于给定值的元素
const binaryLast = (arr, target) => {
if (Array.isArray(arr)) {
const len = arr.length
if (!len) return -1
let low = 0
let high = len - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (arr[mid] < target) {
low = mid + 1
} else if (arr[mid] > target) {
high = mid - 1
} else {
if ((mid === len - 1) || (arr[mid + 1] > target)) return mid
low = mid + 1
}
}
}
return -1
};
// test case
console.log(binaryLast([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 8)) // 7
console.log(binaryLast([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 11)) // 8
console.log(binaryLast([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 19)) // -1
console.log(binaryLast([1, 4, 4, 5, 6, 8, 8, 8, 11, 18], 4)) // 2
console.log(binaryLast([1], 1)) // 0
console.log(binaryLast([1, 1], 1)) // 0
console.log(binaryLast([1], 0)) // -1
console.log(binaryLast([1, 3, 4, 5, 6, 8, 8, 8, 11, 18, 18], 18)) // 10
- 如果mid === len - 1,说明已经到了元素已经到了数组末尾,肯定是最后一个相同元素了。
- 如果arr[mid +1]> target,说明已经是最后一个相同元素了。
- 如果还不是最后一个元素,那么继续往后扫描,最后一个相同元素范围肯定是[mid + 1, high]。
- 查找第一个大于等于给定值的元素
const binaryFindFistBig = (arr, target) => {
if (Array.isArray(arr)) {
const len = arr.length
if (!len) return -1
let low = 0
let high = len - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (arr[mid] >= target) {
// 如果mid === 0 说明数组第一个元素满足情况
// 如果arr[mid - 1] < target 说明如果当前元素前一个元素比目标值小那么当前元素肯定第一个满足条件的
if ((mid === 0) || arr[mid - 1] < target) return mid
high = mid - 1
} else {
low = mid + 1
}
}
}
return -1
}
// test case
console.log(binaryFindFistBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 7)) // 5
console.log(binaryFindFistBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 8)) // 5
console.log(binaryFindFistBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 19)) // -1
console.log(binaryFindFistBig([1, 4, 4, 5, 6, 8, 8, 8, 11, 18], 4)) // 1
console.log(binaryFindFistBig([1], 1)) // 0
console.log(binaryFindFistBig([1, 1], 1)) // 0
console.log(binaryFindFistBig([1], 0)) // 0
console.log(binaryFindFistBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18, 18], 18)) // 9
- 查找最后一个小于等于给定值的元素
const binaryFindLastBig = (arr, target) => {
if (Array.isArray(arr)) {
const len = arr.length
if (!len) return -1
let low = 0
let high = len - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (arr[mid] > target) {
high = mid - 1
} else {
if ((mid === len - 1) || arr[mid + 1] > target) return mid
low = mid + 1
}
}
}
return -1
}
// test case
console.log(binaryFindLastBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 7)) // 4
console.log(binaryFindLastBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 8)) // 7
console.log(binaryFindLastBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18], 19)) // 9
console.log(binaryFindLastBig([1, 4, 4, 5, 6, 8, 8, 8, 11, 18], 4)) // 2
console.log(binaryFindLastBig([1], 1)) // 0
console.log(binaryFindLastBig([1, 1], 1)) // 1
console.log(binaryFindLastBig([1], 0)) // -1
console.log(binaryFindLastBig([1, 3, 4, 5, 6, 8, 8, 8, 11, 18, 18], 18)) // 10