LeetCode之HOT100--034 在排序数组中查找元素的第一个和最后一个位置

181 阅读3分钟

这是我参与11月更文挑战的18天,活动详情查看:2021最后一次更文挑战」。

前言

一直都计划学习数据结构与基本算法,但是平时都看一阵停一阵。现在决心坚持下去,我准备从LeetCode的HOT100开始,每天完成1~2道习题,希望通过这种方式养成持续学习的习惯。因为我是做iOS开发的,主要是用Objective-C语言,最近也在学习Swift,所以本系列的题解都将使用swift语言完成,本文更新的是LeetCode中HOT100的第18题034 在排序数组中查找元素的第一个和最后一个位置。

题目

给定一个按照升序排列的整数数组 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. 0 <= nums.length <= 105
2. -109 <= nums[i] <= 109
3. nums 是一个非递减数组
4. -109 <= target <= 109

分析

直观的思路肯定是从前往后遍历一遍。用两个变量记录第一次和最后一次遇见 target 的下标,但这个方法的时间复杂度为 O(n),没有利用到数组升序排列的条件。
由于数组已经排序,因此整个数组是单调递增的,我们可以利用二分法来加速查找的过程。
二分查找法的基本思路如下:

1. 设置leftright两个指针分别指向数组的两端
2. 取leftright中间下标mid = (left + right) / 2,然后判断nums[mid]与target的大小关系:
    2.1 如果nums[mid] == target,则返回对应的mid即可
    2.2 如果nums[mid] > target,则target只有可能在mid的左边,即将right赋值为mid-1,继续查找
    2.3 如果nums[mid] < target,则target只有可能在mid的右边,即将left赋值为mid+1,继续查找
    2.4 如果一直找到left > right都没有找到,则表明数组中不存在目标值,返回 -1

上述的方法中我们只能找到第一个与目标值相等的下标,所以,为了要找出等于target的最左边和左右边的下标,我们需要进行改造,改造方式就是:

  1. 如果要查找最左边的下标,则在nums[mid] == target时,记录当前下标,并且继续在mid左侧继续进行查找,如果找到则更新目标下标,这样最后的下标才是最左边满足目标值的下标。
  2. 如果要查找最右边的下标,则在nums[mid] == target时,记录当前下标,并且继续在mid右侧继续进行查找,如果找到则更新目标下标,这样最后的下标才是最右边满足目标值的下标。

题解

class KLLC034 {
    func searchRange(_ nums: [Int], _ target: Int) -> [Int] {
        if nums.isEmpty {
            return [-1, -1]
        }
        let targetLeft = binarySearch(nums, target, true)
        let targetRight = binarySearch(nums, target, false)
        return [targetLeft, targetRight]
    }
    
    /// 二分查找目标值
    ///   - nums: 待查找有序数组
    ///   - target: 查找目标值
    ///   - isLeft: 是否是目标值最小的下标
    /// - Returns: 目标值的下标,不存在则返回-1
    func binarySearch(_ nums: [Int], _ target: Int, _ isLeft:Bool) -> Int {
        //初始化目标下标targetIndex为-1,left、right指向数组两端
        var targetIndex = -1
        var left = 0, right = nums.count - 1
        while left <= right {
            //取中间下标
            let mid = (left + right) >> 1
            if nums[mid] == target {
                //满足目标值则更新目标下标
                targetIndex = mid
                if isLeft {
                    //如果是查找最左边的下标,则继续在左侧查找
                    right = mid - 1
                } else {
                    //如果是查找最右边的下标,则继续在右侧查找
                    left = mid + 1
                }
            } else if nums[mid] > target {
                right = mid - 1
            } else {
                left = mid + 1
            }
        }
        return targetIndex
    }
}