近日刷了很多二分法相关的题目,想以一文简单总结一下
     文章目录
    
- 
      
 1.二分查找经典框架
 
- 
      
 2.题型一:给定一个特定的target然后在数组中寻找
 
- 
      
 3.题型二:给定一个限定的值(最大,最小)而且无法直接在数组中找到,只能通过大致判断在哪个区间,然后缩小查找范围
 
- 
      
 4.题型三:答案二分
 
    
    
    1.二分查找经典框架
   
- 1.相邻就退出 start + 1<end
- 2.求中点要防溢出
- 3.三种情况分开写
- 4.双重检查更保险
 public static int binarySearch(int[] nums, int target) {
        // write your code here
        if (nums == null || nums.length == 0){
            return -1;
        }
        int start = 0;
        int end = nums.length - 1;
        //要素1.相邻就退出 start + 1<end
        while (start + 1<end){
            // mid = (start +  end) /2 这种写法在大多数情况下是对的
            // 但是如果 start = 2^31 -1 end = 2^32 - 1 明显这会发生一个整数溢出
            // 要素2.求中点要防溢出
            int mid = (end - start)/2 + start;
            // 要素3.三种情况分开写
            if (nums[mid] == target){
               return mid; //这里的写法要根据题目来 如果是找到并返回, 则此处可以直接返回
               // end = mid; //如果是找满足target的第一个位置 则这里继续往前面找
               //start = mid; //如果是找满足target的最后一个位置 则此处继续往后找
            }else if(target < nums[mid]){
                //在左区间
                end = mid;
            }else {//在右区间
                start = mid;
            }
        }
        // 要素4.双重检查更保险
        if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
        return -1;
    }
    
    
    2.题型一:给定一个特定的target然后在数组中寻找
   
    
    
    2.1 First/Last Position
   
    
    
    2.1.1First Position(lintcode14)
   
给定一排序数组,其中元素可能重复,给定一目标数,寻找该目标数在数组中首次出现的位置
First Position
- 非常清晰的二分法的应用,寻找最开始的,所以往最前面找
- 
     关键点
if (nums[mid] == target){ //寻找最开始的位置 所以往前找 end = mid; ........... //因为找最开始的位置,所以先检查前面的位置 if (nums[start] == target){ return start; } if (nums[end] == target){ return end; }
- 完整代码
   */
    public static int binarySearch02(int[] nums, int target) {
        if (nums == null || nums.length == 0){
            return -1;
        }
        int start = 0;
        int end = nums.length - 1;
        //1.相邻就退出
        while (start+1<end){
            //2.求中点 放溢出
            int mid = (end - start)/2 + start;
            //3.三种情况分别判断
            if (nums[mid] == target){
                //寻找最开始的位置 所以往前找
                end = mid;
            }else if (target<nums[mid]){
                end = mid;
            }else {
                start = mid;
            }
        }
        //4.二次判断更保险
        if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
        return -1;
    }
    
    
    2.1.2First Bad Version(lintcode74)
   
寻找第一个坏的版本,思路和2.1几乎完全一样,,都是寻找第一个满足条件的。需要注意的点也都一样
第一个坏的版本
- 完整代码
  public int findFirstBadVersion(int n) {
        // write your code here
        //可利用二分查找来解决
        //如果每次查找 中点是坏的 则往前找
        //如果每次查找 中点是好的 则往后找
        //这里要注意这里不是数组下标
        int start = 1;
        int end = n;
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            //如果中点对应版本是坏的 返回true 则继续往前半部分找
            if (SVNRepo.isBadVersion(mid)){
                end = mid;
            }else {
                //如果中点对应版本是好的 返回false 则继续往后半部分找
                start = mid;
            }
        }
        //二次判断
        //从while中退出时 start 和 end一定是相邻的
        //那么显然这两个之中 一定有一个是坏的 但是题目求最近的那一个
        //所以先验证start
        //如果不行 则一定是end
        if (SVNRepo.isBadVersion(start)){
            return start;
        }
        return end;
    }
    
    
    2.2 Last Position(lintcode458)
   
和First Position刚好相反,寻找满足添加的最后一个数。那关键的地方也正好相反。
Last Position
- 
     首先要注意,找到满足添加的点后继续往后半部分找
