算法练习题力扣练习题——数组: 在有序数组中查找元素存在的范围

Posted ricardoislearning

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法练习题力扣练习题——数组: 在有序数组中查找元素存在的范围相关的知识,希望对你有一定的参考价值。

原题说明:给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

你的算法时间复杂度必须是 O(log n) 级别。

如果数组中不存在目标值,返回 [-1, -1]。

原题链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array


 

题目分析

这道题目当然也是用二分查找来解题。经过上道题的教训,这次我详细考察了各个实例的可能(特别是空集以及数组元素比较少的时候可能出现的各种情况)。

那么整体上,这道题我当时觉得要写两个查找算法的函数,

PART1:第一个用于“随便”确定一个索引位置,其数值只要是目标值即可。

PART2:以索引值分别作为左半区间和右半区间的右边界和左边界,两边各自做一个二分查找。起初觉着第二部分用一个函数就行,后来在代入左半区间查找的时候发现,应该拆成两个函数会更方便一些。

 

下面是具体分析——

PART1:找到索引值

先po源代码

 1 private int[] locate(int[] nums, int left, int right, int target) {
 2     int mid = (left+right+1)>>1;
 3     int val = -1;
 4     while(left<=right) {
 5         if(nums[mid]==target) {
 6             val = mid;
 7             break;
 8         }
 9         else if(nums[mid]<target) {
10             left=mid+1;
11             mid=(left+right+1)>>1;
12         }
13         else if(nums[mid]>target) {
14             right=mid-1;
15             mid=(left+right+1)>>1;
16         }            
17     }
18     //val=(nums[left]==target)?left:val;
19     int[] borders = {left,right,val};
20     return borders;
21 }

 有上一篇的博客分析铺垫,二分查找的细节这里就不多提。需要说明的是我这里最后输出的变量形式,为什么要一起输出二分查找最后的左边界和右边界呢?这里给出一个实例${0,1,1,1,2,3,4,5,5,5}$,那么当$target$是$1$的时候,最后这部分查找完,左边界索引应该是$0$,右边界索引是$4$,查找到的索引值是$2$,那么之后的查找范围就迅速缩小了。

 

PART2:

分析左边区间的查找,代码如下

 1 private int left_binary_search(int[] nums, int left, int right, int target) {
 2     int mid = (left+right+1)>>1;
 3     int val = right;
 4     
 5     while(left<=right) {
 6         if(nums[mid]<target) {
 7             left=mid+1;
 8             mid=(left+right+1)>>1;
 9         }
10         else {
11             if(mid>0&&nums[mid-1]==target) {
12                 right=mid-1;
13                 mid=(left+right+1)>>1;
14             }
15             else if(mid>0&&nums[mid-1]!=target) {
16                 val = mid;
17                 break;
18             }
19             else if(mid==0)
20                 return 0;
21         }
22     }
23     return val;
24 }

 首先,返回值$val$在这里是作为最后输出的左边界。那么我设定输出值的初值就是上一步查找得到的索引值,这样就包括了最后数组中只有一个目标值得情况,同时也是左边区间的右边界

然后需要分成两种情况讨论(如图1):

 技术图片

情况1,$nums[mid]<target$,那么这个时候目标值应该在搜索区间的右半边(默认递增数列)。此时应该移动左边界。

情况2,$nums[mid]==target$,即算法找到了左边区间内目标值的位置。但是这里要注意,标准的二分查找此时应该结束,然而题目要求需要找到所有重复的目标值的索引,所以需要让$mid$指针继续移动。怎么做呢?

可以看图2,由于是递增数列,那么在本情况下,$mid$到$right$之间应该都是目标值,若还要继续,则应该在$mid-1$的索引位置出现目标值,$right$取值$mid-1$;反之,则结束(因为重复数字必然构成连续)或继续查找直到满足循环的终止条件。

技术图片

