【算法-LeetCode】34. 在排序数组中查找元素的第一个和最后一个位置(indexOf;二分查找)

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

发布:2021年10月2日19:05:59

问题描述及示例

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的题解

我的题解1(indexOf和lastIndexOf)

既然是用JavaScript来做,那么自然最容易想到的就是这种方法了,一试,居然还真可以~🤣,而且性能表现也不是想象中那么拉胯,具体就不多解释了, 怕招打……

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
  return [nums.indexOf(target), nums.lastIndexOf(target)];
};

提交记录
88 / 88 个通过测试用例
状态:通过
执行用时:76 ms, 在所有 JavaScript 提交中击败了46.00%的用户
内存消耗:39 MB, 在所有 JavaScript 提交中击败了84.75%的用户
时间:2021/10/02 19:12

我的题解2(暴力解法)

这种解法也比较好理解。第一个 while 循环用于寻找 target 在数组中首次出现的位置,第二个 while 循环用于寻找 target 在数组中最后一次出现的位置。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
  let index = 0;
  let result = [-1, -1];
  // 第一个 `while` 循环用于寻找 `target` 在数组中首次出现的位置
  while(index < nums.length) {
    if(nums[index] === target) {
      result[0] = index;
      // 一旦找到target,则立马用break结束这个while循环
      break;
    }
    index++;
  }
  // 不要忘记重置index的值
  index = nums.length - 1;
  // 第二个 `while` 循环用于寻找 `target` 在数组中最后一次出现的位置
  // 注意这里的结束条件不是简单的写成了index >= 0,因为result[1]一定不比result[0]小
  while(index >= result[0]) {
    if(nums[index] === target) {
      result[1] = index;
      // 一旦找到target,则立马用break结束这个while循环
      break;
    }
    index--;
  }
  return result;
};


提交记录
88 / 88 个通过测试用例
状态:通过
执行用时:72 ms, 在所有 JavaScript 提交中击败了64.86%的用户
内存消耗:39.1 MB, 在所有 JavaScript 提交中击败了69.58%的用户
时间:2021/10/02 19:39

我的题解3(二分查找)

上面的两种解法中都没有利用【 nums是按照升序排列的】这一题目特点。而根据这一特点,我们可以利用二分查找的思路来搜索目标。

之前做过一个二分查找的题目:

参考:【算法-LeetCode】704. 二分查找_赖念安的博客-CSDN博客

本题的二分查找核心思路和上面的纯二分查找一样。唯一不同的是本题在找到 target 之后,还要分别进一步用二分思想来查找 target 区间的左边界和右边界。

在这里插入图片描述

找到 target 后,继续分别寻找左边界和右边界

注意,leftright 以及 mid 指针都是可重复利用的。

本题的思路大体可以分为三部分:

  1. 利用 frontback 以及 mid 指针遵循常规的二分查找思路搜寻 target,并用 mid 指针指向搜寻到的 target。(对应最外层的 while 循环)
  2. 找到 target 后,保存 frontback 指针的状态,并利用 leftright 以及 mid 指针来在【1】中所找到的 target 左半部分继续遵循二分查找思路搜寻左边界。(对应内层的第一个 while 循环)
  3. 找到左边界后,再利用之前保存的 frontback 指针的状态继续按照【2】的套路搜寻右边界。(对应内层的第二个 while 循环)

其中三个部分中的二分查找结束条件都有细微的不同。

二分查找的思路可以查看上面的博客。本题的相关详解请看下方注释:

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
  // front和back和纯二分查找中的作用一样
  let front = 0;
  let back = nums.length - 1;
  // left和right可以看做是两个临时的辅助指针,用于确定左边界和右边界,
  // 其初始值无所谓是什么,因为之后在被用之前会被覆盖,他们都可以被重复利用
  let left = 0;
  let right = 0;
  // mid用于指示一段区间的中间位置,它也可以重复利用
  let mid = 0;
  // result用于存储最后结果,初始化为[-1,-1]
  let result = [-1, -1];
  // 最外层while循环用于初步定位target,但是无法准确确定左边界和右边界,
  // 这里是【步骤1】的部分
  while(front <= back) {
    mid = Math.floor((back - front) / 2 + front);
    // 下面的if判断中的逻辑就是找到target后,继续搜寻左边界和右边界的逻辑(即步骤2和3)
    if(nums[mid] === target) {
      // 先搜寻左边界,这里是【步骤2】的部分
      left = front;
      right = mid;
      while(!(nums[mid] === target && nums[mid - 1] !== target)) {
        // 【tag1】注意这里要用Math.floor,而不能用Math.ceil,详情请看下方【补充1】
        mid = Math.floor((right - left) / 2 + left);
        left = nums[mid] === target ? left : mid;
        right = nums[mid] === target ? mid : right;
      }
      // 上面的while循环结束后,说明已经找到了左边界,且就是mid所指之处
      result[0] = mid;
      // 左边界找到后,恢复mid指针的指向,并重新初始化left、right指针的指向开始搜寻右边界
      // 这里是【步骤3】的部分
      mid = Math.floor((back - front) / 2 + front);
      left = mid;
      right = back;
      while(!(nums[mid] === target && nums[mid + 1] !== target)) {
        // 【tag2】注意这里要用Math.ceil,而不能用Math.floor,详情请看下方【补充1】
        mid = Math.ceil((right - left) / 2 + left);
        left = nums[mid] === target ? mid : left;
        right = nums[mid] === target ? right : mid;
      }
      // 上面的while循环结束后,说明已经找到了右边界,且就是mid所指之处
      result[1] = mid;
      // 找到左右边界后,最外层的while循环也应该停止了,所以此处应直接break
      break;
    }
    // 这里是【步骤1】的部分
    front = nums[mid] < target ? mid + 1 : front;
    back = nums[mid] < target ? back : mid - 1;
  }
  return result;
};


提交记录
88 / 88 个通过测试用例
状态:通过
执行用时:68 ms, 在所有 JavaScript 提交中击败了81.37%的用户
内存消耗:39.6 MB, 在所有 JavaScript 提交中击败了22.12%的用户
时间:2021/10/02 19:39

可以看到,这种做法的时间表现还是好了一点的,但是空间表现就不如前面的两种解法了,毕竟用到了好几个辅助变量。而且这种写法在理解上的复杂程度也更高了。

补充1
【tag1】处要使用 Math.floor 是为了应对 mid 指针和 right 指针重合的情况,如果使用 Math.ceil 的话就会在步骤【2】陷入死循环。(比如:nums = [8,8,9]

同理, 【tag2】处要使用 Math.ceil 是为了应对 mid 指针和 left 指针重合的情况,如果使用 Math.floor 的话就会在步骤【3】陷入死循环。(比如:nums = [7,8,8]

具体过程可以自行在浏览器的开发者工具中观察。

官方题解

更新:2021年7月29日18:43:21

因为我考虑到著作权归属问题,所以【官方题解】部分我不再粘贴具体的代码了,可到下方的链接中查看。

更新:2021年10月2日20:42:41

参考:在排序数组中查找元素的第一个和最后一个位置 - 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

【更新结束】

有关参考

更新:2021年10月2日20:41:24
参考:【算法-LeetCode】704. 二分查找_赖念安的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值