int mid = (end - start)/2 + start; if (target == nums[mid]){ //由于这里是寻找 最后一个等于target的位置, 显然应该尽可能的往后面找(这里和First position的区别) //所以如果当前的mid = target的话 则继续往后寻找 start = mid; }
- 
     其次就是最后判断的时候先判断end
//因为是找最后一个 所以先判断后者 if (nums[end] == target){ return end; } if (nums[start] == target){ //start 和 end一定相邻,如果其中一个不等于目标 则最后的一个一定是start return start; } return -1;
- 
     完整代码
public static int lastPosition(int[] nums, int target) { // write your code here if (nums == null || nums.length == 0){ return -1; } int start = 0; int end = nums.length -1; while (start +1 < end){ int mid = (end - start)/2 + start; if (target == nums[mid]){ //由于这里是寻找 最后一个等于target的位置, 显然应该尽可能的往后面找(这里和First position的区别) //所以如果当前的mid = target的话 则继续往后寻找 start = mid; }else if (target < nums[mid]){ end = mid; }else { start = mid; } } //因为是找最后一个 所以先判断后者 if (nums[end] == target){ return end; } if (nums[start] == target){ //start 和 end一定相邻,如果其中一个不等于目标 则最后的一个一定是start return start; } return -1; }
    
    
    2.3寻找插入位置(leetcode35)
   
二分法的简单应用,在给定数组中寻找给定的target,如果找到则返回位置,如果没找到则返回该目标数应该插入的位置,插入后数组仍然有序。
寻找插入位置
- 
1.首先两种特殊情况的判断,如果给定数小于数组的最小值和大于数组的最大值,则插入位置是固定的。 //首先判断两种特殊的情况 if(target < nums[0]){ return 0; } if(target > nums[nums.length - 1]){ return nums.length; }
- 
2.如果找到,则显然可以直接返回位置 int mid = (end - start)/2 + start; // 要素3.三种情况分开写 循环内部不返回 if (nums[mid] == target){ return mid; }
- 
3.跳出while继续判断,此时start 和 end一定相邻,先判断start是否与之相等,如果不等,则end也不可能与之相等,则返回应该插入的位置,显然应该是end的所在位置。 if (target == nums[start] ){ return start; } //这里就是处理 在给定数组的中间寻找 找不到的情况 那么target 一定是处于 start 和end之间 //之间的(因为相邻即退出)那么 要返回插入的位置 一定是end return end;
- 
4.完整代码 if (nums == null || nums.length == 0){ return -1; } //首先判断两种特殊的情况 if(target < nums[0]){ return 0; } if(target > nums[nums.length - 1]){ return nums.length; } int start = 0; int end = nums.length - 1; //要素1.相邻就退出 start + 1<end while (start + 1<end){ // mid = (start + end) /2 这种写法在大多数情况下是对的 // 但是如果 start = 2^31 -1 end = 2^32 - 1 明显这会发生一个整数溢出 // 要素2.求中点要防溢出 int mid = (end - start)/2 + start; // 要素3.三种情况分开写 循环内部不返回 if (nums[mid] == target){ return mid; }else if(target < nums[mid]){ //在左区间 end = mid; }else {//在右区间 start = mid; } } // 要素4.双重检查更保险 //下面两种if是能找到的情况 则正常返回 if (target == nums[start] ){ return start; } //这里就是处理 在给定数组的中间寻找 找不到的情况 那么target 一定是处于 start 和end之间 //之间的(因为相邻即退出)那么 要返回插入的位置 一定是end return end; }
    
    
    2.4区间查找(leetcode34)
   
给的数组和目标数,查找目标数在数组中的起始位置。
区间查找
- 
     完全就是
 
 2.1
 
 和
 
 2.3
 
 的一个结合
- 
     完整代码
public int[] searchRange02(int[] nums, int target) { int[] rs = {-1,-1}; if (nums == null || nums.length == 0){ return rs; } rs[0] = findFirst(nums, target); rs[1] = findLast(nums, target); return rs; } /** * 20190802 * 找到满足条件的第一个位置 * @param nums * @param target * @return */ public static int findFirst(int[] nums, int target){ int start = 0; int end = nums.length - 1; while (start +1 <end){ int mid = (end - start)/2 + start; if (target == nums[mid]){ //往前找 end = mid; }else if (target > nums[mid]){ //往后找 start = mid; }else { end = mid; } } if (target == nums[start]){ return start; } if (target == nums[end]){ return end; } return -1; } /** * 20190804 * 找最后一个位置 * @param nums * @param target * @return */ public static int findLast(int[] nums, int target){ int start = 0; int end = nums.length - 1; while (start +1 <end){ int mid = (end - start)/2 + start; if (target == nums[mid]){ //往后找 start = mid; }else if (target > nums[mid]){ //往后找 start = mid; }else { end = mid; } } if (target == nums[end]){ return end; } if (target == nums[start]){ return start; } return -1; }
    
    
    2.5 Search in a Big Sorted Array(lintcode447)
   
给定一个很大的升序正数数组,不能求长度,但是可以通过一个函数返回第k个数的值。要求在log(k)的时间内找到指定target的值
- 不能求长度,二分法的end的初始值如何确定?
- end的值可以不等于整个数组的长度,只要大于target即可
- 那么,只要我能找到一个index,它在数组中对应的值大于target,也就可以确定end的初始值
- 
     所以,关键就在于
 
 如何在log(k)的时间内找到大于target的index
 
- 
     
 倍增法
 
 :一次寻找 1 2 4 8 …
 
 
 
 2n 2^n 
 
 
 
 
 
 
 
 2
 
 
 
 
 
 
 
 
 
 
 n
 
 
 
 
 
 
 
 
 
 
 
 
- 只要确定了end的初始值,后面的则是一个普通的二分查找问题了
- 
     完整代码
/** * 20190804 * 显然,这道题目需要二分法来解决 * 但是 因为不能求长度,而无法直接为 end赋值 * 其实 end的值 不一定是需要整个数组长度的 * nums[end] 只要 > target即可 * 所以关键点 如何在log(k)的时间内 能找到一个nums[end] 则成为了关键 * 这里介绍一种“倍增”的思想 就是找1,找不到就找2 然后找4!! * @param reader: An instance of ArrayReader. * @param target: An integer * @return : An integer which is the index of the target number */ public int searchBigSortedArray(ArrayReader reader, int target){ //1利用倍增法 寻找end的最大值 //即找到第一个 大于target 的 index值即可 int index = 1; while (reader.get(index - 1) < target ){ index = index * 2; } //只要找到了index 下面就是一个普通的二分查找了 int start = 0; int end = index - 1; while (start + 1< end){ int mid = (end - start)/2 + start; if (reader.get(mid) == target){ //因为这里要找 First,所以继续往前找 不能返回 end = mid; }else if (target > reader.get(mid)){ start = mid; }else { end = mid; } } if (reader.get(start) == target){ return start; } if (reader.get(end) == target){ return end; } return -1; }
    
    
    2.6 搜索二维矩阵(leetcode74)
   
给定一二维数组,其中每一行从左到右是升序,而且下一行的第一个数大于上一行的最后一个数,也就是按每一行排列,整个二维数组的值都是升序
- 本质还是二分法的应用,关键在于把二维的数组转换为一维数组,然后进行二分查找
    
    
    2.6.1解法一:先确定目标数所在行,然后在该行进行二分查找
   
   /**
     * 20190805(自解AC)
     * 关键在于如何把2D转换为1D 然后进行二分查找
     * @param matrix 给定矩阵
     * @param target 给定搜索的数
     * @return
     */
    public static boolean searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return false;
        }
        int row = matrix.length;
        int col = matrix[0].length;
        int target_row = 0;
        for (int i=0;i<row; i++){
            //按行 遍历 找到target所在的行
            if (target>=matrix[i][0] && target<=matrix[i][col-1]){
                target_row = i;
            }
        }
        //这里已经把2D转换为了1D直接 利用二分查找即可
        //对target_row 行 进行二分查找
        int start = 0;
        int end = col - 1;
        while (start+1<end){
            int mid = (end - start)/2 + start;
            if (matrix[target_row][mid] == target ){
                return true;
            }else if (matrix[target_row][mid] < target){
                start = mid;
            }else {
                end = mid;
            }
        }
        if (target == matrix[target_row][start]){
            return true;
        }
        if (target == matrix[target_row][end]){
            return true;
        }
        return false;
    }
    
    
    2.6.2解法2
   
- 和解法1没有本质区别
- 
     关键在于一个知识点:
 
 在一个m*n的数组中,第k个数的位置是[k/n][k%n]
 
- 又因为整个数组的数按行排列起来都升序的,因此可以直接在整个数组中进行二分查找
 /**
     * 20190805
     * 参考:https://yisuang1186.gitbooks.io/-shuatibiji/search_a_2d_matrix.html
     * 其实和我的解法 没有本质上的差别,仍然是把2D转换为1D
     * 但是关键的地方在于如和转换 这里的转换方法感觉比我的优雅
     * 给定 m*n的数组 第k个数 在其中的位置为 [k/n][k%n]
     * 知道了这个 就可以查找了
     * @param matrix
     * @param target
     * @return
     */
    public static boolean searchMatrix02(int[][] matrix, int target){
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
            return false;
        }
        int row = matrix.length;
        int col = matrix[0].length;
        int start = 0;
        int end = row * col - 1;
        while (start + 1 < end){
            int mid = (start) + (end - start)/2;
            if (matrix[mid/col][mid%col] == target){
                return true;
            }else if (matrix[mid/col][mid%col] < target){
                start = mid;
            }else {
                end = mid;
            }
        }
        if (matrix[start/col][start%col] == target){
            return true;
        }
        if (matrix[end/col][end%col] == target){
            return true;
        }
        return false;
    }
    
    
    2.7 搜索二维矩阵II(leetcode240,lintcode38)
   
给定二维矩阵,该二维矩阵有如下特性:
每行中的整数从左到右是排序的。
每一列的整数从上到下是排序的。
在每一行或每一列中没有重复的整数。
给定一目标数,要求返回该数在矩阵中的出现次数lintcode。
- 
     此题在两个网站上的描述略有不同,lintcode要求返回出现
 
 target
 
 的次数,leetcode要求返回是否存在该
 
 target
 
 。
- 显然litcode的要求更高一点,因此以lintcode为准
    
    
    2.7.1解法一
   
- 
     因为每一行或者每一列都是有序的,完全可以遍历行或者列,然对每一列或者行进行二分搜索,时间复杂度为
 
 
 
 O( n l o g ( m ) ) \mathcal{O}(nlog(m)) 
 
 
 
 
 
 
 
 O
 
 
 
 (
 
 
 n
 
 
 l
 
 
 o
 
 
 g
 
 
 (
 
 
 m
 
 
 )
 
 
 )
 
 
 
 
 
 或者
 
 
 
 O( m l o g ( n ) ) \mathcal{O}(mlog(n)) 
 
 
 
 
 
 
 
 O
 
 
 
 (
 
 
 m
 
 
 l
 
 
 o
 
 
 g
 
 
 (
 
 
 n
 
 
 )
 
 
 )
 
 
 
 
 
    
    
    2.7.2解法二
   
- 
     要进一步缩小时间复杂度,则考虑线性的复杂度
 
 
 
 O( n ) \mathcal{O}(n) 
 
 
 
 
 
 
 
 O
 
 
 
 (
 
 
 n
 
 
 )
 
 
 
 
 
- 显然从时间复杂度的角度看不能使用二分法了
- 那么只能把该二维数组当作一个整体去考虑
 /**
     * 20190806
     * @param matrix
     * @param target
     * @return
     */
    public int searchMatrix(int[][] matrix, int target) {
        // write your code here
        if (matrix.length == 0 || matrix[0].length == 0){
            return 0;
        }
        int m = matrix.length;//行数
        int n = matrix[0].length;//列数
        //从 最后一行的第一个元素开始搜索
        int x = m-1;
        int y = 0;
        //只要不越界
        int count = 0;
        while (x>=0 && y<n){
            //如果在当前位置 找到这样的一个target
            if (matrix[x][y] == target){
                count++;
                //则继续往右上搜索
                //从上到下增 从左到右增 则[x][y] = target的时候 只可能[x--][y++]的方向可能还有一个值等于target
                x--;
                y++;
                continue;
            }else if (matrix[x][y] < target){
                //小了 则往大的方向搜索
                y++;
            }else {
                //大了 往小的地方搜索
                x--;
            }
        }
        return count;
    }
    
    
    3.题型二:给定一个限定的值(最大,最小)而且无法直接在数组中找到,只能通过大致判断在哪个区间,然后缩小查找范围
   
    
    
    3.1 Find Minimum in Rotated Sorted Array寻找旋转排序数组中的最小值(leetcode153,lintcode159)
   
给定一旋转的排序数组,要求找到其中的最小值。例如:Input: [3,4,5,1,2] Output: 1
- 
     首先一个很直白的思路,就是一个for循环嘛,遍历整个数组,只要找到第一个
 
 nums[i]>nums[i+1]
 
 ,则就是最小值,时间复杂度为
 
 
 
 O( n ) \mathcal{O}(n) 
 
 
 
 
 
 
 
 O
 
 
 
 (
 
 
 n
 
 
 )
 
 
 
 
 
- 
     如果想把一个
 
 
 
 O( n ) \mathcal{O}(n) 
 
 
 
 
 
 
 
 O
 
 
 
 (
 
 
 n
 
 
 )
 
 
 
 
 
 的继续做优化的话,只能到
 
 
 
 O( l o g n ) \mathcal{O}(logn) 
 
 
 
 
 
 
 
 O
 
 
 
 (
 
 
 l
 
 
 o
 
 
 g
 
 
 n
 
 
 )
 
 
 
 
 
 ,而且几乎只能用二分法
- 
     这道题还非常明显的告知了
 
 有序数组
 
 ,所以几乎应该用二分来做了
 
   
- 
     这道题的结构,可以用这个图很清晰的表达出来,显然A点和B点的值都是可以获取的
 
 A = nums[0],B=nums[nums.length-1]
 
- 
     
 C
 
 点的值明显就是最小值,那如何求得C点的值呢?
- 
     二分法中一定会涉及到一个
 
 目标数
 
 ,这里也一样,只是这里要求的是
 
 小于等于目标数的第一个位置上的数
 
 ,取等号是因为可能原数组只旋转一位,则原来最小的第一位跑到了最后一位
- 
     所以这里选B作为目标数,
 
 寻找<=B的First position
 
 即可
- 后面用二分法解即可
- 完整代码
  public static int findMin(int[] nums) {
        if(nums == null || nums.length == 0){
            return -1;
        }
        //定义 target
        int target = nums[nums.length-1];
        //下面的工作就变成寻找 小于target的第一个数
        //当然也是用二分法寻找
        int start = 0;
        int end = nums.length - 1;
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            //如果当前终点位置 的值 小于等于 target
            // 则当前的mid 一定在旋转后数组 的后一段了
            // 因为数组是旋转过的 target一定是 后一段的最大值
            // 当前的mid位置 小于 target则一定是在后半段了(前半段的值一定都大于target)
            // 为了找到小于target的第一个位置 则继续往前找
            if (nums[mid] <= target ){
                end = mid;
            }else {
                //如果 当前中点的值 大于target 则显然小于target的值肯定还在后面
                // 这是因为数组是旋转过的 target一定是 后一段的最大值
                // 当前还有比target大的 则说明当前的位置一定在前一段 所以继续往后找
                start = mid;
            }
        }
        if (nums[start] <= target){
            return nums[start];
        }
        //给定排序数组中的最小值是一定存在的
        return nums[end];
    }
    
    
    3.2Search in Rotated Sorted Array在旋转排序数组中搜索(leetcode33)
   
给定一个旋转的排序数组,和一个目标数
targte
,在旋转排序数组中查找该
targte
,找到返回其位置,未找到返回
-1
。
    
    
    3.2.1解法一
   
- 根据第6题的思路,可以找到旋转排序数组中的最小值
- 很明显,该最小值把旋转排序数组划分为两个有序区间
- 然后在这个两个有序区间内做二分查找即可。
  /**
     *20190805
     * 受今天leetcode153题得启发
     * 可以先找到最小得那个数
     * 然后就可以把数组分成两个有序的部分了
     * 然后对这两个有序的部分进行二分查找即可
     */
    public int search02(int[] nums, int target) {
        if (nums == null || nums.length == 0){
            return -1;
        }
        //先利用153的思路找到旋转点的位置
        //也就是 数组中最小的那个数的位置
        int pivot = 0;
        //数组中的最后一个数
        int lastNumber = nums[nums.length - 1];
        int start = 0;
        int end = nums.length - 1;
        //二分查找寻找pivot
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            if (nums[mid] <= lastNumber){
                end = mid;
            }else {
                start = mid;
            }
        }
        //这里因为找最小 则一定存在 不是start 就是end
        if (nums[start] <= lastNumber){
                pivot = start;
        }else{
            pivot = end;
        }
       //分别在两个区别找
        int rs0 = findPivot(nums,0,pivot,target);
        int rs1 = findPivot(nums,pivot, nums.length - 1, target);
        if (rs0!=-1){
            return rs0;
        }
        if (rs1!=-1){
            return rs1;
        }
        return -1;
    }
    public static int findPivot(int[] nums, int start, int end, int target){
        while (start +1< end){
            int mid = (end - start)/2 + start;
            if (target == nums[mid]){
                return mid;
            }else if (target > nums[mid]){
                start = mid;
            }else {
                end = mid;
            }
        }
        if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
        return -1;
    }
    
    
    3.2.2 解法2
   
- 给定的旋转排序数组一定存在两个有序区间
- 
     二分查找一定有三个位置
 
 start mid end
 
- 
     根据抽屉原理,三个位置,两个区间,一定
 
 至少有两个位置落在同一个有序区间
 
- 
     也就是说可以确定两个位置在一个确定的有序区间内,然后就可以确定
 
 targte
 
 是否落在这个有序区间内,然后就可以进一步缩小
 
 targte
 
 的可能位置
    /**
     * 20190806
     * 另一种基于分类讨论的思想
     * 旋转数组 一定存在两个有序区间,二分法中的 start mid end 一定有两个位置 落在同一个有序区间内(猛然想到这是组合数学里的很重要的抽屉原理)
     * 那显然就分两种情况讨论了
     * nums[mid] < nums[end] 那么显然 mid 和 end 一定在有序区间内 可能在前面 也可能在后面
     * 而 start 的位置是不能确定的
     * start---->mid---->end
     *当mid 和 end 确定在同一个区间了 , 现在就可以确定 target 是否 落在 mid 和 end区间内了
     *  nums[mid] < target <= nums[end] 如果满足这个不等式 则start = mid 即可 进一步缩小targte所在的区间(这也是二分法的精髓
     *  尽可能的缩小目标数所在区间)
     *如果不满足 那target 一定在前面的区间  则为了进一步缩小区间 则 end = mid
     *
     * 上面是一种情况, 那如果 nums[mid] >= nums[end],逻辑是完全一样的
     * 现在 start 和 mid 一定落在同一个有序区间内 那么先确定target是否在这个区间 在 就 end = mid
     * 不在则 start = mid
     * @param nums 给定数组
     * @param target 给定target
     * @return
     */
    public static  int search03(int[] nums, int target){
        if (nums.length == 0){
            return -1;
        }
        int start = 0;
        int end = nums.length - 1;
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            if (nums[mid] == target){
                return mid;
            }
            //如果mid 和 end 在同一个有序区间内
            if (nums[mid] < nums[end]){
                //如果target落在 mid 和 end区间内
                if (nums[mid]<target && target <= nums[end]){
                    //则往后缩小区间
                    start = mid;
                }else {
                    end = mid;
                }
            }else {//如果start 和 mid 落在同一个有序区间内
                //如果 target 落在 start 和 mid 之间
                if (nums[start] <= target && target<nums[mid]){
                    //则往前缩小区间
                    end = mid;
                }else {
                    start = mid;
                }
            }
        }
        if (nums[start] == target){
            return start;
        }
        if (nums[end] == target){
            return end;
        }
        return -1;
    }
    
    
    3.3 Maximum Number in Mountain Sequence山脉序列中的最大值(lintcode585)
   
