二分查找
记录leetcode关于二分查找的题目
二分法的基本思想都是 减而治之
一、常规二分查找
1. 查找法
public int IterativeBS(int[] nums, int target){
// 应用的是查找的思想
int left=0, right=nums.length-1;
// 最后会得到一个空区间
while(left<=right){
int mid = left + (right-left) / 2;
// 右查找只需要将判断条件增加等号判断,可以表示越过左侧相等元素
if(nums[mid] < target){
left = mid+1;
}
else right = mid-1;
}
return left;
}
使用查找法最直观的一点就是最后得到的是一个空区间,比较适合用于待查元素可能不在有序数组中的查找。
2. 排除法
public int IterativeBSExclude(int[] nums, int target){
int left=0, right=nums.length-1;
// 对于出现在最后位置的情况,该算法没有考虑进去,因此需要单独判断或者将右指针包括该位置
// if(nums[right] < target) return right+1;
// 最后会得到一个单一的值
while(left<right){
int mid = left+(right-left)/2;
if(nums[mid] < target){
left = mid+1;
}
// 否则还是有可能存在于右侧中,不能直接进行剔除
else right = mid;
}
return left;
}
使用排除法在一些复杂情况中处理起来更为方便,适合于待查元素存在于有序数组中的查找。(上面代码中如果需要查询任意一个数字的插入位置时,需要将注释代码还原,或者将mid向上取整)
3. 递归版本的查找法
public int RecursiveBS(int[] nums, int target){
return RBS(nums, 0, nums.length-1, target);
}
private int RBS(int[] nums, int left, int right, int target){
if(left > right) return left;
int mid = left+(right-left)/2;
if(nums[mid] < target) return RBS(nums, mid+1, right, target);
else return RBS(nums, left, mid-1, target);
}
PS:如果数组中包含重复元素,则上述皆为左查询,即返回的是满足条件的最左边的值。
二、寻找旋转排序数组的最小值
1、无重复元素
public int findMin(int[] nums) {
// 该实现为查找版本,需要额外考虑很多情况
// int left=0, right = nums.length-1;
// if(right<0) return -1;
// while(left <= right){
// int mid = left+(right-left)/2;
//
// // 出现中间位时的必要判断
// if(mid >0 && nums[mid]<nums[mid-1])
// return nums[mid];
// if(nums[mid] < nums[right]){
// right = mid-1;
// }else left = mid+1;
// }
//
// // 设置判断条件,当位置不在初始位上时,找到的位置位于下一位
// if(left>0) return nums[left-1];
// return nums[left];
// 使用排除法的更简洁版本
int left=0, right=nums.length-1;
if(right<0) return -1;
while(left < right){
int mid = left+(right-left)/2;
if(nums[mid] < nums[right]){
// 核心点:当前的位置也可能为最终结果,也是此处没有想好
right=mid;
}else left=mid+1;
}
return nums[left];
}
不包含重复元素时,可以通过测试发现右侧有序时,待查结果一定会位于左侧,利用这个不变量进行排除。
2、 可能包含重复元素
public int findMinRt(int[] nums) {
int left=0, right=nums.length-1;
if(right<0) return -1;
while(left < right){
int mid = left+(right-left)/2;
if(nums[mid] < nums[right]){
right=mid;
}else if(nums[mid] > nums[right]) left=mid+1;
// 由于使用的是右边判断,因此出现相同情况时应该优先缩小右区间
// 错误案例:1,3,3
// else ++left;
else --right;
}
return nums[left];
}
对于包含重复元素的情况,此时需要考虑一种特殊情况,即出现中间与右边相等时(0,2,2,2),此时如果还是按照之前的判断进行右移,则会出现直接跳过结果的情况,因此需要单独进行判断。
三、搜索旋转排序数组
1、无重复元素
public int search(int[] nums, int target) {
// 该类型不适合采用排除法,因为有可能不存在,需要额外的判断
int left=0, right=nums.length-1;
while(left <= right){
int mid = left + (right-left) / 2;
if(nums[mid] == target) return mid;
if(nums[mid] >= nums[left]){
if(nums[mid] > target && nums[left] <= target){
right = mid-1;
}else left = mid+1;
}else {
if(nums[mid] < target && nums[right] >= target){
left = mid+1;
}else right=mid-1;
}
}
return -1;
}
此处选择查找法,对于元素不一定存在与数组中的情况,查找法更为合适。注意到这里事先进行的是右侧判断,原因是因为右边有序比左边有序的情况更简单(可以通过实例来找到这个规律)
2、可能包含重复元素
public boolean search_repeat(int[] nums, int target) {
int left = 0, right = nums.length-1;
if(right<0) return false;
while(left <= right){
int mid = left + (right-left) / 2;
if(nums[mid] == target) return true;
// 由题意,可知待查询结果不一定存在于数组中,因此采用查找法比较合适
if(nums[mid] > nums[left]){
// 左有序
if(target >= nums[left] && target < nums[mid]){
right = mid-1;
}else left = mid+1;
}else if(nums[mid] < nums[left]){
// 右有序
if(target <= nums[right] && target > nums[mid]){
left = mid+1;
}else right = mid-1;
// 两边相等时退化为逐步查询
}else left++;
}
return false;
}
出现了重复元素,则需要将相等时候的情况进行单独考虑,缩小判断区间,算法会退化为O(n)时间复杂度。
四、山脉数组
1、山脉数组的峰顶索引
public int peakIndexInMountainArray(int[] arr) {
// 由题意,一定会存在一个满足该条件的索引,因此选择排除法
int left =0, right = arr.length-1;
// 甚至能够省略判断条件,右边会收缩到最终位
// if(arr[0]>arr[1]) return 0;
while(left<right){
// 因为先对右边进行收缩,因此采用向上取整
int mid = left + (right-left+1) / 2;
if(mid>0 && arr[mid-1]>arr[mid]){
right = mid-1;
}else left = mid;
}
return left;
}
排除法+向上取整(排除法先对右边进行收缩,所以采用向上取整)。
2、山脉数组中查找目标值
public int findInMountainArray(int target, MountainArray mountainArr) {
// 先寻找峰值(必存在,排除法)
int left=0 ,right = mountainArr.length()-1, mid= 0;
while(left < right){
mid = left + (right-left+1) /2;
if(mid>0 && mountainArr.get(mid-1)>mountainArr.get(mid)){
right = mid-1;
}else left= mid;
}
int peak_index = left, pos=-1;
if(mountainArr.get(peak_index) < target) return pos;
// 再寻找左边符合的下标(可能不存在,查找法)
left = 0;
right = peak_index;
while(left<= right){
mid = left+ (right-left)/2;
if(mountainArr.get(mid) < target){
left = mid+1;
}else right = mid-1;
}
if(mountainArr.get(left) == target) return left;
// 如果没找到,再找右边的
left = peak_index+1;
right = mountainArr.length()-1;
while(left <= right){
mid = left + (right-left)/2;
if(mountainArr.get(mid) < target){
right = mid-1;
}else left = mid+1;
}
// 注意右侧与左边会刚好相反,因此最后的结果也需要处理
if(mountainArr.get(left-1) == target) return left-1;
return pos;
}
这里主要注意山脉数组的定义,并不会出现层峦叠嶂的现象!!!
五、寻找两个正序数组的中位数
暴力法(逐项过滤)
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 朴素法,先合并再利用中位数性质求取
int len_1 = nums1.length, len_2 = nums2.length;
int center = (len_1+len_2) / 2, even = 0;
if((len_1+len_2)%2 == 0) even = 1;
int[] res = new int[]{0,0};
int i_1 = 0, i_2 = 0, count=0;
while(i_1<len_1 || i_2<len_2){
// 设置尾部判断条件,防止越界错误
int cur_val_1 = 1000000+1, cur_val_2 = 1000000+1;
if(i_1 < len_1) cur_val_1 = nums1[i_1];
if(i_2 < len_2) cur_val_2 = nums2[i_2];
if(cur_val_1 < cur_val_2){
i_1 ++;
}else i_2++;
// 先不管奇偶性,都找到后再进行判断
if(count == center-1) res[0] = Math.min(cur_val_1, cur_val_2);
if(count == center){
res[1] = Math.min(cur_val_1, cur_val_2);
break;
}
count++;
}
if(even == 0) return res[1];
return (double)(res[0]+res[1])/2;
}
自己写的暴力法,不需要额外的空间开销,但是还是有点冗余,可以写得更加优美。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len_1 = nums1.length, len_2 = nums2.length;
int i_1=0, i_2 = 0;
int len = len_1+len_2, pre_value=0, cur_value=0;
for(int j=0;j<=len/2;++j){
pre_value = cur_value;
// 当前一个数组还存在元素,并且比后一个数组元素小或者后一个数组没元素时,从前一个数组中选取
if(i_1<len_1 && (i_2>=len_2 || nums1[i_1]<nums2[i_2])){
cur_value = nums1[i_1++];
}else cur_value = nums2[i_2++];
}
if(len%2==1) return cur_value;
return (double)(cur_value+pre_value)/2;
}
优化的部分:将奇偶判断合并,取消了尾部判断,直接换成条件赋值。时间复杂度为O(n+m),空间复杂度为O(1)。
寻找第k小数(二分优化)
public double findMedianSortedArrays_Recursive(int[] nums1, int[] nums2){
int len_1 = nums1.length, len_2 = nums2.length;
// 由于k是从1开始的,因此需要寻找的是上界与下一个值
int pre = (len_1+len_2+1) / 2, cur=(len_1+len_2+2) / 2;
return (getKthValue(nums1, 0, len_1-1, nums2, 0, len_2-1, pre)+getKthValue(nums1,0,len_1-1, nums2,0, len_2-1, cur))*0.5;
}
// 寻找第k小数
private int getKthValue(int[] nums1, int st_1, int ed_1, int[] nums2, int st_2, int ed_2, int k){
int len1 = ed_1-st_1+1;
int len2 = ed_2-st_2+1;
// 始终将短数组作为 nums1,便于后续的判断
if(len1 > len2) return getKthValue(nums2, st_2, ed_2, nums1, st_1, ed_1, k);
// 当第一个数组中已经被排除完后,答案必定在另一个数组中,直接返回即可
if(len1 == 0) return nums2[st_2+k-1];
// 当只剩下最后一个时,选择其中的小值作为最终答案
if(k == 1) return Math.min(nums1[st_1], nums2[st_2]);
// 注意截取的长度不能超过当前数组的最大长度
int i = st_1+ Math.min(k/2, len1)-1;
int j = st_2+ Math.min(k/2, len2)-1;
// 删除两者中的小值,注意此处删除的值在最终合并数组中不一定都是连续的,但是必定是在第k小数的左边
if(nums1[i] < nums2[j]){
return getKthValue(nums1, i+1, ed_1, nums2, st_2, ed_2, k-(i-st_1+1));
}else
return getKthValue(nums1, st_1, ed_1, nums2, j+1, ed_2, k-(j-st_2+1));
}
将求取中位数转化为寻找第k小数问题,需要注意的是这个k是从1开始的,所以在使用时需要-1(边界条件害死人)。时间复杂度为O(log(m+n)),空间复杂度为O(1)。