Leetcode刷题记录之二分查找
34. 在排序数组中查找元素的第一个和最后一个位置
题目描述
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
输入输出
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
题解
设计两个函数,lower_bound
和 upper_bound
,这俩函数的功能和 STL
中同名函数的功能一样。
lower_bound
lower_bound
用于找出 第一个 小于等于 target 的值,如果数组中所有值都大于 target,则返回 -1。
使用二分查找,如果数组中有多个 值 和 target 相等,比如 nums = [5,7,7,8,8,10]
,target = 8
,因为要找出最左边的那个 8,所以当遇到当前值和 target 相等时,要继续往左搜索,即令 right = mid - 1
。二分查找结束时,left
要么指向第一个 小于等于 target 的值,要么指向 nums.size()
。
upper_bound
upper_bound
用于找出 第一个 大于等于 target 的值,如果数组中所有值都小于 target,则返回-1。
当遇到当前值和 target 相等时,要继续往 右 搜索,即令 left = mid + 1
。二分查找结束时,right
要么指向第一个 大于等于 target 的值,要么指向 nums.size()
。
代码
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int low_idx = lower_bound(nums, target);
if (low_idx != -1 && nums[low_idx] == target) {
int up_idx = upper_bound(nums, target);
return {low_idx, up_idx};
}
return {-1, -1};
}
int lower_bound(vector<int> &nums, int target) {
int left = 0, right = static_cast<int>(nums.size()) - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (target <= nums[mid]) right = mid - 1;
else left = mid + 1;
}
return left >= nums.size() ? -1 : left;
}
int upper_bound(vector<int> &nums, int target) {
int left = 0, right = static_cast<int>(nums.size()) - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (target >= nums[mid]) left = mid + 1;
else right = mid - 1;
}
return right < 0 ? -1 : right;
}
};
刷题记录
一刷 | 2022年8月7日 |
---|
81. 搜索旋转排序数组 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 。
你必须尽可能减少整个操作步骤。
输入输出
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
题解
使用二分查找,每次二分时,当前子序列都可以分为左右两半部分,左右两半部分一定有一个是严格非递减的。分三种情况讨论:
- 当
nums[right] == nums[mid]
时,比如10111
和11101
这种。此种情况下分不清到底是前面有序还是后面有序,此时left++
即可。相当于去掉一个重复的干扰项。 - 当
nums[right] < nums[mid]
时,比如2 3 4 5 6 7 1
,2 < 5
这种。这种情况下,前半部分是有序的。- 因此如果
nums[left] <= target < nums[mid]
,则在前半部分找 - 否则去后半部分找。
- 因此如果
- 当
nums[right] > nums[mid]
时,6 7 1 2 3 4 5
,6 > 2
这种。这种情况下,后半部分有序。- 因此如果
nums[mid] <target <= nums[right]
。则在后半部分找。 - 否则去前半部分找。
- 因此如果
代码
class Solution {
public:
bool search(vector<int>& nums, int target) {
int sz = static_cast<int>(nums.size());
int left = 0, right = sz - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] == target || nums[left] == target || nums[right] == target) return true;
if (nums[mid] == nums[right]) { // 无法判断左半边有序还是右半边有序
++left;
} else if (nums[mid] < nums[right]) { // 右半边有序
if (nums[mid] < target && target < nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
} else { // 左半边有序
if (nums[left] < target && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
}
return false;
}
};
刷题记录
一刷 | 2022年8月8日 |
---|
154. 寻找旋转排序数组中的最小值
题目描述
已知一个长度为 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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
输入输出
输入:nums = [1,3,5]
输出:1
题解
同样将数组分为左右两部分,最小值一定在左右两部分的分界点,即右半部分的起始位置处。假设我们要找的这个起始点为 i
。
-
当
nums[mid] > nums[right]
时,mid
一定在第 1 个排序数组中,i
一定满足mid < i <= right
,因此执行left = mid + 1
; -
当
nums[mid] < nums[right]
时,mid
一定在第 2 个排序数组中,i
一定满足left < i <= mid
,因此执行right = mid
; -
当
nums[mid] == nums[right]
时,此题中数组的元素可重复,难以判断分界点i
指针区间,我们令right = right - 1
,缩小搜索区间;
最后,left
指针一定指向起始点 i
。
代码
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = static_cast<int>(nums.size()) - 1;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == nums[right]) { // 无法判断 mid 在哪个区间
--right;
} else if (nums[mid] < nums[right]) { // mid 在右区间
right = mid;
} else { // mid 在左区间
left = mid + 1;
}
}
return nums[left];
}
};
刷题记录
一刷 | 2022年8月8日 |
---|
540. 有序数组中的单一元素
题目描述
给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。
请你找出并返回只出现一次的那个数。
你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。
输入输出
输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2
题解
出现一次的那个元素是个分界点,
- 当
nums[mid] = nums[mid + 1]
时- 对于分界点左边的元素,其下标为偶数
- 对于分界点右边的元素,其下标为奇数
- 当
nums[mid] = nums[mid - 1]
时- 对于分界点左边的元素,其下标为奇数
- 对于分界点右边的元素,其下标为偶数
代码
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int left = 0, right = static_cast<int>(nums.size()) - 1;
while (left <= right) {
int mid = (left + right) / 2;
int left_num = mid == 0 ? nums[mid] - 1 : nums[mid - 1];
int right_num = mid == nums.size() - 1 ? nums[mid] + 1 : nums[mid + 1];
if (nums[mid] != left_num && nums[mid] != right_num) {
return nums[mid];
} else if ((nums[mid] == left_num && mid % 2 == 0) || (nums[mid] == right_num && mid % 2 == 1)) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return 0;
}
};
刷题记录
一刷 | 2022年8月9日 |
---|
4. 寻找两个正序数组的中位数
题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
输入输出
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
题解

