「前端刷题」34. 在排序数组中查找元素的第一个和最后一个位置

256 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

题目

给定一个按照升序排列的整数数组 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

解题思路

本文我们一起从O(n)O(logn)找更优解。借纳兰性德《木兰花令·拟古决绝词》说明解法特点

1. 循环

  • 遍历初见target赋值开始位结束位。再见target只更新结束位直到当前数>目标值

代码

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

2.哈希表

  • 对象作哈希表当前数键名。初见赋值。再见只更新结束位。终得所有数的起止位置

代码

var searchRange = function(nums, target) {
    return nums.reduce((h, v, i) => (h[v] ? h[v][1] = i : h[v] = [i, i], h), Object.create(null))[target] || [-1, -1]
};
  • 对比循环解法,未用到升序性质。虽大材小用,却求得所有数,是以不变应万变之解法

3.二分查找

  • 每轮取中间数。大于目标值,继续查找左边一半。小于目标值,继续查找右边一半
  • 找不到,返回-1。找到,以其为中心左右扩散不等于目标值不越界的边界

代码

var searchRange = function(nums, target) {
    var i = binarySearch(nums, target)
    if (i === -1) return [-1, -1]
    else {
        var l = i, r = i + 1
        while(nums[l] === target && l >= 0) l--
        while(nums[r] === target && r < nums.length) r++
    }
    return [l + 1, r - 1]
                                            
};
var binarySearch = (nums, target, l = 0, r = nums.length - 1, m = l + r >>> 1) => 
          l  >  r      ? -1 :
    nums[m] === target ?  m :
    nums[m]  >  target ? binarySearch(nums, target, l, m - 1)
                       : binarySearch(nums, target, m + 1, r)
  • 候选减,速度加快的二分法比较
    • 二分查找:查找复杂度稳定O(logn)。顺序存储,空间100%利用。删除插入难
    • 二叉搜索树:查找复杂度理想O(logn),深度为n时为O(n)
      链式存储,指针占额外空间。删除插入只用移动指针

4.二分查找

  • 区别在于当nums[m] === target时,上解法,直接返回值
  • 本解法,寻左边界时,继续向左查找。寻右边界时,继续向右查找,如图
  • l > r时返回右边界。这样左边界会到初见目标值左侧,右边界会到目标值最后出现位

二分查找 · 直达边界的查找过程

代码

var searchRange = function(nums, target) {
    var l = binarySearch(nums, target, true), r = binarySearch(nums, target, false)
    return l === r ? [-1, -1] : [l + 1, r]                            
};
var binarySearch = (nums, target, isL, l = 0, r = nums.length - 1, m = l + r >>> 1) => 
    l  >  r ? r : (isL ? nums[m] >= target : nums[m] > target)
            ? binarySearch(nums, target, isL, l, m - 1)
            : binarySearch(nums, target, isL, m + 1, r)
  • [2,2,2,2,2,2...] target = 2该例在上解法左右扩散时,会退化成O(n)
  • 比翼鸟只有一目一翼,需两两齐飞2二分查找可稳定时间复杂度
  • 为什么是l > r,为什么要返回r?因为要考虑4种边界情况,如图
    情况一:找8而8只有1个
    情况二:找8而8位于最右
    情况三:找8而8位于最左
    情况四:找8而没有8
  • 以上4种情况即本题二分查找的边界情况,正是边界帮助我们确定了条件和返回指针