二分搜索算法:
主要问题:while是否有=,mid的加一和减一
常见搜索场景:寻找一个数,寻找一个数的左侧边界,寻找一个数的右侧边界
1 寻找一个数字:搜索区间选择双闭,故while要加=(退出条件是left==right+1),mid+1,mid-1。这样虽然也可以通过线性搜索这个数的左、右侧边界,但这样就难以保证二分搜索对数级的时间复杂度了
2 寻找一个数的左侧边界:搜索区间选择双闭,故while要加=(退出条件是left==right+1),mid+1,mid-1,等于target时right=mid-1,返回left,要检查其是否越上界
3 寻找一个数的右侧边界:搜索区间选择双闭,故while要加=(退出条件是left==right+1),mid+1,mid-1,等于target时left=mid+1,返回right,要检查其是否越下界
这里主要是借鉴《labuladong的算法小抄》p71~p84的思想
例题1:在排序数组中查找元素的第一个和最后一个位置
将"寻找一个数的左侧边界"和"寻找一个数的右侧边界"合体
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result=new int[2];
result[0]=mySearch(nums,target,"left");
result[1]=mySearch(nums,target,"right");
return result;
}
public int mySearch(int[] nums, int target,String type){
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
if(type=="left"){ //左右边界的区别1
right=mid-1;
}else{
left=mid+1;
}
}else if(nums[mid]>target){
right=mid-1;
}else if(nums[mid]<target){
left=mid+1;
}
}
if(type=="left"){ //左右边界的区别2
if(left>=nums.length||nums[left]!=target){
return -1;
}
return left;
}else{
if(right<0||nums[right]!=target){
return -1;
}
return right;
}
}
}
例题2:在排序数组中查找数字 I
例题1的衍生
class Solution {
public int search(int[] nums, int target) {
int smallIndex=mySearch(nums,target,"left");
int bigIndex=mySearch(nums,target,"right");
return (smallIndex==-1)?0:bigIndex-smallIndex+1; //注意判断为-1的情况,以及其他情况时下标之差+1
}
public int mySearch(int[] nums, int target,String type){
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
if(type=="left"){
right=mid-1;
}else{
left=mid+1;
}
}else if(nums[mid]>target){
right=mid-1;
}else if(nums[mid]<target){
left=mid+1;
}
}
if(type=="left"){
if(left>=nums.length||nums[left]!=target){
return -1;
}
return left;
}else{
if(right<0||nums[right]!=target){
return -1;
}
return right;
}
}
}
例题3:0~n-1中缺失的数字
这题即可以理解为找下标和内容符合的右边界下标+1,或者下标和内容不符合的左边界下标。(这里要分清楚这题下标和内容的关系)
下标和内容符合的右边界下标+1:
class Solution {
public int missingNumber(int[] nums) {
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==mid){
left=mid+1;
}else if(nums[mid]!=mid){
right=mid-1;
}
}
return right+1;
}
}
下标和内容不符合的左边界下标:
class Solution {
public int missingNumber(int[] nums) {
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]!=mid){
right=mid-1;
}else if(nums[mid]==mid){
left=mid+1;
}
}
return left;
}
}
例题4:旋转数组的最小数字
为什么本题二分法不用 nums[mid] 和 nums[left] 作比较?
二分的目的是判断结果和mid的位置关系,从而缩小区间。而在 nums[m] > nums[i]情况下,无法判断m在哪个排序数组中。
如1,2,3,4,5和2,3,4,5,1。结果可能在mid的左边,也可能在mid的右边。
class Solution {
public int minArray(int[] numbers) {
int left=0,right=numbers.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(numbers[mid]<numbers[right]){
right=mid;
}else if(numbers[mid]>numbers[right]){
left=mid+1;
}else if(numbers[mid]==numbers[right]){
--right;
}
}
return numbers[left];
}
}
例题5:搜索旋转排序数组
根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分
class Solution {
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[right]){ //有序的在右侧。其实和nums[left]比也可以
if(target>nums[mid]&&target<=nums[right]){ //target在右侧
left=mid+1;
}else{
right=mid-1;
}
}else{ //有序的在左侧。其实和nums[left]比也可以
if(target>=nums[left]&&target<nums[mid]){ //target在左侧
right=mid-1;
}else{
left=mid+1;
}
}
}
return -1;
}
}
例题6:寻找两个正序数组的中位数
解法1:归并为一个数组后,得到中位数
时间复杂度O(n+m)
空间复杂度O(n+m)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n=nums1.length;
int m=nums2.length;
if(n+m==0){
return -1;
}
int[] temp=new int[n+m];
int p1=0,p2=0,p3=0;
while(p1<n&&p2<m){
if(nums1[p1]<nums2[p2]){
temp[p3++]=nums1[p1++];
}else{
temp[p3++]=nums2[p2++];
}
}
if(p1!=n){
while(p1<n){
temp[p3++]=nums1[p1++];
}
}
if(p2!=m){
while(p2<m){
temp[p3++]=nums2[p2++];
}
}
if((m+n)%2==0){
return (temp[(m+n)/2-1]+temp[(m+n)/2])*1.0/2;
}else{
return temp[(m+n)/2];
}
}
}
解法2:不进行合并,只要找到中位数的位置即可
由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始时分别指向两个数组的下标 0 的位置,每次将指向较小值的指针后移一位(如果一个指针已经到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。
时间复杂度 O(n+m)
空间复杂度 O(1)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n=nums1.length;
int m=nums2.length;
if((n+m)==1){
return n!=0?nums1[0]:nums2[0];
}
int target=(n+m)/2;
int p1=0,p2=0;
int number1=0,number2=0;
while(p1<n&&p2<m){
if((p1+p2)==target-1){
if(nums1[p1]<=nums2[p2]){
number1=nums1[p1];
}else{
number1=nums2[p2];
}
}
if((p1+p2)==target){
if(nums1[p1]<nums2[p2]){
number2=nums1[p1];
}else{
number2=nums2[p2];
}
}
if(nums1[p1]<nums2[p2]){
++p1;
}else{
++p2;
}
}
if(p1!=n){
while((p1+p2)<=target){
if((p1+p2)==target-1){
number1=nums1[p1];
}
if((p1+p2)==target){
number2=nums1[p1];
}
++p1;
}
}
if(p2!=m){
while((p1+p2)<=target){
if((p1+p2)==target-1){
number1=nums2[p2];
}
if((p1+p2)==target){
number2=nums2[p2];
}
++p2;
}
}
if((m+n)%2==0){
return (number1+number2)*1.0/2;
}else{
return number2;
}
}
}
解法3:二分查找
这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2或 (m+n)/2+1。
假设两个有序数组分别是A 和B。要找到第 k 个元素,我们可以比较A[k/2-1]和B[k/2-1],其中 /表示整数除法。由于A[k/2−1] 和B[k/2−1] 的前面分别有A[0…k/2−2] 和B[0…k/2−2],即k/2−1 个元素,对于A[k/2−1] 和B[k/2−1] 中的较小值,最多只会有(k/2−1)+(k/2−1)≤k−2 个元素比它小,那么它就不能是第 k小的数了。
时间复杂度 O(log(n+m))
空间复杂度 O(1)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n1=nums1.length,n2=nums2.length;
//奇偶数不同情况
if((n1+n2)%2==0){
int k1=(n1+n2)/2,k2=(n1+n2)/2+1;
return (kSmall(nums1,nums2,k1)+kSmall(nums1,nums2,k2))/2.0;
}else{
int k=(n1+n2)/2+1;
return (double)kSmall(nums1,nums2,k);
}
}
//两个数组中第k小的数
public int kSmall(int[] nums1, int[] nums2,int k){
int n1=nums1.length,n2=nums2.length;
int index1=0,index2=0;
while(true){
//终止边界条件
if(index1==n1){
return nums2[index2+k-1];
}
if(index2==n2){
return nums1[index1+k-1];
}
if(k==1){
return Math.min(nums1[index1],nums2[index2]);
}
//二分
int half=k/2;
int newIndex1=Math.min(index1+half,n1)-1;
int newIndex2=Math.min(index2+half,n2)-1;
if(nums1[newIndex1]<=nums2[newIndex2]){
k-=(newIndex1-index1+1);
index1=newIndex1+1;
}else{
k-=(newIndex2-index2+1);
index2=newIndex2+1;
}
}
}
}
解法4:划分数组
在统计中,中位数被用来:将一个集合划分为两个长度相等的子集,其中一个子集中的元素总是大于另一个子集中的元素。
时间复杂度 O(log(min(n,m)))
空间复杂度 O(1)
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if(nums1.length>nums2.length){
return findMedianSortedArrays(nums2,nums1);
}
int m=nums1.length;
int n=nums2.length;
int left=0,right=m;
// median1:前一部分的最大值
// median2:后一部分的最小值
int median1=0,median2=0;
while(left<=right){
// 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
// 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
int i=(left+right)/2;
int j=(m+n+1)/2-i;
// nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
int nums_im1=(i==0?Integer.MIN_VALUE:nums1[i-1]);
int nums_i=(i==m?Integer.MAX_VALUE:nums1[i]);
int nums_jm1=(j==0?Integer.MIN_VALUE:nums2[j-1]);
int nums_j=(j==n?Integer.MAX_VALUE:nums2[j]);
if(nums_im1<=nums_j){
median1=Math.max(nums_im1,nums_jm1);
median2=Math.min(nums_i,nums_j);
left=i+1;
}else{
right=i-1;
}
}
return (m+n)%2==0?(median1+median2)/2.0:median1;
}
}