如上图所示,用一条线分割把两个数组分别分割成两个部分,这个分割线满足两个条件:
-
分割线左边和右边的元素个数相等,或者左边比右边元素多 1 个。
- 当两数组总元素个数为偶数时,左边元素个数 = 右边元素个数, 中位数就是左边的元素最大值(比较一下分割线左边上下两个数组的最大值就能找出来)。比如下图,比较 6 和 7 两个元素就可以。
- 当两数组总元素个数为奇数时,左边元素个数 = 右边元素个数 + 1。左边元素最大值和右边元素最小值加一起取平均值就是中位数。比如下图。
-
分割线左边所有元素的数值 <= 分割线右边所有元素的数值。
只要能找到这么一条分割线,那么就能确定中位数。寻找这条分割线使用二分查找。
如何保持条件1
假设数组 1 的长度为 m,数组 2 的长度为 n。
当 m + n 为偶数时,$size_{left} = \frac{m + n}2 = \frac{m + n + 1}2$;因为 m + n 为偶数,所以$\frac{m + n}2 = \frac{m + n + 1}2$;
当 m + n 为奇数时,$size_{left} = \frac{m + n + 1}2$;
这样,我们不用分奇偶讨论,只需要确定一个数组的分割线位置,另一个数组的分割线位置就能通过公式计算出来。即,假设第一个数组$size_{left1} = x$,那么第二个数组$size_{left2} = \frac{m + n + 1}2 - x$。
为了防止数组访问越界的情况发生,应该始终通过二分查找先确定长度较小的那个数组的分割线位置,再通过公式计算长度较大的那个数组的分割线位置。
比如,假设数组 1 的长度为 10,数组 2 的长度为 4。如果先确定数组 1 $size_{left1} = 2$,通过计算数组 2 $size_{left2} = \frac{10 + 4 + 1}2 - 2 = 5$,很显然,5 已经超出了数组2 的长度,会出现越界的情况。
如何保持条件2
由于两个数组都是有序数组,在同一个数组内,分割线一定满足左边的所有元素小于等于右边的所有元素。
在不同的数组之间,应该保证交叉小于等于关系成立,即:
-
分割线左边第 1 个数组的最大值 <= 分割线右边第 2 个数组的最小值
-
分割线左边第 2 个数组的最大值 <= 分割线右边第 1 个数组的最小值
如下图:

只要不符合交叉小于等于关系,就要适当调整分割线位置。
举例1
比如下图,左边的 8 大于右边的 6,不符合交叉小于等于关系的第 2 种情况。
调整方案:将中位数分割线在数组 1 的位置右移。

举例2
比如下图,左边的 8 大于右边的 7,右边的数太小,不符合交叉小于等于关系的第 1 种情况。
调整方案:将中位数分割线在数组 1 的位置左移。
代码
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// 始终让nums1是长度较小的那个数组
if (nums1.size() > nums2.size()) {
return findMedianSortedArrays(nums2, nums1);
}
int m = static_cast<int >(nums1.size());
int n = static_cast<int >(nums2.size());
int left = 0, right = m;
// left_max:前一部分的最大值
// right_min:后一部分的最小值
int left_max = INT32_MIN, right_min = INT32_MAX;
while (left <= right) {
// 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
// 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
int i = (left + right) / 2;
int j = (m + n + 1) / 2 - i;
// nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
int nums_im1 = (i == 0 ? INT32_MIN : nums1[i - 1]);
int nums_i = (i == m ? INT32_MAX : nums1[i]);
int nums_jm1 = (j == 0 ? INT32_MIN : nums2[j - 1]);
int nums_j = (j == n ? INT32_MAX : nums2[j]);
left_max = std::max(nums_im1, nums_jm1);
right_min = std::min(nums_i, nums_j);
if (nums_im1 > nums_j) { // 不满足交叉小于条件1
right = i - 1;
} else if (nums_jm1 > nums_i) { // 不满足交叉小于条件2
left = i + 1;
} else { // 成功找到分割点
break;
}
}
return (m + n) % 2 == 0 ? (left_max + right_min) / 2.0 : left_max;
}
};
刷题记录
一刷 | 2022年8月10日 |
---|