二分查找算法学习
最近学习了二分查找算法后,发现在查阅网上的代码实现时有多种实现形式,因此将各种实现方式以及区别适用场景整理出来,以供学习…
算法介绍
二分查找也叫作折半查找,在某些情况下使用二分查找的效率要比顺序查找的效率要高很多,但是使用二分查找的前提是数组必须是有序的
1.基础版
这种是最常见的形式,也是最好理解的一种形式,代码注意点如下
- 这里
j = nums.length - 1
表示的是将j指向的元素也加入比较 - 因为
j
指向的元素加入比较了,因此需要考虑i == j
的情况,所以循环条件为i <= j
public int binarySearchBasic(int[] nums,int target){
int i = 0,j = nums.length - 1; // 注意点1
while(i <= j){ // 注意点2
int mid = (i + j) >>> 1;
if(target < nums[i]){
j = mid - 1;
}else if (nums[i] < target){
i = mid + 1;
}else{
return mid;
}
}
return -1; //-1表示查找不到target
}
2.改动版
这里的改动是相较于基础版的,代码注意点如下:
- 这里
j = nums.length
表示j此时不参与比较,j只做遍历元素的边界 - 由于
j
只是做边界不参与比较,因此当target
小于当前元素时,右边界j = mid
(因为nums[mid]已经比较过了,之后不会参与比较了,故可做边界) i
是指向需要进行比较的元素的,而j
不参与比较,因此循环条件中i < j
,如果加上了i == j
的话 有可能会导致死循环情况(target < nums[i]并且target不存在)
public int binarySearchPromote(int[] nums,int target){
int i = 0, j = nums.length; //注意点1
while(i < j){ //注意点3
int mid = (i + j) >>> 1;
if(target < nums[i]){
j = mid; //注意点2
}else if (nums[i] < target){
i = mid + 1;
}else{
return mid;
}
}
return -1;
}
3.平衡版
这种实现方式主要是通过减少循环内平均的比较次数来提高查找效率的
在前两种方式如果一直都是nums[i] < target
,那么就每次都进行两次比较(先比较target < nums[i] 再比较 nums[i] < target)
- 这里循环条件
j - i > 1
是因为这种方式不确定是否已经找到了元素了可以返回了,只能通过缩小范围来结束查找 - 如果target存在,则
i
下标对应的元素就是target值,因此跳出循环后只需要判断nums[i] == target
是否成立即可
//先找到对应下标,再比较值是否一致; 循环内的平均比较次数减少了
public int binarySearch(int[] nums,int target){
int i = 0, j = nums.length;
while(j - i > 1){ //注意点1
int mid = (i + j) >>> 1;
if(target < nums[i]){
j = mid;
}else{
i = mid; //注意点2
}
}
if(nums[i] == target){
return i;
}else{
return -1;
}
}
4.Java版
这种实现方式是java中Arrays类中的binarySearch()方法
这种写法与1.基础版的写法一样,只不过找不到时返回的值是 - (insertion point + 1)
insertion point 就是插入目标元素这个插入点的下标,也可以看出来其实基础版的
i的下标
就是插入点
public int binarySearch0(int[] a,int fromIndex,int toIndex, int key){
int low = fromIndex;
int high = toIndex - 1;
while(low <= high){
int mid = (low + high) >>> 1;
int midVal = a[mid];
if(midVal < key){
low = mid + 1;
}else if (midVal > key){
high = mid - 1;
}else{
return mid;
}
}
return -(low + 1);
}
这种方式查找方式可以用在 寻找插入元素的位置 的场景
5.LeftMost
这种实现方式与平衡版的区别在于 找到target时的处理 和 返回值为i
- 这里当找到
target == nums[i]
时右边界会向左移动
因为会向左移动,因此这里返回i
的含义值得思考
- 如果找得到值,那么返回的就是满足条件的最左侧的元素的下标。例如
target=8
,nums={1,1,1,4,8,8,8,9,21}
,这里返回的就是第一个8 - 如果找不到对应的值,那么返回的就是大于target的、最左侧的元素的下标(nums[x] >= target)。例如
target
等于8,nums={1,2,3,7,9,9,10,11}
,这里返回的就是第一个9 - 应用场景:找出某个值的后任、最近的领军、排名
LeftMost可以理解成就是最左的,离target最近的还要是最左的那就是大于target的值
public int binarySearchLeftMost(int[] nums,int target){
int i = 0,j = nums.length - 1;
while(i <= j){
int mid = (i + j) >>> 1;
if(target <= nums[i]){ //注意点1
j = mid - 1;
}else{
i = mid + 1;
}
}
return i;
}
6.RightMost
这种实现与LeftMost的区别在于
- 当
nums[i] == target
时,左边界向右移动 - 返回值为
i - 1
这里返回值 i-1
的含义
- 如果找到值,返回的就是满足条件的最右侧的元素的下标。
- 如果找不到值,返回的就是小于target的、最右侧的元素的下标(nums[x] <= target)
同理,RightMost可以理解为最右的,离target最近的并且是最右边的那就是小于target的值
public int binarySearchLeftMost(int[] nums,int target){
int i = 0,j = nums.length - 1;
while(i <= j){
int mid = (i + j) >>> 1;
if(target < nums[i]){
j = mid - 1;
}else{
i = mid + 1;
}
}
return i - 1;
}
为什么这里要返回
i - 1
,其实是因为当不考虑target==nums[i]
时,其他情况的处理逻辑都是一样的,最终的结果就是都会找到大于target的最左边的元素的下标,即i就指向大于target的、最左侧的下标。这个下标再去减1,由于target不存在,因此就是i-1
就是小于target的最右侧的元素的下标了
总结
总结起来,二分查找算法有多种实现形式和变体,每种实现方式针对不同的查找需求进行了优化。
基本二分查找适用于普通的有序数组查找,而其他的变体适用于有序数组中存在重复元素或需要找到特定类型元素的情况。
因此在大部分情况我们选定一种理解的作为日常使用即可,只是在看到其他的变体的时候我们也能理解为什么是这种形式…