Loading...

LeetCode分类专题(三)——二分查找1

  iwehdio的博客园:https://www.cnblogs.com/iwehdio/

学习自:

1、二分搜索

  • 二分搜索
    • 首先,二分搜索的思想是,每次减少问题的一半规模。对于有序数组而言,是通过每次比较mid的值与目标值的大小而定的。
    • 根据右边界是闭区间还是开区间,可以分为两种。区别在于:
      1. 右边界的初始值是length还是length-1。
      2. 结束条件是left < right还是left <= right。
      3. 改变右边界时是right = mid还是right = mid - 1。
      4. 一般来说,选取闭区间的程序可读性较好,所以一般选第二种。
    • 查找有序数组中的元素:
int binarySearch(int[] nums, int target) {
    int left = 0; 
    int right = nums.length; // 注意
    while(left < right) { // 注意
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1;
        else if (nums[mid] > target)
            right = mid; // 注意
        }
    return -1;
}


//-----------------------------------------


int binarySearch(int[] nums,int target) {
    int left = 0; 
    int right = nums.length-1; // 注意
    while (left <= right) {  // 注意
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            return mid;
        }else if (nums[mid] < target) {
            left  = mid + 1;
        }else if (nums[mid] > target) {
            right = mid - 1; // 注意
        }
    }
    return -1
} 
  • 对于右边界为length-1的情况而言:

    1. left/right左右边界对应的就是最左和最右元素的索引。

    image-20210218171709392

    1. nums[mid] < target就意味着mid的左侧都比target小,那么target可能落在 [mid,hi],但是mid如果单独判断过的话,就是[mid+1,hi],所以left赋值为mid+1,即left左侧(不包含)都比target小。
    2. 相应的,right右侧(不包含)都比target大,所以right赋值为mid-1。
    3. 结束条件是left <= right。这意味着,最后一次循环时,left与right是重合的。此时,只需要判断这个重合的位置mid是否等于target。如果等于就找到返回。
    4. 如果不等于,就应该返回插入位置。插入位置的含义是,数组中大于target的最小值。
    • 如果mid小于target,说明left应该右移一位,插入位置在left。
    • 如果mid大于target,说明right应该左移一位,插入位置还是在left。
  • 查找有序数组中的元素并返回插入位置:

int binarySearch(int[] nums,int target) {
    int left = 0; 
    int right = nums.length-1;
    while (left <= right) {
        int mid = left + ((right - left) >> 1);
        if (nums[mid] == target) {
            return mid;
        }else if (nums[mid] < target) {
            left  = mid + 1;
        }else if (nums[mid] > target) {
            right = mid - 1;
        }
    }
    return left;
}
  • 如果数组中存在重复元素,那么目标就成了搜索重复元素的最左或最右端元素。如果是找做边界,在这种情况下,left的含义还是左侧(不包含)都比target小,而right的含义变为了right右侧(不包含)都大于等于target,此时target=mid的情况归入right。

  • 因此,最后的情况就是,最后一次循环中,重合的mid要么是target的左边界,要么是左边界的往左一个元素。因为只有这两种情况才能同时符合left和right的定义。

    image-20210218171718422

  • 计算左右边界,二者唯一的区别就是等号被归入了不同的情况:

int leftBound(int[] nums, int target) {
      int left = 0, right = nums.length - 1;
      while (left <= right) {
          int mid = left + ((right - left) >> 1);
          if (target <= nums[mid]) {
              right = mid - 1;
          }else if (target > nums[mid]) {

              left = mid + 1;
          }
      }
      return left;
  }
  
  //------------------------------------------------------
  
int rightBound(int[] nums, int target) {
      int left = 0, right = nums.length - 1;
      while (left <= right) {
          int mid = left + ((right - left) >> 1);
          if (target >= nums[mid]) {
               left = mid + 1; 
          }else if (target < nums[mid]) {
              right = mid - 1;
          }
          
      }
      return left;
  }

2、旋转数组

  • 旋转数组

    • 旋转数组,是由有序数组从一个点旋转而来的,也可以用二分查找来查找元素。原因是,二分查找只是要求每次减少一半规模,如果不是完全有序的话,不能只根据mid与target的大小关系,需要借助其他条件。

    image-20210218171727125

    • 根据二分的思想,应该得出target在mid的左侧或者右侧。在无重复元素的情况下:
      • 如果mid大于left,那么mid左侧是有序的,可以得出:
        • 如果left<target<mid,那么target在mid左侧,否则在右侧。
      • 如果mid小于left,那么mid右侧是有序的,同样可以得出:
        • 如果mid<target<right,那么target在mid右侧,否则在左侧。
