算法刷题笔记之二分法小结

  • Post author:
  • Post category:其他


近日刷了很多二分法相关的题目,想以一文简单总结一下



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 …



    2

    n

    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;
    }



版权声明:本文为fat_cai_niao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。