我们通过一个题目来学习这个运用思路.
给定一个升序数组 nums, 写一个函数搜索 nums 中的 target, 如果 target 存在返回下标, 否则返回 -1. 注意, 这个数组的大小是未知的, 因此, 我们不能通过数组的长度来计算中间元素的位置. 你只可以通过 ArrayReader 接口来访问数组中的元素.
1interface ArrayReader {2get(index: number): number;3}
如果你访问的数组下标越界, ArrayReader 会返回 2^31 - 1.
1const reader = new ArrayReader([-1,0,3,5,9,12]);2reader.get(0); // return 03reader.get(3); // return 54reader.get(5); // return 125// 越界6reader.get(6); // return 2^31 - 1
在上一章中, 我们学习到的二分搜索, 是在数组长度已知的情况下, 对数组进行二分. 但是, 这里的题目是, 我们无法得知数组的长度, 因此, 我们不能通过数组的长度来计算中间元素的位置.
这里, 我们可以通过二分搜索的反向运用, 来尝试性查找大概的数组末尾. 那就是通过放大倍数, 来找到一个可以使用二分搜索的数组末尾. 找到的这个数组末尾可能为空, 但是不影响我们使用二分搜索的算法.
我们图示来演示这个思路
初始状态, 我们设计一个变量 right 来表示数组末尾. 设计一个变量 left 来表示数组起始位置. 初始值为 0 和 1.
1let left = 0;2let right = 1;

此时, 两个指针共同构成了一个区间数组, 这个区间数组可能包含 target, 也可能不包含 target.
然后我们通过判断 reader.get(right) 是否大于 target 来判断区间数组是否包含 target. 如果还没包含, 则将 right 放大倍数.
1while (reader.get(right) < target) {2right = right * 2;3}

这样, 我们可以快速找到一个必定包含 target 的区间数组. 我们只需要在这个区间数组中, 使用二分搜索, 就可以找到 target 的索引了.
完整的代码实现如下所示
01function search(reader: ArrayReader, target: number): number {02let left = 0;03let right = 1;04while (reader.get(right) < target) {05right = right * 2;06}07while (left <= right) {08const mid = Math.floor((left + right) / 2);09const midValue = reader.get(mid);10if (midValue === target) {11return mid;12} else if (midValue < target) {13left = mid + 1;14} else {15right = mid - 1;16}17}18return -1;19}
时间复杂度: O(log(n))
空间复杂度: O(1)