LeetCode —— 34. 在排序数组中查找元素的第一个和最后一个位置

100 阅读2分钟

启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

该题是数组二分查找题型第三题。

数组专栏 - 掘金

题目来源

34. 在排序数组中查找元素的第一个和最后一个位置(LeetCode)

题目描述(中等

给你一个按照非递减顺序排列的整数数组 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]

提示

  • 0<=nums.length<=1050 <= nums.length <= 10^5
  • 109 <=nums[i] <=109-10^9 <= nums[i] <= 10^9
  • nums 是一个非递减数组
  • 109<=target<=109-10^9 <= target <= 10^9

题目解析

该题我们可以理解为寻找 target 在数组中的左右边界,共计三种情况:

  • target 小于数组范围的左边(最小值)或者右边(最大值),例如数组为 [3, 4, 5]target2 或者数组为 [3, 4, 5]target6 ,此时应该返回 [-1, -1]
  • target 在数组范围内,但数组不存在 target ,例如数组为 [2, 3, 6]target5 ,此时应该返回 [-1, -1]
  • target 在数组范围内,且数组存在 target ,例如数组为 [1, 2, 3]target1 ,此时应该返回 [0, 0]

二分查找左右边界

对二分查找接触不多的小伙伴,一开始就用一个二分循环查找左右边界,很容易混淆,可以先用两个二分循环分别查找左边界和右边界,最后对左右边界的值进行判断,返回最终结果。

代码(两个二分循环)

注:此时计算出来的左边界比 target 的左边界小 1 ,同理,右边界比 target 的右边界大 1

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    let left = getLeft(nums, target),right = getRight(nums,target)
    if(right - left < 2){
        return [-1, -1]
    }
    return [left + 1, right - 1]
};
// 寻找左边界
var getLeft = function(nums,target) {
    let left = 0,right = nums.length - 1
    while (right >= left) {
        let mid = Math.floor((left + right) / 2)
        if(nums[mid] >= target) {
            right = mid - 1
        }else {
            left = mid + 1
        }
    }
    return right
}
// 寻找右边界
var getRight = function(nums,target) {
    let left = 0,right = nums.length - 1
    while(right >= left){
        let mid = Math.floor((left + right) / 2)
        if(nums[mid] <= target){
            left = mid + 1
        }else{
            right = mid - 1
        }
    }
    return left
}

如图:

image.png

代码(优化)

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    let left = getTarget(nums, target, true),right = getTarget(nums, target, false)
    if(right - left < 2){
        return [-1, -1]
    }
    console.log(left,right)
    return [left + 1, right - 1]
};

var getTarget = function(nums, target, area) {
    let left = 0,right = nums.length - 1
    while (right >= left) {
        let mid = Math.floor((left + right) / 2)
        if(nums[mid] > target) {
            right = mid - 1
        }else if (nums[mid] < target) {
            left = mid + 1
        }else {
            area ? right = mid - 1 : left = mid + 1
        }
    }
    return area ? right  : left
}

如图:

image.png

  • 时间复杂度:O(log n),其中 n 为数组的长度。二分查找的时间复杂度为 O(log n) ,一共会执行两次,因此总时间复杂度为 O(log n) 。
  • 空间复杂度:O(1)。只需要常数空间存放若干变量。

二分查找

当数组存在 target 时,但不知道 target 的数量,此时只要通过二分查找找到下标 i 使得 nums[i] === target ,在以下标 i ,为起点分别向前后遍历直到出现不等于 target 的位置,停止遍历并返回。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    let left = 0,right = nums.length - 1
    while (right >= left){
        let mid = Math.floor((left + right)/2)
        if (nums[mid] > target){
            right = mid - 1
        }else if (nums[mid] < target){
            left = mid + 1
        }else{
            return [searchArea(nums, mid, target, -1), searchArea(nums, mid, target, 1)]
        }
    }
    return [-1, -1]
};

var searchArea = function(nums, mid, target, num){
    while (nums[mid] === target){
        mid += num
    }
    return mid -= num
}

如图:

image.png

执行用时和内存消耗仅供参考,大家可以多提交几次。如有更好的想法,欢迎大家提出。