二分查找时间复杂度o(log n)
一、非递减顺序数组目标值的边界位置
1. 题目描述:
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
2. 分析
左边界:在nums[mid] == target
时,另right = mid - 1
,继续在[left,mid-1]
范围寻找target
的左边界,直到[left,mid-1]
范围内没有target,此时最后一次nums[mid] == target
的mid
即为左边界。
左边界:在nums[mid] == target
时,另left = mid + 1
,继续在[mid+1, right]
范围寻找target
的右边界,直到[mid+1, right]
范围内没有target,此时最后一次nums[mid] == target
的mid
即为右边界。
3. 题解
class Solution {
public:
int searchLeftRange(vector<int>& nums, int target, bool ifLeft)
{
int left = 0, right = nums.size() - 1;
int mid = 0, res = -1;
while(left <= right)
{
mid = left + (right - left) / 2;
if(nums[mid] < target)
left = mid + 1;
else if(nums[mid] > target)
right = mid - 1;
else
{
res = mid;
if(ifLeft)
right = mid - 1; //继续在[left,mid-1]范围寻找目标值target
else
left = mid + 1; //继续在[mid+1,right]范围寻找目标值target
}
}
return res;
}
vector<int> searchRange(vector<int>& nums, int target) {
return {searchLeftRange(nums, target, true), searchLeftRange(nums, target, false)};
}
};
二、寻找旋转排序数组中的最小值
1. 题目描述:
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
2. 分析
主要思路:用二分法查找,始终将目标值(这里是最小值)套住,并不断收缩左边界或右边界,循环条件left < right
,当left = right
时找到最小值,下标为left或right,又因为mid = left + (right - left) / 2
地板除,mid永远不可能等于right
if条件判断:
- 中值>右值:有旋转,如
[4,5,6,7,0,1,2]
,且mid一定不是最小值,最小值范围[mid+1, right]
,故收缩左边界left = mid + 1
- 中值<右值:
(1)无旋转,如[0,1,2,4,5,6,7]
,最小值范围[left, mid]
,收缩有边界
(2)有旋转,如[5,6,7,0,1,2,3]
,但mid有可能是最小值,最小值范围[left, mid]
,故收缩边界right = mid
3. 题解
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
int mid = 0;
while(left < right)
{
mid = left + (right - left) / 2;
if(nums[mid] > nums[right])
left = mid + 1; //收缩左边界
else
right = mid; //收缩右边界
}
return nums[left]; //此时left=right,left和right均是最小值的索引
}
};
4. 扩展
可能存在重复元素值的数组,非降序排序进行旋转
与无重复元素值题目的区别:可能存在mid == right
的情况
if条件判断:
-
中值>右值:有旋转,如
[4,5,6,6,0,1,2]
,且mid一定不是最小值,最小值范围[mid+1, right]
,故收缩左边界left = mid + 1
-
中值<右值:
(1)无旋转,如[0,1,2,4,5,6,6]
,最小值范围[left, mid]
,收缩右边界
(2)有旋转,如[5,6,6,0,1,2,3]
或[6,6,0,1,2,3,5]
,但mid有可能是最小值,最小值范围[left, mid]
,故收缩右边界right = mid
-
中值=右值:如
[5,6,6,6,6]
或[1,1]
,右值的左边一定还有最小值(也可能是重复的最小值),最小值范围[left, right-1]
,故收缩右边界right--
题解:
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
int mid = 0;
while(left < right)
{
mid = left + (right - left) / 2;
if(nums[mid] > nums[right])
left = mid + 1;
else if(nums[mid] < nums[right])
right = mid;
else
right--;
}
return nums[left];
}
};
三、搜索旋转排序数组
1. 题目描述:
整数数组 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,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
2. 分析:
旋转后仍部分有序,从mid
分成两部分[left, mid)
和(mid, right]
必有一部分是有序的。
先判断nums[mid]
和nums[right]
的关系,若nums[mid] > nums[right]
,说明[left, mid)
部分有序,判断target
是否在[nums[left], nums[mid])
范围内,再在[left, mid-1]
或[mid+1, right]
范围内搜索。
否则,nums[mid] <= nums[right]
,说明(mid, right]
部分有序,判断target
是否在(nums[mid], nums[right]]
范围内,再在[left, mid-1]
或[mid+1, right]
范围内搜索。
3. 题解:
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
int mid = 0;
while(left <= right)
{
mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
if(nums[mid] > nums[right])
{
if(target >= nums[left] && target < nums[mid])
right = mid - 1;
else
left = mid + 1;
}
else
{
if(target > nums[mid] && target <= nums[right])
left = mid + 1;
else
right = mid - 1;
}
}
return -1;
}
};
4. 扩展:
数组中的值不必互不相同
与数组中的值必相互相同的区别:
可能存在在nums[mid] = nums[right]
时,无法判断左或右是否有虚,如[1,0,1,1,1]
或[1,1,1,2,1]
,无法判断target
在[left, mid-1]
或[mid+1, right]
范围。
因此在nums[mid] = nums[right]
时,直接将右边界收缩right--
,再继续判断直至nums[mid] != nums[right]
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
int mid = 0;
while(left <= right)
{
mid = left + (right - left) / 2;
if(nums[mid] == target)
return true;
if(nums[mid] > nums[right])
{
if(target >= nums[left] && target < nums[mid])
right = mid - 1;
else
left = mid + 1;
}
else if(nums[mid] < nums[right])
{
if(target > nums[mid] && target <= nums[right])
left = mid + 1;
else
right = mid - 1;
}
else
right--;
}
return false;
}
};
四、困难题:寻找两个正序数组的中位数
1. 题目描述:
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
2. 分析:
题目要求时间复杂度O(log (m+n)),想到二分查找。
中位数分成m+n
奇数和偶数讨论,或直接求第(m + n + 1) / 2
个数和第(m + n + 2) / 2
个数的和求平均。
要找到第K个数,可以先排除前K/2
个数,因为没有重复数字,故nums1[i+K/2-1]
(i为开始寻找的下标,因为是第K/2个数,需要-1)和nums2[j+K/2-1]
中较小的那个一定不是第K个数,可以将较小的那个数组开始搜索的下标右移到i+K/2
或i+K/2
从而排除掉K/2
个数,继续迭代搜索第K-K/2
个数。
迭代终止条件:
(1)直到i
超过了nums1
的边界,说明此时第K个数在nums2
中,要找的数就是nums2[j + K - 1]
,或j
超过了nums2
的边界,说明此时第K个数在nums1
中,要找的数就是nums1[i + K - 1]
(2)迭代到K=1,找剩下部分排序后第一个数,即为min(nums1[i], nums2[j])
3. 题解
class Solution {
public:
int findKth(vector<int>& nums1, int start1, vector<int>& nums2, int start2, int K)
{
if(start1 >= nums1.size())
return nums2[start2 + K - 1];
if(start2 >= nums2.size())
return nums1[start1 + K - 1];
if(K == 1)
return min(nums1[start1], nums2[start2]);
int value1 = start1 + K/2 - 1 < nums1.size() ? nums1[start1 + K/2 - 1] : INT_MAX;
int value2 = start2 + K/2 - 1 < nums2.size() ? nums2[start2 + K/2 - 1] : INT_MAX;
if(value1 < value2)
return findKth(nums1, start1 + K/2, nums2, start2, K - K/2);
else
return findKth(nums1, start1, nums2, start2 + K/2, K - K/2);
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
int K1 = (m + n + 1) / 2;
int K2 = (m + n + 2) / 2;
return (findKth(nums1, 0, nums2, 0, K1) + findKth(nums1, 0, nums2, 0, K2)) / 2.0;
}
};