给定一个整数数组,该数组是一个先增后减的序列,求序列中的最大值
- 
一图胜千言 
 
   
- 
二分法的核心在于不断的缩小目标数所在的范围,一次能够抛弃掉一半的不可能位置,一步步缩小,直到可以用一个判断语句就把结果找出来。 
 /**
     * 20190808
     * @param nums: a mountain sequence which increase firstly and then decrease
     * @return: then mountain top
     */
    public static int mountainSequence(int[] nums) {
        // write your code here
        if (nums.length == 0){
            return -1;
        }
        int start = 0;
        int end = nums.length - 1;
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            if (nums[mid] <= nums[mid + 1]){
                start = mid;
            }else {
                end = mid;
            }
        }
        if (nums[start] > nums[end]){
            return nums[start];
        }else {
            return nums[end];
        }
    }
    
    
    3.4 Find Peak Element寻找峰值(lincode75,leetcode162)
   
给定一个数组,满足一下条件
- 相邻位置的数字是不同的
- A[0] < A[1] 并且 A[n – 2] > A[n – 1]
- 假定P是峰值的位置则满足A[P] > A[P-1]且A[P] > A[P+1],返回数组中任意一个峰值的位置。
- 
此题完全和上一题几乎一样的思路,只是此题的情况多了一种   
- 
 跳出while后,注意两种特殊情况的判断
 if (start == 0){ return start; } if (nums[start-1] < nums[start] && nums[start] > nums[start+1]){ return start; } if (end == nums.length - 1){ return end; } if (nums[end - 1] < nums[end] && nums[end] > nums[end+1]){ return end; }
