二分查找算法描述
二分查找算法实现
1、i<=j:当 i 等于 j 的时候,有可能 i 和 j 同时指向的元素就是我们要找的元素,所以还需要再进行一次比较。
2、当数组的长度过大,超过int的取值范围的时候,可能会出现 i + j 等于负数的情况(java把二进制首位当成是符号位),可以利用无符号右移运算>>>(除2向下取整),eg:0000 1000=8右移一位等于0000 0100=4。所以可以把m=(i+j)/2换成m=(i+j)>>>1
3、之所以都用小于号<是因为数组元素是升序的,左边的元素比右边的小,i < j 符合主观感受。
二分查找算法的第二种实现方式
1、j = m-1 变为 j = m,这里 j 只是一个边界,不希望 j 指向的元素是查找目标,不希望 j 参与比较,
2、while(i<=j)---->while(i<j):因为 j 指向的元素不会是查找目标,所以当i==j的时候,就不需要再比较一次。
二分查找算法与顺序查找的时间复杂度对比
顺序查找
二分查找:while循环的执行次数和数组内的元素个数有关。
java里的二分查找算法
如果没有找到元素target,通常返回-1,但在java里的binarySearch0方法里,返回的是 -(low+1),这里的low相当于target可以在数组当中插入的位置。例如:
target=4如果要插入数组a里,则是插入到a[1]的位置里。因为在java当中 0 = - 0,所以不能直接返回-low而是要返回 -(low+1)
包含重复元素的二分查找算法(Leftmost)
当数组里面包含了重复的target元素,总是返回最左边的第一个target。当找到一个target时,先把它记录下来当作一个备选目标candidate,然后继续向candidate左边的数组元素里找看看有没有其他target,如果有就更新candidate的值。
更改return的值后,i 代表的是>=target的最靠左的索引,如果数组中存在target,那么返回的就是最左边的target的索引,如果数组中不存在target,那么返回的就是大于但最接近target的值的最左边的索引。
包含重复元素的二分查找算法(Rightmost)
当数组里面包含了重复的target元素,总是返回最右边的第一个target。当找到一个target时,先把它记录下来当作一个备选目标candidate,然后继续向candidate右边的数组元素里找看看有没有其他target,如果有就更新candidate的值。
更改return的值后,i - 1代表的是<=target的最靠右的索引,如果数组中存在target,那么返回的就是最右边的target的索引,如果数组中不存在target,那么返回的就是小于但最接近target的值的最右边的索引。
Leftmost和Rightmost的具体应用(大于某数,小于某数)
求前任如果用Rigthmost的话,如果是数组不存在的元素可以用,但是如果是Rigthtmost(4)会找到最右边的4,没有办法得到前任。
例题1
力扣原题:. - 力扣(LeetCode)
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
1
2
3
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
1
2
3
提示:
- 你可以假设 nums 中的所有元素是不重复的。
- n 将在 [1, 10000]之间。
- nums 的每个元素都将在 [-9999, 9999]之间。
前情提要:
二分查找算法通常会搞不清:
1、while()里头的条件要<还是<=
2、left是等于mid,还是等于mid-1
首先,二分查找通常由以下两种情形:
1、选择左闭右闭的区间,[ left , right ]
2、选择左闭右开的区间,[ left , right )
当你选定了一种方式,在接下来的代码逻辑当中就一定要遵循这种方式的规则。
选择第一种方式:
在这种方式中,left和right都是合法的值,最开始时right=nums.length-1(这是一个合法的可以参与比较的数),在接下来的while循环里条件是while(left<=right),在这条语句里if(target<nums[mid]),因为以及比较过target和中间值的值了,且[ left , right ]要求的是right在接下来的循环里仍旧是一个可以比较的合法的值(次数mid以及比较过了,不能再比较一次)所以要用right=mid-1,而不是j=mid。
选择第二种方式:
在这种方式里,right不是合法的值,最开始时right=nums.length(超越了数组的界限,没有合法值),在接下来的while循环里条件是不能包含right,所以while(left<right),在这条语句里if(target<nums[mid]),因为以及比较过target和中间值的值了,且[ left , right )要求的是right在接下来的循环里是一个不能参与比较的不合法的值(次数mid以及比较过了,不能再比较一次,是一个不合法的值)所以要用right=mid,而不是j=mid-1。在这种方法里的if(target>nums[mid])情况下,因为left要求一个合法的值,所以left要用left=mid+1,而不是left=mid。
解题:
class Solution {
public int search(int[] nums, int target) {
int i=0,j=nums.length-1;
while(i<=j){
int mid=(i+j)/2;
if(target<nums[mid]){
j=mid-1;
}else if(nums[mid]<target){
i=mid+1;
}else{
return mid;
}
}
return -1;
}
}
例题2
力扣原题:. - 力扣(LeetCode)
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
- 输入: [1,3,5,6], 5
- 输出: 2
示例 2:
- 输入: [1,3,5,6], 2
- 输出: 1
示例 3:
- 输入: [1,3,5,6], 7
- 输出: 4
示例 4:
- 输入: [1,3,5,6], 0
- 输出: 0
分析
首先采用二分查找算法的左闭右开[left,right)的书写方式,因为采用左闭右闭的话,后面找不到数组元素的情况会有很多种,会很麻烦。二分查找算法来找包含在数组内的元素。
关于不在数组内的元素,有三种情况:
1、查找的元素不在数组内并且插入的位置在最左边。
2、查找的元素不在数组内并且插入的位置在中间。
3、查找的元素不在数组内并且插入的位置在最右边。
第一种和第二种情况可以合并为一种情况来看:因为在这种情况下left和right最后会指向同一个位置(此时指向的位置是有效的索引),如果target小于left和right指向的元素,那么target就要插入到left和right当前的位置,如果target大于left和right指向的元素,那么target就要插入到left和right当前位置的后一个位置。
第三种情况单独谈:在这种情况下,left和right最终会相等,并且指向无效索引nums.length,如果target大于数组最右边的元素(也就是最大的那个元素),那么target就要插入到left和right当前的索引(nums.length),如果target小于数组最右边的一个元素,那么target就要插入到left和right当前位置的后一个位置(也就是num.length-1)
解题
class Solution {
public int searchInsert(int[] nums, int target) {
int i=0,j=nums.length;//[left,right)
while(i<j){
int mid=(i+j)/2;
if(target<nums[mid]){
j=mid;
}else if(nums[mid]<target){
i=mid+1;
}else{
return mid;
}
}
if(i==nums.length){//第三种情况
if(target<nums[i-1]){
return i;
}else{
return i;
}
}else{//第一二种情况
if(target<nums[i]){
return i;
}else{
return i+1;
}
}
}
}
例题3
力扣原题:. - 力扣(LeetCode)
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
进阶:你可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题吗?
示例 1:
- 输入:nums = [5,7,7,8,8,10], target = 8
- 输出:[3,4]
示例 2:
- 输入:nums = [5,7,7,8,8,10], target = 6
- 输出:[-1,-1]
示例 3:
- 输入:nums = [], target = 0
- 输出:[-1,-1]
分析
在这道题里有三种情况:
1、数组内和target相等的元素>1
2、数组内和target相等的元素=1
3、数组内没有target
代码解读:在原来的二分查找算法做一点儿改动,当找到target的时候不是直接返回索引,而是现用candidate记录这个索引,然后如果是找最左边的target,那么接下来搜索的区域就要往左,不用去看右边了,这也是j=j-1的原因,如果是找最右边的target,那么接下来搜索的区域就要往右,不用去看左边了,这也是i=i+1的原因。
1和2两种情况可以一起看,下面用了searchLeft和searchRight来找到左边界和右边界(第二种情况,left和right的值相同,也直接返回left和right就好)
第三种情况:最先开始用candidate=-1,记录找到的target索引,如果当前数组内没有target,那么searchLeft和searchRight都是直接返回candidate=-1。
解题
class Solution {
public int[] searchRange(int[] nums, int target) {
int left=searchLeft(nums,target);
int right=searchRight(nums,target);
int[] result=new int[2];
if(left==-1){
result[0]=result[1]=-1;
}else{
result[0]=left;
result[1]=right;
}
return result;
}
public int searchLeft(int[] nums,int target){
int i=0,j=nums.length-1,candidate=-1;
while(i<=j){
int mid=(i+j)/2;
if(target<nums[mid]){
j=mid-1;
}else if(nums[mid]<target){
i=mid+1;
}else{
candidate=mid;
j=j-1;
}
}
return candidate;
}
public int searchRight(int[] nums,int target){
int i=0,j=nums.length,candidate=-1;
while(i<j){
int mid=(i+j)/2;
if(target<nums[mid]){
j=mid;
}else if(nums[mid]<target){
i=i+1;
}else{
candidate=mid;
i=i+1;
}
}
return candidate;
}
}