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

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) 时,跳出循环时,leftright 可能是相同的,区间 [left, right] 的长度为1,而且如果有解的话 leftright 一定相同,此时的解就是 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 相等时)。
  • 对于区间划分,容易出现死循环,都是在剩两个元素时出现,所以不确定是否会出现死循环时,将两个元素的情况带入试一试就好,看是否需要将下取整改为上取整。

猜你喜欢

转载自blog.csdn.net/qq_41512783/article/details/109588198