- 
完整代码 public int findPeakElement(int[] nums) { if (nums.length == 0){ return -1; } if (nums.length ==1){ return 0; } if (nums.length == 2){ if (nums[0]>nums[1]){ return 0; }else { return 1; } } int start = 0; int end = nums.length - 1; while (start +1 < end){ int mid = (end - start)/2 + start; //情况一 mid刚好在peak上 if (nums[mid-1] < nums[mid] && nums[mid] > nums[mid+1] ){ return mid; //情况2 mid 在上坡 顶点肯定在右边 }else if (nums[mid-1] < nums[mid] && nums[mid] < nums[mid+1]){ start = mid; //情况3 mid 在下坡 顶点肯定在左边 }else if (nums[mid-1] > nums[mid] && nums[mid] > nums[mid+1]){ end = mid; }else { //情况4 mid 在谷点 则往两边走都可以 end = mid; } } //说明 start 没有改变 而上述while循环都是往最大的区间跑 说明start就是最大值 //下方的end同理 if (start == 0){ return start; } if (nums[start-1] < nums[start] && nums[start] > nums[start+1]){ return start; } if (end == nums.length - 1){ return end; } if (nums[end - 1] < nums[end] && nums[end] > nums[end+1]){ return end; } return -1; }
    
    
    4.题型三:答案二分
   
此类题不会给出一个明确的数组和目标数,而是要经过分析发现满足某个条件的最大和最小,通过二分可以不断的逼近那个值。
    
    
    4.1Sqrt(x) 求x的平方根(leetcode69)
   
给定一个整数,求其开平方以后向下取整的值
- 样例 2:
输入: 3
输出: 1
样例解释:
返回对x开根号后向下取整的结果。
- 
本质上是求 
 
 小于
 
 x的平方根的
 
 最大整数
 
- 
如果把从 
 
 1
 
 到
 
 x
 
 看做一个数组
 
 [1,2,3,4,...,n]
 
- 
则是求小于x的平方根的 
 
 最后一个整数
 
- 
分析到这里就可以用二分来解决了 
- 
先确定二分的范围 
 
 start = 1,end= x
 
- 
 注意一个细节
 
 ,在做判断的时候不能写成
 
 mid * mid > x
 
 这样会造成溢出
- 
全部代码 public static int mySqrt(int x) { if (x == 0){ return 0; } //确定二分的范围 int start = 1; int end = x; while (start + 1 < end){ int mid = (end - start)/2 + start; //如果当前中点 大于x的平方根 显然继续往前找 if (mid > Math.sqrt(x)){ end = mid; }else { //小于等于 x的平方根 则往后找,因为我们要找最大的那个数 start = mid; } } if (end <= Math.sqrt(x)){ return end; } return start; }
    
    
    4.2Wood cut 木材加工(lintcode183)
   
给定一个数组,数组的每一个元素值代表木材的长度;给定一个
k
,要求将每一根木材进行切分,切分出来的小木材的数量至少为
k
,而且每根小木材的长度要
最大
。
- 木材长度为整数
- 如果无法切分成k个,则返回0
L = [232, 124, 456]
k = 7
Output: 114
- 
首先要明白求的答案是什么 
- 
求的是木材切分的 
 
 长度
 
 ,这个长度要满足两个条件- 
       
 按照这个长度切,切分出来的木材数量至少为k(>=k)
 
- 
       
 显然满足条件1的长度有很多,我们要求所有满足条件1的长度里最大的一个,换个角度看,就是这个长度+1就不满足条件1了
 
 
- 
       
- 
明确了要求的答案是什么,接下来要明确 
 
 答案可能存在的范围
 
- 
 start=1
 
 ,也就是按长度为1来切分,这样显然会大于k
- 
 end = max(L)
 
 ,按数组元素的最大值来切,则至少能切出k=1
- 
接下来就可以不断的用二分法来缩小答案所在的区间 
- 
如果按当前长度mid来切,切出来的数量大于等于 
 
 k
 
 ,说明mid太小了,数量多了,则增大长度mid
- 
如果按当前长度mid来切,切出来的数量小于k,说明mid大了,数量少了,则减少长度k 
- 
代码: public int woodCut(int[] L, int k) { if (L.length== 0){ return 0; } // write your code here //排序是为了方便后面求最大值 Arrays.sort(L); //确定答案可能存在的区间 //最小值 按长度为1 来切 则一定满足条件 int start = 1; // 最大值按max(L)来切 这样至少满足k=1; int end = L[L.length-1]; while (start + 1 < end){ int mid = (end - start)/2 + start; if (count(L, mid)>=k){ //如果按长度mid 来切分出来的数量 大于等于K //说明 mid 太小了 切分出来的太多了 所以需要增大mid start = mid; }else { //如果按 长度mid切分出来的数量 小于等于k // 说明mid 有点大了 end = mid; } } //因为求最大的长度 所以先验证end if (count(L, end) >= k){ return end; } if (count(L, start) >= k){ return start; } return 0; } public static int count(int[] L, int length){ int sum = 0; for (int x:L){ sum += (x/length); } return sum; }
    
    
    4.3Copy Books(lintcode437)
   
给定一个数组,数组的长度代表书的数量,数组的元素代表每一本书的页数。给定一个数k,代表有k个人来复印这些书,复印一本书的时间就等于这本书的页数,一个人可以复印多本书,但是这多本书必须是连续的。k个人同时开始复印这些书,要求的是复印完所有的书的最少时间。
pages = [3, 2, 4], k = 2
5
题意描述得有点复杂了,拿一个具体实例来说,三本书,每本的页数如数组所示,两个人来复印,问复印完所需要的最少的时间。
两个人来复印三本书,显然有两种分法 [3,2][4] 和 [3] [2,4]
按前面的分法 至少只需要5分钟 而后面至少需要6分钟
所以就是 k个人同时来分n本书,有很多种不同的分法,需要从这不同中分法中找出
所花时间最少的
分法,让k个人能完成n本书的复制
- 
因为每本书复制所花的时间就是页数,所以上题可以进一步解释为 
 
 给定含有n个元素的数组,和一个整数k,将数组分成k份,
 
 ,显然分法有很多种,假设有m种,每一种分法里都有一个子数组最大值,我们要在这
 
 m个最大值里求一个最小值
 
- 
注意我们要求的是时间,满足两个条件 - 这个时间保证k个人都能把书复印完
- 这个时间是在所有可能的时间组合里最小的
 
- 
同样先确定范围 
- 
这个时间最小 肯定就是 
 
 max(array)
 
 ,按这个时间来分 可以保证
 
 k = len(array)
 
 个人复印完所有的书
- 
最大是 
 
 sum(array)
 
 , 按这个时间份,可以办证
 
 k =1
 
 个人复印完所有的书
- 
显然 时间减少, 则需要更多的人来复印 
- 
时间增多 则需要更少的人来复印 
 public int copyBooks(int[] pages, int k) {
        // write your code here
        int max = 0;
        int sum = 0;
        for (int x :pages){
            sum += x;
            if (x > max){
                max = x;
            }
        }
        int start = max;
        int end = sum;
        while (start + 1 < end){
            int mid = (end - start)/2 + start;
            if (numberOfWorkers(pages, mid)>k){
                //如果按照当前的时间分配 所需工人大于 k
                //说明时间少了 则需要增加时间
                start = mid;
            }else {
                //如果按照当前的时间分配 所需工人小于等于 k
                //因为要求的时间是最少的 所以继续往前面找 看有不有更少的时间能满足
                end = mid;
            }
        }
        if (numberOfWorkers(pages, start) <= k){
            return start;
        }
        return end;
    }
    /**
     * 20190809
     * 要理解这段程序 他的意义是求 在给定的时间下 求最少的人数来完成 n本书籍的复制
     * @param pages
     * @param t
     * @return
     */
    public  static  int numberOfWorkers(int[] pages, int t) {
        int count = 0;
        int sum = 0;
        for (int i = 0; i < pages.length; i++) {
            // 如果说 当前书籍所需要花的时间 和前面累加起来的时间  在时间t内 已经不能搞定了 则需要分配一个人手了
            // 则从 j --- i-1 的书籍 都交给这个人来复印
            // 而且同时 需要从当前这个i 重新开始累积了
            if (sum + pages[i] > t) {
                sum = pages[i];
                count++;
            } else {
                //如果从 j 到 i 书籍的复印时间都 <= t  那说明 还不需要增加一个人手
                //则继续累积时间
                //这里的逻辑是因为要求的人数是最少的
                //也就是说 我需要在给定的时间t内 尽可能的完成多的书籍的复制
                sum += pages[i];
            }
        }
        //这里需要+1的原因在于 如果前面刚好累积到最后一个 i=n-1 此时跳出循环了 但是从 j 到 n-1还没有分配人来复制
        //所以这里需要一个人来复制
        count++;
        return count;
    }
 
