LeetCode 在排序数组中查找元素的第一个和最后一个位置
@author:Jingdai
@date:2020.11.09
题目描述(34题)
给定一个按照升序排列的整数数组
nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回
[-1, -1]
。
示例输入
nums = [5,7,7,8,8,10], target = 8
示例输出
[3,4]
思路
这个题是典型的二分查找问题,但是这个和最基本的二分查找问题有一点点区别,就是查找的数可能是重复的,而一般的二分查找是查找到直接返回,这里不能直接返回,因为你并不知道你找到的值是第一个、最后一个还是中间的某个。
对于没有重复数的基本二分查找,一般就是使用
while(left <= right)
来进行循环判断,找到就直接返回,找不到就返回-1
,而这里需要稍微改进一点点。这里不使用while(left <= right)
来循环,而使用while(left < right)
来循环判断。使用
while(left <= right)
循环跳出时,left > right
,区间[left, right]
的长度为负 ,跳出循环时一定是没有解的。而使用while(left < right)
时,跳出循环时,left
和right
可能是相同的,区间[left, right]
的长度为1,而且如果有解的话left
和right
一定相同,此时的解就是left
。接下来回归到这个题目,将题目分解成两个部分,查找元素的第一个位置和最后一个位置。先看查找元素的第一个位置怎么求,就是如何减半查找区间:
target == nums[middle]
:此时不能直接返回,因为这并不一定是第一个元素,那应该如何缩小区间呢?要找第一个元素,那第一个元素要么就是middle
,要么就在middle
前面,所以middle
后面的区间就可以忽略了。此时让right = middle
。target > nums[middle]
:这里就和基本的二分查找一样了,让left = middle + 1
。target < nums[middle]
:让right = middle - 1
。看整个查找第一个元素位置的代码片段:
public int findFisrtIndex(int[] nums, int target) { if (nums == null || nums.length == 0) return -1; if (nums.length == 1) return nums[0] == target ? 0 : -1; int left = 0; int right = nums.length - 1; int middle; while (left < right) { middle = left + (right - left) / 2; if (target == nums[middle]) { right = middle; } else if (target > nums[middle]) { left = middle + 1; } else { right = middle - 1; } } if (nums[left] == target) return left; return -1; }
接下来是查找最后一个元素的位置,和第一个思路基本一样,就不解释了,直接看后面的代码部分就好。
这里再说一点点细节问题,在写二分查找的时候区间分的不对的话很容易造成死循环,就是在剩两个元素的时候区间不能缩小,就会造成死循环。在写查找最后一个元素的代码时,
middle
的取法应该向上取整,不能像找第一个元素位置那样向下取整,否则会造成死循环,自己在两个元素时试一下就懂了,这里注意一下,很容易错。
代码
public int[] searchRange(int[] nums, int target) { int firstIndex = findFisrtIndex(nums, target); if (firstIndex == -1) return new int[] { -1, -1}; int lastIndex = findLastIndex(nums, target); return new int[] { firstIndex, lastIndex}; } public int findFisrtIndex(int[] nums, int target) { if (nums == null || nums.length == 0) return -1; if (nums.length == 1) return nums[0] == target ? 0 : -1; int left = 0; int right = nums.length - 1; int middle; while (left < right) { middle = left + (right - left) / 2; if (target == nums[middle]) { right = middle; } else if (target > nums[middle]) { left = middle + 1; } else { right = middle - 1; } } if (nums[left] == target) return left; return -1; } public int findLastIndex(int[] nums, int target) { if (nums == null || nums.length == 0) return -1; if (nums.length == 1) return nums[0] == target ? 0 : -1; int left = 0; int right = nums.length - 1; int middle; while (left < right) { middle = left + (right - left + 1) / 2; if (target == nums[middle]) { left = middle; } else if (target > nums[middle]) { left = middle + 1; } else { right = middle - 1; } } if (nums[left] == target) { return left; } return -1; }
小结
最后,整理一下写这个二分查找的一点点总结。
- 对于最基本的二分查找,用
while (left <= right)
就行,而稍微复杂点的问题,用while (left < right)
更容易解决。- 对于二分查找,就是缩小查找区间,要在每一步明确如何缩小区间(比如这个题
nums[middle]
和target
相等时)。- 对于区间划分,容易出现死循环,都是在剩两个元素时出现,所以不确定是否会出现死循环时,将两个元素的情况带入试一试就好,看是否需要将下取整改为上取整。