目录
力扣题址:
今天刷统计目标成绩的出现次数(数字在排序数组中出现次数),大家有兴趣可以点上看看题目要求,试着做一下。
我们直接看题解吧:
看到题目可能首先想到直接暴力循环,遍历数组,
当然,也有人可能会想到用哈希表
但以上方法都没有利用题目的关键条件,那就是排序的数组,在做排序数组问题时首先想到的应该是二分查找
因此方法适合用二分法(单边或双边)
审题目+事例+提示:
数组是非严格递增(允许重复元素)排序数组
思路(二分法,双边):
关键:利用target首次出现与最后出现的下标差
1、初始化,左边界i=0,右边界j=length-1
2、循环二分:当闭区间[i,j]无元素跳出
a、计算中点m=i+(j-i)/2(向下取整)
b、Scores[m]>target,则i=m+1
c、cores[m]<target,则i=m-1
d、Scores[m]==target
·右边界right在[m+1,j],查找右边界则i=m+1(i指向右边界)
若查找右边界后,用scores[m]==target判断数组是否含target,无返回0,有则遍历左边界。
此时左边界一定在闭区间[0,j]中
· 左边界right在[i,m-1],查找左边界则j=m-1(指向左边界)
3、返回值:应用二分查找right和left,最终返回right-left-1
这里分享一个取中点的小知识:
假设 low与high分别在数组左右两边,则中点=low+(high-low)
(这种写法看起来有点脱裤子放屁的意思,但)它主要好处是防止(high+low)过大时造成溢出
代码:
class Solution {
public int countTarget(int[] scores, int target) {
// 搜索右边界 right
int i = 0, j = scores.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(scores[m] <= target) i = m + 1;
else j = m - 1;
}
int right = i;
// 若数组中无 target ,则提前返回
if(j >= 0 && scores[j] != target) return 0;
// 搜索左边界 right
i = 0; j = scores.length - 1;
while(i <= j) {
int m = (i + j) / 2;
if(scores[m] < target) i = m + 1;
else j = m - 1;
}
int left = j;
return right - left - 1;
}
}
优化:
由于上面出现了两次二分查找,我们可以提高代码的复用性,将查找右边界right封装在一个函数中,而target 是整数,所以利用target 与target-1的下标之差
代码:
class Solution {
public int countTarget(int[] scores, int target) {
return helper(scores, target) - helper(scores, target - 1);
}
int helper(int[] scores, int tar) {
int i = 0, j = scores.length - 1;
while(i <= j) {
int m = i+(j-i) / 2;
if(scores[m] <= tar) i = m + 1;
else j = m - 1;
}
return i;
}
}
评论区大佬的注释:
1、 while(i <= j),这里不可用是i < j ,设想int[] nums = {1,2,3},target=3,第一次i = 0,j = 2;第二次 i = 1,j = 2;第三次 i = 2,j = 2。如果是while(i < j),而第三次是i = j ,不满足 i < j 的条件,退出while循环,会导致无法判断 i = j 的情况,不知道nums[i]是否等于target,就直接返回。
2、第一个if语句: if(nums[m] <= tar) ,这里因为合并了nums[m] < tar和nums[m] = tar两种情况,所以一开始看的十分头晕,建议为了可读性还是分开写。主要说 nums[m] = tar,因为helper方法是求右边界的!所以只要nums[m] = tar,则 i = m + 1,直到nums[m] 不等于 tar,此时就找到了tar的右边界。所以只要返回(tar的右边界减去tar-1的右边界),即可得到tar出现的次数。(这也说明为什么nums[m] = tar时,还需要 i = m +1,因为这是为了【找右边界】,与二分查找略有不同,二分查找是nums[m] = tar,就返回m了)
3、假如tar-1不存在呢?比如{1,2,3,5,7,7,7,8},tar=7,而tar-1=6并不在数组nums里边。回看第二个问题if(nums[m] <= tar)合并了两个条件, 上边说了nums[m] = tar,这里说说nums[m] < tar。其实tar-1存在与否并不重要,按照开头的数组,设tar=6。第一次i = 0,j = 7,m=3,nums[m]=5<tar;第二次 i = 4,j = 7,m = 5,nums[m] = 7 > tar;第三次i = 4,j = 4,m = 4,nums[m] = 7 >tar;第四次 i = 4,j = 3,i>j 退出while,renturn 4,tar的右边界为下标4。为什么tar不存在不影响?我展示一下拙见,如有合适的解释,欢迎大佬补充。因为helper方法是为了求右边界,tar不存在时,不考虑nums[m] == tar的判断;nums[m] > tar,j = m-1;nums[m] < tar,i = m+1,最终返回的是tar的右边界(return i 保证是返回右边界,不可用return j)。所以tar的右边界一定存在,即使tar不存在
直接一次二分查找:
(不过在某些极端情况会出现时间复杂度退化)
class Solution {
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low<=high){
//防止整数过大而溢出
int mid=low+(high-low)/2;
if(nums[mid]<target){
low=mid+1;
}else if(nums[mid]>target){
high=mid-1;
}else{
if(nums[high]!=target)
high--;
else if(nums[low]!=target)
low++;
else
break;
}
}
//返回的要+1,因为是从数组下标开始
return high-low+1;
}
}