【原题】
Given an array of integers sorted in ascending order, find the starting and ending position of a given target value. Your algorithm's runtime complexity must be in the order of O(log n). If the target is not found in the array, return [-1, -1]. For example, Given [5, 7, 7, 8, 8, 10] and target value 8, return [3, 4].
【翻译】
数字在排序数组中出现的次数。
给定一个排好序的数组和一个数字,输出该数字出现的起始位置和结束位置,以及出现的次数(即:结束位置-起始位置+1)。
【解题思路】
排好序的数组→二分查找。
改进的二分查找:
二分查找算法总是先拿数组中间的数字和k作比较。如果中间的数字比k大,那么k只有可能出现在数组的前半段,下一轮我们只在数组的前半段查找就可以了。如果中间的数字比k小,那么k只有可能出现在数组的后半段,下一轮我们只在数组的后半段查找就可以了。如果中间的数字和k相等呢?我们先判断这个数字是不是第一个k。如果位于中间数字的前面一个数字不是k,此时中间的数字刚好就是第一个k。如果中间数字的前面一个数字也是k,也就是说第一个k肯定在数组的前半段,下一轮我们仍然需要在数组的前半段查找。
二分法在数组中查找一个合乎要求的数字时间复杂度是O(logn),因此总的时间复杂度也只有O(logn)。
==============================================================================================
伪代码1:找到开头的k
1、一旦发现start>end,就说明数组中根本没有K,所以返回-1表示出错;
2、计算中点位置,将中点的值和k进行比较:
2.1. 若k小于中点值,则在前半段找
2.2. 若k大于中点值,则在后半段找
2.3. 若k等于中点值:
a)若中点已经是数组第一个元素,则直接返回中点位置;
b)若中点值的前一个元素和中点值不同,则直接返回中点位置;
c)若中点值前一个元素和中点值相同,则继续在前半段找;
3、计算出新的start、end后继续递归。
伪代码2:主函数
1、健壮性判断:若数组为空、数组的第一个元素大于k、最后一个元素小于k,则根本不存在k,直接返回0.
2、分别计算k开头位置、结尾位置
3、只要其中一个为-1则返回-1;否则返回(结尾-开头+1)
===============================================================================================
(1)递归:每次的前半段、后半段有重复的操作,所以用递归,但是每次操作的数组范围不一样,所以应该再加两个参数传入start和end。
class Solution {
public:
//找起始位置
int GetFirstTarget(vector<int>& nums, int target, int start, int end){
if(start > end){
return -1;
}
int mid = (start+end)/2;
//当nums[mid]=target,又分三种情况
if(nums[mid] == target){
if((mid>0 && nums[mid-1] != target) || mid == 0){//当mid不是第一个且左邻居不同(防止越界) 或者 mid就是数组第一个元素
return mid;
}
else{//否则在前半段继续找第一次出现的位置
end = mid-1;
}
}
//当nums[mid]>target,在前半段找
else if(nums[mid] > target){
end = mid-1;
}
//当nums[mid]<target,在后半段找
else{
start = mid+1;
}
return GetFirstTarget(nums, target, start, end);//缩小要寻找的范围进行递归
}
//找结束位置
int GetLastTarget(vector<int>& nums, int target, int start, int end){
if(start > end){
return -1;
}
int mid = (start+end)/2;
//当nums[mid]=target,又分三种情况
if(nums[mid] == target){
if((mid<(nums.size()-1) && nums[mid+1] != target) || mid == nums.size()-1){//当mid不是最后一个且右邻居不同(防止越界) 或者 mid就是数组最后一个元素
return mid;
}
else{//否则在后半段继续找最后一次出现的位置
start = mid+1;
}
}
//当nums[mid]>target,在前半段找
else if(nums[mid] > target){
end = mid-1;
}
//当nums[mid]<target,在后半段找
else{
start = mid+1;
}
return GetLastTarget(nums, target, start, end);//缩小要寻找的范围进行递归
}
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> result;
if(nums.size() <= 0){
result.push_back(-1);
result.push_back(-1);
}
else{
int firstNum = GetFirstTarget(nums, target, 0, nums.size()-1);
int lastNum = GetLastTarget(nums, target, 0, nums.size()-1);
result.push_back(firstNum);
result.push_back(lastNum);
}
return result;
}
};
(2)非递归:超时!
//找起始位置
int GetFirstTarget(vector<int>& nums, int target, int start, int end){
if(start > end){
return -1;
}
int mid = (start+end)/2;
while(start <= end){
//当nums[mid]=target,又分三种情况
if(nums[mid] == target){
if((mid>0 && nums[mid-1] != target) || mid == 0){//当mid不是第一个且左邻居不同(防止越界) 或者 mid就是数组第一个元素
return mid;
}
else{//否则在前半段继续找第一次出现的位置
end = mid-1;
}
}
//当nums[mid]>target,在前半段找
else if(nums[mid] > target){
end = mid-1;
}
//当nums[mid]<target,在后半段找
else{
start = mid+1;
}
}//while
}
【O(n)的解法】
1、遍历一次,直接找,设置一个标记用来保证不移动start只移动end。可是时间为O(n),不符合。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int start=-1, end = -1;
for(int i=0; i<nums.size(); i++){
if(nums[i]==target && start == -1){
start = i;
end = i;
}
else if(start != -1 && nums[i] == target){
end += 1;
}
}
vector<int> v;
v.push_back(start);
v.push_back(end);
return v;
}
};
2、若直接运用二分查找:给出的例子中,可以先用二分查找算法找到一个3。由于3可能出现多次,因此我们找到的3的左右两边可能都有3,于是在找到的3的左右两边顺序扫描,分别找出第一个3和最后一个3。因为要查找的数字在长度为n的数组中有可能出现O(n)次,所以顺序扫描的时间复杂度是O(n)。因此这种算法的效率和直接从头到尾顺序扫描整个数组统计3出现的次数的方法是一样的。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> result;
int low = 0, high = nums.size()-1;
int mid;
while(low <= high){
mid = (low+high)/2;
if(nums[mid] == target) break;
else if(nums[mid] > target){ high = mid-1; }
else { low = mid+1; }
}//找不到跳出循环此时low>high,返回{-1,-1};找到了也跳出循环此时要判断nums[mid]周围情况
//往mid两边顺序扫描,直到找到起始位置和解释位置,最坏为O(n)
if(low <= high){
low = mid-1;//把low放在mid的左邻居,看nums[mid-1]等不等于target
while(low >=0 && nums[low] == target){//条件1:如果mid=0是数组的第一个元素;条件2:mid左边也是target
low--;
}
high = mid+1;
while(high < nums.size() && nums[high] == target){//条件1:如果mid=nums.size()-1是数组的最后一个元素;条件2:mid右边也是target
high++;
}
result.push_back(low + 1);
result.push_back(high - 1);
}
else{
result.push_back(-1);
result.push_back(-1);
}
return result;
}
};