0%

二分查找刷题

按照算法和数据结构进行分类,一起来刷题,用于自己在面试前查漏补缺。我的意向岗位是前端,选择用javascript来刷题,优点是动态语言,语法简单,缺点是遇见复杂数据结构会出现较难的写法,如堆、并查集,每题对应leetcode的题号。本篇是二分查找

专题部分

二分查找

33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

示例 1:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

示例 2:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1

示例 3:

1
2
输入:nums = [1], target = 0
输出:-1

提示:

  • 1 <= nums.length <= 5000
  • -10^4 <= nums[i] <= 10^4
  • nums 中的每个值都 独一无二
  • 题目数据保证 nums 在预先未知的某个下标上进行了旋转
  • -10^4 <= target <= 10^4

进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?

二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while(left <= right){
let mid = (left + right + 1) >> 1;
if (nums[mid] == target) return mid;
if (nums[left] < nums[mid]){ // 左边升序
if (nums[left] <= target && target < nums[mid]){ // 在左边范围内
right = mid -1;
} else { // 只能从右边找
left = mid + 1;
}
} else { // 右边升序
if (nums[mid] < target && target <= nums[right]){ // 在右边范围内
left = mid + 1;
} else { // 只能从左边找
right = mid -1;
}
}
}
return -1; // 没找到
};

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

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

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

进阶:

  • 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

1
2
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

1
2
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

1
2
输入:nums = [], target = 0
输出:[-1,-1]

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

分别找下边界和上边界,注意两种的判断条件是不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
// 先找左边界
// 左闭右闭
let left1 = 0, right1 = nums.length - 1;
// 左>右找到该点
while (left1 <= right1) {
// 中间点
let mid = left1 + ((right1 - left1) >> 1);
if (nums[mid] === target) {
// 中间点为目标,右边界移至中点减一的位置
right1 = mid - 1;
} else if (nums[mid] > target) {
// 中间点大于目标,右边界移至中点减一的位置
right1 = mid - 1;
} else if (nums[mid] < target) {
// 中间点小于目标,左边界移至中点减一的位置
left1 = mid + 1;
}
}
// left1为下边界
if (left1 >= nums.length || nums[left1] !== target) {
// 不存在目标值 target
return [-1, -1];
}

// 找右边界
// 左闭右闭
let left2 = 0, right2 = nums.length - 1;
// 左>右找到该点
while (left2 <= right2) {
// 中间点
let mid = left2 + ((right2 - left2) >> 1);
if (nums[mid] === target) {
// 中间点为目标,左边界移至中点加一的位置
left2 = mid + 1;
} else if (nums[mid] > target) {
// 中间点大于目标,右边界移至中点减一的位置
right2 = mid - 1;
} else if (nums[mid] < target) {
// 中间点小于目标,左边界移至中点加一的位置
left2 = mid + 1;
}
}
// right2为上边界
return [left1, right2];
};

将两个判断条件写成函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var searchRange = function(nums, target) {
// 先找左边界
// 左闭右闭
let left1 = 0, right1 = nums.length - 1;
// 左>右找到该点
while (left1 <= right1) {
// 中间点
let mid = left1 + ((right1 - left1) >> 1);
if (nums[mid] === target) {
// 中间点为目标,右边界移至中点减一的位置
right1 = mid - 1;
} else if (nums[mid] > target) {
// 中间点大于目标,右边界移至中点减一的位置
right1 = mid - 1;
} else if (nums[mid] < target) {
// 中间点小于目标,左边界移至中点减一的位置
left1 = mid + 1;
}
}
// left1为下边界
if (left1 >= nums.length || nums[left1] !== target) {
// 不存在目标值 target
return [-1, -1];
}

// 找右边界
// 左闭右闭
let left2 = 0, right2 = nums.length - 1;
// 左>右找到该点
while (left2 <= right2) {
// 中间点
let mid = left2 + ((right2 - left2) >> 1);
if (nums[mid] === target) {
// 中间点为目标,左边界移至中点加一的位置
left2 = mid + 1;
} else if (nums[mid] > target) {
// 中间点大于目标,右边界移至中点减一的位置
right2 = mid - 1;
} else if (nums[mid] < target) {
// 中间点小于目标,左边界移至中点加一的位置
left2 = mid + 1;
}
}
// right2为上边界
return [left1, right2];
};