这里比较担心的情况是数组越界。所以对于左区间,在分支语句的判断条件上加入了$mid>0$,确保$mid-1$一定有值可以取到

再考虑边界情况,即第一个元素若是目标值怎么办?这里推导一下上一步,如果$nums[left]==target, left=0$,那么$nums[mid]$必然等于$target$,且有$mid-1==left$,此时是满足第11行的判断条件的,所以$right=mid-1$(附带一句,此前$right==1$或$right==2$)。这样一来,得到新的$mid$值为$0$。所以,只要设定$mid$等于$0$时,返回$0$。见图4

 技术图片

这里还想讨论一下,是否需要考虑情况3,即$nums[mid]>target$?我的想法是,不需要。因为左半区间作为一个递增数组,右边界就已经是目标值了,那么左半区间的所有元素都只能小于和等于目标值

 

右半区间的查找类似。代码如下

 1 private int right_binary_search(int[] nums, int left, int right, int target) {
 2     int mid = (left+right+1)>>1;
 3     int val = left;
 4     
 5     while(left<=right) {
 6         if(nums[mid]>target) {
 7             right=mid-1;
 8             mid=(left+right+1)>>1;
 9         }
10         else {
11             if(mid+1<nums.length&&nums[mid+1]==target) {
12                 left=mid+1;
13                 mid=(left+right+1)>>1;
14             }
15             else if (mid+1<nums.length&&nums[mid+1]!=target) {
16                 val = mid;
17                 break;
18             }
19             else if (mid+1==nums.length) {20                 return nums.length-1;
21             }        
22         }
23     }
24     return val;
25 }

 同样的,左边界就是PART1中的索引值,同时作为返回值$val$的初始值,该变量是最后输出的右边界

考虑的情况也是类似的,分成两种情况(如图4):

技术图片

情况1,$nums[mid]>target$时,与之前的情况是相反的,所以此时应该移动右边界。

情况2,$nums[mid]==target$时,考虑情况也是类似的,只是数组越界的具体考虑不一样。我总结了一下,这里可以这么考虑,正常情况下,$mid$是可以取值$length-1$的,所以是mid $leq$ nums.length $-1$。那么由于考虑到$mid+1$的越界情况,所以$mid<nums.length - 1$

在边界条件上的考虑,也是类似的,故在此不再赘述。

 

最后是主函数的代码

 1 public int[] searchRange(int[] nums, int target) {
 2     int[] range = {-1,-1};
 3     if(nums == null || nums.length < 1)
 4         return range;
 5     
 6     
 7     int left = 0, right = nums.length-1;
 8     int mid;
 9     int leftborder, rightborder;
10     
11     int[] borders = locate(nums, left, right, target);
12     left=borders[0];right=borders[1];mid=borders[2];
13     
14     if (mid != -1) {
15         leftborder = left_binary_search(nums, left, mid, target);
16         rightborder = right_binary_search(nums, mid, right, target);
17         range[0]=leftborder;
18         range[1]=rightborder;
19     }
20 
21     return range;
22 }

 这里看到第14行,如果在第一次二分查找定位索引时,就没有找到,那么自然返回$-1$ ,于是就没有再进行左右区间分别查找的必要了(我突然在想,这部分其实可以是同时执行?);反之,在左右区间得到的左右边界(也可呢就是第一部分查找得到的索引值)就是输出的范围。


  

总结

  • 对所有的情况要穷尽
  • 考虑数组越界的可能要好好推导
  • 对于边界条件要举例分析
  • 对二分查找的认识更深入(指针移动,对半快速搜索)

以上是关于算法练习题力扣练习题——数组: 在有序数组中查找元素存在的范围的主要内容,如果未能解决你的问题,请参考以下文章

顺序表的实现以及力扣练习题

算法练习题力扣练习题——数组:三数之和

力扣 练习2(十题)

力扣 练习2(十题)

力扣 练习2(十题)

AK leetcode 流浪计划 - 二分查找