这是我参与更文挑战的第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
链接
解释
这题啊,这题是经典二分。
简单解法就真的很简单了,因为是非降序数组,注意,这里用的是非降序数组,因为数组中有重复元素,感觉非降序数组更合适一点。
既然是非降序数组,那么只要按照顺序扫描一遍,就能得出最后的结论了,记录下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:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