算法解释

二分查找也常被称为二分法或者折半查找,每次查找时通过将待查找区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。二分查找适用对象必须是排好序的数组。

具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在做题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

求开方

x 的平方根

给你一个非负整数 x,计算并返回 x 的算术平方根。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。

注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5。

示例 1:

输入:x = 4
输出:2

示例 2:

输入:x = 8
输出:2

在 [0, x] 区间二分查找。另外还有一个更快的牛顿迭代法 $x_{n+1}=x_n-\frac{f(x_n)}{f^{‘}(x_n)}$

代码

二分查找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int mySqrt(int a) {
if (a == 0)
return a;
int l = 1, r = a, mid, sqrt;
while (l <= r) {
mid = (l + r) / 2;
sqrt = a / mid;
if (sqrt == mid)
return mid;
else if (mid > sqrt)
r = mid - 1;
else
l = mid + 1;
}
return r;
}

牛顿迭代法:

1
2
3
4
5
6
int mySqrt(int a) {
long x = a;
while (x * x > a)
x = (x + a / x) / 2;
return x;
}

查找区间

查找元素始末位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

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

示例 3:

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

二分查找定位到目标值后向前向后扩大区间即可。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vector<int> range(vector<int>& nums, int mid, int target) {
int l = mid, r = mid;
while (l >= 0 && nums[l] == target)
l--;
while (r < nums.size() && nums[r] == target)
r++;
return vector<int>{l+1, r-1};
}
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty())
return vector<int> {-1, -1};
int l = 0, r = nums.size() - 1, mid;
while (l <= r) {
mid = (l + r) / 2;
if (nums[mid] == target)
return range(nums, mid, target);
else if (nums[mid] < target)
l = mid + 1;
else
r = mid - 1;
}
return vector<int> {-1, -1};
}

优化后(寻找上下边界也使用二分查找):

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
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty())
return vector<int>{-1, -1};
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target) - 1;
if (lower == nums.size() || nums[lower] != target) {
return vector<int>{-1, -1};
}
return vector<int>{lower, upper};
}
int lower_bound(vector<int> &nums, int target) {
int l = 0, r = nums.size(), mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] >= target)
r = mid;
else
l = mid + 1;
}
return l;
}
int upper_bound(vector<int> &nums, int target) {
int l = 0, r = nums.size(), mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] > target)
r = mid;
else
l = mid + 1;
}
return l;
}

旋转数组查找数字

搜索旋转排序数组II

已知存在一个按非降序排列的整数数组 nums,数组中的值不必互不相同。在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为了[nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如,[0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4]。

给你旋转后的数组 nums 和一个整数 target,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target,则返回 true,否则返回 false。

你必须尽可能减少整个操作步骤。

示例 1:

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

示例 2:

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

其实数组即使被旋转过一次,也并不影响二分查找的使用,如果中点值小于右端点,则右半区间为排好序的;若中点值等于右端点,不能确定,尝试右端点左移重新取中点;若中点值大于右端点,则左半区间为排好序的。继续二分查找即可。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool search(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1, mid;
while (l <= r) {
mid = (l + r) / 2;
if (nums[mid] == target)
return true;
if (nums[mid] < nums[r]) {
if (nums[mid] <= target && target <= nums[r])
l = mid + 1;
else
r = mid - 1;
}
else if (nums[mid] == nums[r])
r--;
else {
if (nums[l] <= target && target <= nums[mid])
r = mid - 1;
else
l = mid + 1;
}
}
return false;
}

练习

寻找旋转排序数组的最小值II

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次旋转后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
  • 若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]

注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]]。

给你一个可能存在重复元素值的数组 nums,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的最小元素。

你必须尽可能减少整个过程的操作步骤。

示例 1:

输入:nums = [1,3,5]
输出:1

示例 2:

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

别看题目旋转多少次,实际上是上一题的不同表述,同样是一直二分查找至最小值。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] < nums[r])
r = mid;
else if (nums[mid] == nums[r]) {
r--;
}
else
l = mid + 1;
}
return nums[l];
}

有序数组中的单一元素

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。请你找出并返回只出现一次的那个数。

你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

示例 1:

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

示例 2:

输入: nums = [3,3,7,7,10,11,11]
输出: 10

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int singleNonDuplicate(vector<int>& nums) {
int l = 0, r = nums.size() - 1, mid;
while (l < r) {
mid = (l + r) / 2;
if (nums[mid] == nums[mid - 1]) {
if (mid % 2 == 0)
r = mid - 2;
else
l = mid + 1;
}
else if (nums[mid] == nums[mid + 1]) {
if (mid % 2 == 0)
l = mid + 2;
else
r = mid - 1;
}
else
return nums[mid];
}
return nums[l];
}

寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。

算法的时间复杂度应该为 O(log (m+n))。

示例 1:

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000

示例 2:

输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000

根据中位数的定义,当 m+n 是奇数时,中位数是两个有序数组中的第 (m+n)/2 个元素,当 m+n 是偶数时,中位数是两个有序数组中的第 (m+n)/2 个元素和第 (m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1。

假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2-1] 和 B[k/2-1],有三种情况:

  • A[k/2-1] < B[k/2-1],则可以排除 A[0]~A[k/2-1]
  • A[k/2-1] > B[k/2-1],同理
  • A[k/2-1] = B[k/2-1],可以合并进 1 中

有以下三种情况需要特殊处理:

  • 如果 A[k/2-1] 或者 B[k/2-1] 越界,那么我们可以选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k 减去 k/2。
  • 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k小的元素。
  • 如果 k=1,我们只要返回两个数组首元素的最小值即可。
代码
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
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
int m = nums1.size();
int n = nums2.size();
int index1 = 0, index2 = 0;
while (true) {
if (index1 == m)
return nums2[index2 + k - 1];
if (index2 == n)
return nums1[index1 + k - 1];
if (k == 1)
return min(nums1[index1], nums2[index2]);
int newIndex1 = min(index1 + k / 2 - 1, m - 1);
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
int pivot1 = nums1[newIndex1];
int pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= newIndex1 - index1 + 1;
index1 = newIndex1 + 1;
}
else {
k -= newIndex2 - index2 + 1;
index2 = newIndex2 + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int totalLength = nums1.size() + nums2.size();
if (totalLength % 2 == 1)
return getKthElement(nums1, nums2, (totalLength + 1) / 2);
else
return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
}