class Solution {
    public int search(int[] nums, int target) {
        int lo = 0, hi = nums.length-1;
        while(lo<=hi) {
            int mid = (lo+hi)/2;
            if(nums[mid]==target) {
                return mid;
            } else if(nums[mid]>=nums[lo]) {
                if(target>=nums[lo] && target<nums[mid]) {
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            }  else {
                if(target>nums[mid] && target<=nums[hi]) {
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            }
        }
        return -1;
    }
}
  • 如果旋转数组中有重复元素的话,就可能出现mid与left相等的情况,这样就没法判断是左边有序还是右边有序了,需要移动左边界一直到左边界与mid不等。

image-20210218171736611

class Solution {
    public boolean search(int[] nums, int target) {
        int lo=0, hi = nums.length-1;
        while(lo<=hi) {
            int mid = (lo+hi)/2;
            if(nums[mid]==target) {
                return true;
            }
            if(nums[mid]==nums[lo]) {
                lo++;
                continue;
            }
            if(nums[mid]<nums[lo]) {
                if(target>nums[mid] && target<=nums[hi]) {
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            } else {
                if(target<nums[mid] && target>=nums[lo]) {
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            }
        }
        return false;
    }
}
  • 查找旋转数组中的最小值,这种情况下延续之前的思想,比较mid与left:
    • 如果mid小于left,则[mid,right]是有序的,最小值可能在[left,mid]中。
    • 如果mid大于等于left,则[left,mid]是有序的,最小值可能在[mid+1,right]中。
    • 需要注意的是退化情况,即如果数组是有序的,直接返回left。这通过比较left和right来决定。
  • 为什么left=mid+1,而right=mid?
    • 因为mid右侧有序,那么mid本身可能是最小值,但是如果mid左侧有序,那么mid必定不是最小值。
  • 为什么=mid的情况归到了left?
    • 因为如果出现=的情况,一定是区间只存在left/mid和right两个元素,而又没有left<right自然有序,最小值就是right了。
class Solution {
    public int findMin(int[] nums) {
        int lo = 0, hi = nums.length-1;
        while(lo<=hi) {
            if(nums[lo]>nums[hi]) {
                int mid = (lo+hi)/2;
                if(nums[mid]>=nums[lo]) {
                    lo = mid+1;
                } else {
                    hi = mid;
                }
            } else {
                return nums[lo];
            }
        }
        return nums[lo];
    }
}

3、题目

  • 29 两数相除

    image-20210216200903469

    image-20210216202654863

    • 这个题的如果使用一次次减来等效乘法,时间复杂度太高了,所以需要倍数递增。
    • 比如19/6,19比6大,然后6倍增为12;19比12大,然后12倍增为24;19比24小,则19/6的商为:倍数2 + (19-12)/6。递归的执行。
    • 所以,这里div函数的含义是,计算出x<y*2^n中n的最大值(x、y为负数)。
    • 这里比较坑的是,需要考虑int类型的溢出问题。数值为-231~231-1,也就是说,整数比负数少一个,因此可以全部转为负数来规避溢出。
    • 此外,在倍增过程中,如果当前的减数大于Integer.MIN_VALUE=-2^31,也不能再递增了,否则就会溢出。而最后的结果,如果是Integer.MIN_VALUE,则其相反数会溢出,需要返回Integer.MAX_VALUE。
    • 溢出的结果是Integer.MAX_VALUE+1=,Integer.MIN_VALUE-1=Integer.MAX_VALUE。
    class Solution {
    
        int ans = 0;
        public int divide(int dividend, int divisor) {
            //全部转为负数
            boolean flag = false;
            if((dividend<0 && divisor>0)||(dividend>0 && divisor<0)) {
                flag = true;
            }
            if(dividend>0) dividend = -dividend;
            if(divisor>0) divisor = -divisor;
            //递归除法
            while(dividend <= divisor) {
                dividend = div(dividend, divisor);
            }
            //根据flag复原结果
            if(flag) {
                return -ans;
            } else {
                //如果溢出
                if(ans == Integer.MIN_VALUE) return Integer.MAX_VALUE;
                return ans;
            }
        }
    	//倍增除法
        public int div(int x, int y) {
            int res = 1;
            while(x <= (y << 1)){
                if(y <= (Integer.MIN_VALUE >> 1)) break;
                y <<= 1;
                res <<= 1;
            }
            ans += res;
            return x - y;
        }
    }
    
  • 33 搜索旋转排序数组

    image-20210216212928702

    image-20210216212935841

    • 二分查找的本质甚至不是有序,而是每次减半规模,也就是分治。在这种情况下,每次减少一半的问题规模,就是看目标元素是在中间元素的左边还是右边。

    • 具体的来说,是跟边界元素、中间元素和目标元素的大小关系有关。

    • 如果中间元素大于等于左边界元素:

      • 如果左边界元素<=目标元素<中间元素,则目标元素在中间元素的左侧。

      image-20210216230719191

      • 否则,在右侧。
    • 如果中间元素小于左边界元素:

      • 如果中间元素<目标元素<=右边界元素,则目标元素在中间元素的右侧。
      • 否则,在左侧。
    class Solution {
        public int search(int[] nums, int target) {
            int lo = 0, hi = nums.length-1;
            while(lo<=hi) {
                int mid = (lo+hi)/2;
                if(nums[mid]==target) {
                    return mid;
                } else if(nums[mid]>=nums[lo]) {
                    if(target>=nums[lo] && target<nums[mid]) {
                        hi = mid - 1;
                    } else {
                        lo = mid + 1;
                    }
                }  else {
                    if(target>nums[mid] && target<=nums[hi]) {
                        lo = mid + 1;
                    } else {
                        hi = mid - 1;
                    }
                }
            }
            return -1;
        }
    }
    
  • 34 在排序数组中查找元素的第一个和最后一个位置

    image-20210217094319273

    • 首先用一个二分查找找到目标元素,然后再用两个二分查找搜索左右边界。
    • 对于返回值,二分搜索最后一定是hi在lo左侧终止,即hi+1=lo。
      • findleft,lo的含义是小于target的都在lo左侧,而大于等于target的都在hi右侧,因此左边界是lo。
      • findright,小于等于target的都在lo左侧,大于hi的都在hi右侧,因此返回hi。
    class Solution {
    
        int left = -1;
        int right = -1;
    
        public int[] searchRange(int[] nums, int target) {
            int lo = 0, hi = nums.length-1;
            while(lo<=hi) {
                int mid = (lo+hi)/2;
                if(nums[mid]==target) {
                    findleft(nums, target, lo, hi);
                    findright(nums, target, lo, hi);
                    break;
                } else if(nums[mid]>target) {
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            } 
            return new int[]{left,right};
        }
    
        public void findleft(int[] nums, int target, int lo, int hi) {
            while(lo<=hi) {
                int mid = (lo+hi)/2;
                if(nums[mid]>=target) {
                    hi = mid - 1;
                } else if(nums[mid]<target) {
                    lo = mid + 1;
                }
            }
            left = lo;
        }
    
        public void findright(int[] nums, int target, int lo, int hi) {
            while(lo<=hi) {
                int mid = (lo+hi)/2;
                if(nums[mid]<=target) {
                    lo = mid + 1;
                } else if(nums[mid]>target) {
                    hi = mid - 1;
                }
            }
            right = hi;
        }
    }
    
  • 35 搜索插入位置

    image-20210217100222269

    • 二分查找,重要的是找不到的情况下的返回值。初始值hi设置为数组长度,避免了单独处理大于数组中所有数的问题。
    • 最后当区间长度为1时,如果目标元素小于则就在mid处插入,否则在mid+1处。
    class Solution {
        public int searchInsert(int[] nums, int target) {
            int lo = 0, hi = nums.length-1;
            while(lo<=hi) {
                int mid = (lo+hi)/2;
                if(nums[mid]==target) {
                    return mid;
                } else if(nums[mid]>target) {
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            }
            return lo;
        }
    }
    
  • 69 x的平方根

    image-20210217100612213

    • 二分查找[0,x]区间内的值。坑的点主要有2:
      • 输入的最大值为int的上限,此时mid*mid可能会溢出,需要转换为long。
      • x的平方根可能为x自己,所以搜索应包含x。如果以lo<hi为终止条件,即区间右边界为不包含,起始值hi应为x+1,可能会溢出。所以需要以lo<=hi为终止条件,起始值为x。
    • 因为去掉小数部分,即平方小于x,返回值为lo-1,即小于平方根的最大整数。
    class Solution {
        public int mySqrt(int x) {
            int lo=0, hi = x;
            while(lo<=hi) {
                int mid = lo + (hi-lo)/2;
                if((long)mid*mid==x) {
                    return mid;
                }
                if(x<(long)mid*mid) {
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            }
            return lo-1;
        }
    }
    
  • 50 Pow(x,n)

    image-20210217102750752

    • 本题的问题仍然是负数最大值大于正数,所以转换时要改为long。
    class Solution {
        public double myPow(double x, int n) {
            boolean isNeg = false;
            long N = (long)n;
            if(N<0) {
                isNeg = true;
                N = -N;
            }
            double ans = 1;
            while(N>0) {
                if((N&1)==1) {
                    ans *= x;
                }
                N >>= 1;
                x *= x;
            }
            if(isNeg) {
                return 1/ans;
            } else {
                return ans;
            }
        }
    }
    
  • 74 搜索二维矩阵

    image-20210217110408182

    image-20210217110417828

    • 先用一个二分搜索找出所在的行,即小于等于目标元素的最大值,返回lo-1/hi。
    • 这里需要校验lo-1的合法性,因为可能目标元素小于第一行的最小值,这样就直接返回false了。
    • 然后再用一个普通的二分查找找出所在的列。
    class Solution {
        public boolean searchMatrix(int[][] matrix, int target) {
            int mlo=0, mhi=matrix.length-1;
            while(mlo<=mhi) {
                int mmid = (mlo+mhi)/2;
                if(target<matrix[mmid][0]) {
                    mhi = mmid - 1;
                } else {
                    mlo = mmid + 1;
                }
            }
            int index = mhi;
            if(index<0) return false;
            int lo=0, hi=matrix[index].length-1;
            while(lo<=hi){
                int mid = (lo+hi)/2;
                if(target==matrix[index][mid]) {
                    return true;
                }
                if(target<matrix[index][mid]) {
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            }
            return false;
        }
    }
    
  • 81 搜索旋转排序数组Ⅱ

    image-20210217114750782

    • 与之前的旋转数组不同的地方主要在于,可能会出现重复元素。
    • 出现重复元素,可能会导致无法根据左边界和 mid的大小关系判断旋转点在mid的哪一边(比如数组[1,0,1,1,1]),需要逐个缩减边界。
    • 需要注意的是target和左边界或者右边界比较时,需要加=号。
    class Solution {
        public boolean search(int[] nums, int target) {
            int lo=0, hi = nums.length-1;
            while(lo<=hi) {
                int mid = (lo+hi)/2;
                if(nums[mid]==target) {
                    return true;
                }
                if(nums[mid]==nums[lo]) {
                    lo++;
                    continue;
                }
                if(nums[mid]<nums[lo]) {
                    if(target>nums[mid] && target<=nums[hi]) {
                        lo = mid + 1;
                    } else {
                        hi = mid - 1;
                    }
                } else {
                    if(target<nums[mid] && target>=nums[lo]) {
                        hi = mid - 1;
                    } else {
                        lo = mid + 1;
                    }
                }
            }
            return false;
        }
    }
    
  • 153 寻找旋转排序数组中的最小值

    image-20210217174148705

    • 首先看数组是不是有序的(通过比较左边界和右边界的大小),如果是有序的,就返回左边界。
    • 否则,看mid与左边界(nums[mid]>=nums[lo])或右边界(nums[mid]<nums[hi])的大小关系,减少一半规模。
class Solution {
    public int findMin(int[] nums) {
        int lo = 0, hi = nums.length-1;
        while(lo<=hi) {
            if(nums[lo]>nums[hi]) {
                int mid = (lo+hi)/2;
                if(nums[mid]>=nums[lo]) {
                    lo = mid+1;
                } else {
                    hi = mid;
                }
            } else {
                return nums[lo];
            }
        }
        return nums[lo];
    }
}

iwehdio的博客园:https://www.cnblogs.com/iwehdio/
posted @ 2021-02-18 17:21  iwehdio  阅读(213)  评论(0编辑  收藏  举报