【LeetCode】No.34 Search for a Range

【原题】

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;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值