当我们在操作一组数据时难免会遇到重复元素的出现,那么在二分查找法中我们该如何处理此种问题呢?
可以假设当我们要查找的元素在数组中恰好处于中间位置,但是这个数组中在中间位置的左右两边都有一个或多个和我们要查元素重复的元素,基于普通的二分查找法返回值一定是第一次找到的元素的下标(也就是中间元素位置的下标),但是如果我们想要的是最左侧或者最右侧的元素下标,此时我们该怎么处理呢?emmm.......是不是还需要继续移动m指针(记录中间位置的指针)让其在左边块中或右边块中继续查找呢?但是要是左边块中或右边块中不再有重复元素了,我们就得将之前找到的元素下标返回,因此我们还可以定义一个临时变量来记录当前m指针所找到的符合条件的元素的下标,再循环结束(i>j也就是数组中的元素已全部遍历完之后)后将其返回;
因此便会产生rightmost(最右边查找)和leftmost(最左边查找)这两种情况;
1.leftmost
也就是需要返回数组中最左侧重复元素的下标,那么我们可以先设一临时变量初始值赋为-1;用来记录当前所找到的目标元素的下标,再循环中当我们记录了当前目标元素的下标之后,我们还的继续缩小范围,由于是要找最左侧复合元素的下标,因此我们需将j指针移到m指针位置之前,依次循环直到不满足循环条件是退出循环返回最左侧记录的符合目标值的元素下标,具体实现代码如下:
public static int LeftMost(int []a,int X) //传入数组和要查找的目标元素值
{
int i=0,j=a.length-1; //让i,j分别指向数组两端位置
int candidate=-1; //设置临时变量
whlie(i<=j)
{
int m=(i+j)>>>1; //
if(X<a[m]) //目标元素在左半部分
{
j=m-1;
}
else if(a[m]<X) //目标元素在右半部分
{
i=m+1;
}
else
{
candidate=m; //记录当前m所指向的符合目标元素的下标
j=m-1; //移动指针继续左找
}
}
return candidate;
}
观察以上代码,我们会发现j=m-1词条语句重复出现了两次,于是就出现了下面的优化代码:
public static int LeftMost(int []a,int X) //传入数组和要查找的目标元素值
{
int i=0,j=a.length-1; //让i,j分别指向数组两端位置
//int candidate=-1; //设置临时变量
whlie(i<=j)
{
int m=(i+j)>>>1; //
if(X<=a[m]) //目标元素在左半部分
{
j=m-1;
}
else /*if(a[m]<X)*/ //目标元素在右半部分
{
i=m+1;
}
/*else
{
candidate=m; //记录当前m所指向的符合目标元素的下标
j=m-1; //移动指针继续左找
}*/
}
return i;
}
同理我们可以得到rightmost算法,如下:
2.rightmost
同样的道理,只不过我们要找的元素是最右侧的元素,此时应该返回最右侧的元素下标,因此我们需移动i指针到m指针之前,便于循环的在右侧查找;
public static int RightMost(int []a,int X) //传入数组和要查找的目标元素值
{
int i=0,j=a.length-1; //让i,j分别指向数组两端位置
int candidate=-1; //设置临时变量
whlie(i<=j)
{
int m=(i+j)>>>1; //
if(X<a[m]) //目标元素在左半部分
{
j=m-1;
}
else if(a[m]<X) //目标元素在右半部分
{
i=m+1;
}
else
{
candidate=m; //记录当前m所指向的符合目标元素的下标
i=m+1; //移动指针继续右找
}
}
return candidate;
}
观察以上代码,我们会发现i=m+1词条语句重复出现了两次,于是就出现了下面的优化代码:
public static int LeftMost(int []a,int X) //传入数组和要查找的目标元素值
{
int i=0,j=a.length-1; //让i,j分别指向数组两端位置
//int candidate=-1; //设置临时变量
whlie(i<=j)
{
int m=(i+j)>>>1; //
if(X<a[m]) //目标元素在左半部分
{
j=m-1;
}
else /*if(a[m]<X)*/ //目标元素在右半部分
{
i=m+1;
}
/*else
{
candidate=m; //记录当前m所指向的符合目标元素的下标
i=m+1; //移动指针继续右找
}*/
}
return i-1;
}
3.对比普通二分查找法和for循环查找法时间复杂度
for循环遍历方法:
//for循环查找
public static int findfor(int []a,int X)
{
for(int i=0;i<a.length;i++) //i=0(执行1次)i<a.length执行n-1次,i++执行n次
{
if(a[i]==X) //执行n次
{
return i;
}
}
return -1; //执行1次
} //总共执行fn=(3n+3)次,若每次执行时间为t,则O(n)=(3n+3)*t
普通二分查找法:
public static int LeftMost(int []a,int X) //传入数组和要查找的目标元素值
{
int i=0,j=a.length-1; //执行2次
whlie(i<=j) //执行L+1次
{
int m=(i+j)>>>1; //执行L次
if(X<a[m]) //执行L次
{
j=m-1;
}
else if(a[m]<X) //执行L次
{
i=m+1; //执行L次
}
else
{
return m;
}
}
return -1; //执行1次
}//总共执行fn=5L+4次 O(n)=(5L+4)*t
//通过计算可得若元素个数为n,则whlie循环每次执行L=floor(log2n)+1次
当然少量的数据并不能说明问题的根本,所以我们借助画图工具可得fn=(3n+3)的在某一点之后远远高于fn=5L+4(L=floor(log2n)+1)的图像,所以利用二分查找法更利于我们后面程序的编写;在后面学到的算法依次增多在计算时间复杂度时,实际上是可以将一些影响较小的数据直接抹去只保留影响最大的那一部分的,于是就出现了如下公式方便后面学习的判断:
(按时间复杂度从低到高)