278. 第一个错误的版本

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例:

1
2
3
4
5
6
7
给定 n = 5,并且 version = 4 是第一个错误的版本。

调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true

所以,4 是第一个错误的版本。

其实例子已经给了,关键是返回一个函数,该函数接收一个参数n,再进行二分查找判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Definition for isBadVersion()
*
* @param {integer} version number
* @return {boolean} whether the version is bad
* isBadVersion = function(version) {
* ...
* };
*/

/**
* @param {function} isBadVersion()
* @return {function}
* 二分查找
*/
var solution = function(isBadVersion) {
/**
* @param {integer} n Total versions
* @return {integer} The first bad version
*/
return function(n) {
// 二分法标志位,去寻找第一个isBadVersion结果为true的值
let left = 1, right = n;
// 搜索范围是一个闭区间
while (left < right) {
// 中间值
const mid = left + ((right - left) >> 1);
// 判断中间值是否错误
if (isBadVersion(mid)) {
// 排除右边所有
right = mid;
} else {
// 排除左边所有
left = mid + 1;
}
}
return left;
};
};

374. 猜数字大小

猜数字游戏的规则如下:

  • 每轮游戏,我都会从 1n 随机选择一个数字。 请你猜选出的是哪个数字。
  • 如果你猜错了,我会告诉你,你猜测的数字比我选出的数字是大了还是小了。

你可以通过调用一个预先定义好的接口 int guess(int num) 来获取猜测结果,返回值一共有 3 种可能的情况(-110):

  • -1:我选出的数字比你猜的数字小 pick < num
  • 1:我选出的数字比你猜的数字大 pick > num
  • 0:我选出的数字和你猜的数字一样。恭喜!你猜对了!pick == num

返回我选出的数字。

示例 1:

1
2
输入:n = 10, pick = 6
输出:6

示例 2:

1
2
输入:n = 1, pick = 1
输出:1

示例 3:

1
2
输入:n = 2, pick = 1
输出:1

示例 4:

1
2
输入:n = 2, pick = 2
输出:2

提示:

  • 1 <= n <= 231 - 1
  • 1 <= pick <= n

凡是这种不考虑leftbound和rightbound的都还算简单的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/** 
* Forward declaration of guess API.
* @param {number} num your guess
* @return -1 if num is lower than the guess number
* 1 if num is higher than the guess number
* otherwise return 0
* var guess = function(num) {}
* 二分查找直到查找需要的数
*/

/**
* @param {number} n
* @return {number}
*/
var guessNumber = function(n) {
// 左右闭区间
let left = 1, right = n;
// 循环直至区间左右端点相同
while (left < right) {
const mid = left + ((right - left) >> 1);
if (guess(mid) === 0) {
return mid;
} else if (guess(mid) < 0) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
};

852. 山脉数组的峰顶索引

符合下列属性的数组 arr 称为 山脉数组

  • arr.length >= 3

  • 存在

    i(0 < i < arr.length - 1)使得:

    • arr[0] < arr[1] < ... arr[i-1] < arr[i]
  • arr[i] > arr[i+1] > ... > arr[arr.length - 1]

给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i

示例 1:

1
2
输入:arr = [0,1,0]
输出:1

示例 2:

1
2
输入:arr = [0,2,1,0]
输出:1

示例 3:

1
2
输入:arr = [0,10,5,2]
输出:1

示例 4:

1
2
输入:arr = [3,4,5,1]
输出:2

示例 5:

1
2
输入:arr = [24,69,100,99,79,78,67,36,26,19]
输出:2

提示:

  • 3 <= arr.length <= 104
  • 0 <= arr[i] <= 106
  • 题目数据保证 arr 是一个山脉数组

进阶:很容易想到时间复杂度 O(n) 的解决方案,你可以设计一个 O(log(n)) 的解决方案吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param {number[]} arr
* @return {number}
* 二分查找
*/
var peakIndexInMountainArray = function(arr) {
let left = 0, right = arr.length - 1;
while (left < right) {
const mid = left + ((right - left) >> 1);
if (arr[mid - 1] > arr[mid]) {
right = mid - 1;
} else if (arr[mid + 1] > arr[mid]) {
left = mid + 1;
} else {
return mid;
}
}
return left;
};
-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道