前端刷题路-Day60:在排序数组中查找元素的第一个和最后一个位置(题号34)

859 阅读2分钟

这是我参与更文挑战的第24天,活动详情查看: 更文挑战

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

题目

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

链接

leetcode-cn.com/problems/fi…

解释

这题啊,这题是经典二分。

简单解法就真的很简单了,因为是非降序数组,注意,这里用的是非降序数组,因为数组中有重复元素,感觉非降序数组更合适一点。

既然是非降序数组,那么只要按照顺序扫描一遍,就能得出最后的结论了,记录下target出现的位置就完事了,如果扫完了位置还没出来,直接返回[-1, -1]

这东西一行代码就能写完👇:

var searchRange = function(nums, target) {
  return nums.reduce((position, value, index) => {
    return value === target ? [position[0] === -1 ? index : position[0], index] : position
  }, [-1, -1])
};

利用一个reduce就好了,如果想性能稍微好一点,还可以进行剪枝操作,当当前元素的值大于target的时候就断掉,因为是非降序数组👇:

var searchRange = function(nums, target) {
  return nums.reduce((position, value, index) => {
    return value === target ? [position[0] === -1 ? index : position[0], index] : (value > target && (nums.length = 0), position)
  }, [-1, -1])
};

试了几次,提升还是不少的,但感觉是受测试用例的影响,比方说一个数组的长度是1W,复合条件的元素在前10个,那此时剪枝的就剪得很好了,可以节省大量计算。

这题的重点在于实现时间复杂度为*O(log n)的算法,看到O(log n)*的时候就应该知道是二分了,很明显的时间复杂度。

二分这里主要分为两个部分,第一部分是常规二分,找到元素或者直接返回。第二部分是因为这里的数组是非降序数组,会有重复的数字出现,所以需要向两边进行扩张,找到所有的相同值,最后得到范围区间。

自己的答案(遍历)

var searchRange = function(nums, target) {
  return nums.reduce((position, value, index) => {
    return value === target ? [position[0] === -1 ? index : position[0], index] : (value > target && (nums.length = 0), position)
  }, [-1, -1])
};

这个方法在之前说过了,此处不多赘述。

自己的答案(二分+扩散)

var searchRange = function(nums, target) {
  var left = 0
      right = nums.length - 1
  while (left < right) {
    var mid = ~~((left + right) / 2)
    if (nums[mid] === target) {
      left = mid
      right = mid
    } else if (nums[mid] < target) {
      left = mid + 1
    } else {
      right = mid - 1
    }
  }
  if (nums[left] !== target) return [-1, -1]
  while (nums[left] === target || nums[right] === target) {
    if (nums[left] === target) left--
    if (nums[right] === target) right++
  }
  return [left + 1, right - 1]
};

第一个while是经典二分,第二个while是中心扩散,找到所有符合条件的 值,得到最后的区间。

也就是比较简单的。

更好的方法(二分+定位区间)

这是官方的给的答案,主要还是使用了二分的思路。

先看看代码👇:

const binarySearch = (nums, target, lower) => {
  let left = 0, right = nums.length - 1, ans = nums.length;
  while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      if (nums[mid] > target || (lower && nums[mid] >= target)) {
          right = mid - 1;
          ans = mid;
      } else {
          left = mid + 1;
      }
  }
  return ans;
}

var searchRange = function(nums, target) {
  let ans = [-1, -1];
  const leftIdx = binarySearch(nums, target, true);
  const rightIdx = binarySearch(nums, target, false) - 1;
  if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] === target && nums[rightIdx] === target) {
      ans = [leftIdx, rightIdx];
  } 
  return ans;
};

这里定义了binarySearch方法来进行二分查找,但这并不是典型的二分,而是有一些小小的变换,注意第三个参数——lower,这个参数的是个布尔值,如果是true证明当前查找的结果是区间左边的数,false则是找区间右边的数。

最后调用两次这个方法,找到左右的区间值,如果符合条件就返回左右区间值,否则返回[-1. -1],整体思路确实不复杂,只是二分两次感觉有点耗费性能。

不过这也不一定,如果target重复了很多次,这种方法的性能会好些,如果重复次数不多感觉还是二分+扩散会好些。



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)

有兴趣的也可以看看我的个人主页👇

Here is RZ