二分查找


  1. 二分查找的也称为折半查找,由于每次都能够将查找区间缩小为原来一半,这种算法的时间复杂度为O(logN)。
    
  2. 计算中值的方法有两种
    

    m = (low+ hight) / 2 m = low + (hight - low) / 2 推荐使用第二种,因为减法不会涉及到数据因为相加导致的溢出问题。

  3. 查找的返回值一般返回为low,但是实际情况需要从实际的题目出发。
    
  4. 可以多举例子,便于理解边界问题。
    

Question 1

x 的平方根(https://leetcode-cn.com/problems/sqrtx/)(长按复制到浏览器即可到leetcode对应的题目)

算法的逻辑在具体的代码注释里

/**
 * 计算并返回 x 的平方根,其中 x 是非负整数。
 * 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
 */
public class Question1 {
    public int mySqrt(int x) {
        if (x <= 1) {
            return x;
        }
//        二分
        int low = 1;
        int hight = x;
//        结合双指针
        while (low <= hight) {
//            定义中间值  这样定义middle不会出现溢出问题
            int middle = low + (hight - low) / 2;
//            计算是否能够直接算出开方
            int sqrt = x / middle;
            if (sqrt == middle) {
                return middle;
            } else if (middle > sqrt) {
//                大了 调整hight
                hight = middle - 1;
            } else {
//                小了 调整low
                low = middle + 1;
            }
        }
        return hight;
    }
}

Question 2

寻找比目标字母大的最小字母(https://leetcode-cn.com/problems/find-smallest-letter-greater-than-target/)

/**
 * 给定一个只包含小写字母的有序数组letters 和一个目标字母 target,寻找有序数组里面比目标字母大的最小字母。
 * 数组里字母的顺序是循环的。举个例子,如果目标字母target = 'z' 并且有序数组为 letters = ['a', 'b'],则答案返回 'a'。
 * 关键词:有序、循环
 */
public class Question2 {
    public char nextGreatestLetter(char[] letters, char target) {
//        定义头尾
        int hight = letters.length-1;
        int low = 0;
        while (hight>=low){
            int middle = low + (hight-low)/2;
            if(target>=letters[middle]){
//                middle小了
                low = middle+1;
            }else {
//                middle大了
                hight = middle-1;
            }
        }
//        最终得到的low是接近结果的(因为只有low大于hight的时候才会跳出循环)
        return low<letters.length?letters[low]:letters[0];
    }
}

Question3

** 有序数组中的单一元素(https://leetcode-cn.com/problems/single-element-in-a-sorted-array/)**

/**
 * 给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
 * 注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
 * <p>
 * 因为要求时间复杂度和空间复杂度,所以不能够使用直接遍历的方法。
 */
public class Question3 {
    public int singleNonDuplicate(int[] nums) {
        int low = 0;
        int hight = nums.length - 1;
        while (hight > low) {
            int middle = low + (hight - low) / 2;
//            判断中间的数是偶数还是奇数
            if (middle % 2 == 0) {
//                偶数  说明middle前面有偶数个数字 此时判断middle 和 middle+1的关系
                if (nums[middle] == nums[middle + 1]) {
//                    说明middle前面数字都是出现两次的
                    low = middle + 2;
                } else {
//                    说明middle后面的数字都是正确的
                    hight = middle;
                }
            } else {
//                奇数   说明前后都只有奇数个数字  判断middle 和middle+1的关系
                if (nums[middle] == nums[middle + 1]) {
//                   说明middle之后的都是正确的
                    hight = middle - 1;
                } else {
//                    说明middle+1之后的是有问题的
                    low = middle + 1;
                }
            }
        }
        return nums[low];
    }
}

Question4

第一个错误的版本(https://leetcode-cn.com/problems/first-bad-version/)

/**
 * 你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。
 * 由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
 * <p>
 * 假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
 * <p>
 * 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。
 * 实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
 */
public class Question4 {
    public int firstBadVersion(int n) {
        int low = 1;
        int hight = n;
        while (hight > low) {
            int middle = low + (hight - low) / 2;
//            二分查找 提高效率
            if(isBadVersion(middle)){
//                版本出现错误 可能是在之前就出错了,所以需要往前回溯
                hight = middle;
            }else {
//                版本正确 往后推进
                low = middle+1;
            }
        }
        return low;
    }

    /**
     * leetcode系统提供的方法
     *
     * @param version
     * @return true:错误版本
     * false:正确版本
     */
//    boolean isBadVersion(int version) {
//        return false;
//   }
}

Question 5

寻找旋转排序数组中的最小值(https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/)

/**
 * 旋转数组的最小数字问题
 * 假设按照升序排序的数组在预先未知的某个点上进行了旋转。
 * ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
 * 请找出其中最小的元素。
 * 你可以假设数组中不存在重复元素。
 */
public class Question5 {
    public int findMin(int[] nums) {
        int low = 0;
        int hight = nums.length - 1;
        while (hight > low) {
            int middle = low + (hight - low) / 2;
//            旋转数组的特征就是:发生旋转的话,就会有一个旋转点
//            我们的目标就是找到这个旋转点
//            (通过二分法比对middle和hight的值)
//            hight>middle的话 说明后半部分没哟旋转点 反之同理!
            if(nums[middle]<nums[hight]){
//                说明后半部分是正常的顺序的数组,没有旋转点,将目标缩小到前半部分
                hight = middle;
            }else {
//                说明后半部分数组有旋转点,继续调查后半部分的数组
                low = middle+1;
            }
        }
        return nums[low];
    }

Question 6

**在排序数组中查找元素的第一个和最后一个位置(https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/) **

/**
 * 题目描述:
 * 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
 * 你的算法时间复杂度必须是 O(log n) 级别。
 * 如果数组中不存在目标值,返回 [-1, -1]。
 * <p>
 * 关键词:升序数组、时间复杂度要求O(log n) 所以需要使用二分查找算法
 */
public class Question6 {
    public int[] searchRange(int[] nums, int target) {
//        寻找第一次出现target的地方
        int first = binarySearch(nums, target);
//        寻找第一次出现比target大1的数字的地方
        int last = binarySearch(nums, target + 1) - 1;
        if (first == nums.length || nums[first] != target) {
            return new int[]{-1, -1};
        } else {
            return new int[]{first, Math.max(first, last)};
        }
    }

    /**
     * 封装成方法 用于寻找target第一次出现的位置
     * @param nums
     * @param target
     * @return
     */
    private int binarySearch(int[] nums, int target) {
        int low = 0;
        int hight = nums.length;
        while (hight > low) {
            int middle = low + (hight - low) / 2;
            if (nums[middle] >= target) {
//                说明目标在上半部分
                hight = middle;
            } else {
//                说明目标在下半部分
                low = middle + 1;
            }
        }
        return low;
    }
}