二分查找及其拓展

二分查找,也叫"折半查找",是一个很常见的算法。其原理思想或多或少都了解。我还记得上大学的时候讲数据结构的老师说过:使用二分查找的一个重要的先决条件是被查找的数组必须是有序的。这句话放到当时的语境来讲是正确的。但是,随着工作中逐渐遇到各种各样的查找算法,到头来发现都是二分查找的思想,有些数组不一定就有序,有些也有可能都不是单纯的数组。但是都可以从中提炼出二分查找的思想来。因此,我觉得有必要归纳总结一下这一类的问题。
二分查找的思想虽然简单,但是要想写好一个bug free的算法来还是不容易,这里面是有一些坑的。我们先来看一个标准的二分查找算法:
public static int binarySearch(int[] array, int key) {
    int low = 0;
    int high = array.length;
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (array[mid] == key) {
            return mid;
        }

        if (array[mid] < key) {
            low = mid + 1;
        } else {
            high = mid;
        }
    }
    return -1;
}
首先,我们都认为array和key都是合法的,这里只专注讨论二分查找相关问题。有几个地方值得注意:
1:跳出循环的条件到底是< 还是 <=
2:mid的计算到底是(low+high)/2 还是low+(high-low)/2
3:low 和high的计算到底要不要+1或-1

第二点当然写成low+(high-low)/2要好点,避免整数的溢出。第一点和第三点可以归结为一个问题——就是每次查找完成后,在排除掉一半"元素0"的时候,有没有排除彻底,没有排除彻底,可能就会产生无限循环的死结。
我们再来看看二分查找有哪些拓展:
一、在旋转有序数组中查找
给定一个有序数组,例如0,1,2,3,4,5,6,7;然后按照某个位置旋转,例如按照第三个位置旋转后变成:3,4,5,6,7,0,1,2;按照第0个位置旋转后不变;按照第7个位置旋转后编程7,0,1,2,3,4,5,6。要求给一个旋转后的有序数组(并没有说明从第几个位置旋转)和一个待查找的元素key,返回key在该数组中的位置,如果没有返回-1。
分析:看到"查找"、"有序"等字眼,应该有使用"二分查找"的敏感性。问题是这不是一个标准的有序数组,那我们应该怎么办?二分查找的核心是给定一个"集合",把集合一分为二,你能够通过某种条件判断所要查找的目标在哪一个子集里面,然后排除另一个子集。
这个问题可以理解成两个分段的有序数组,且第一段的最小值都大于等于第二段的最大值。因此,我们可以先找到数组的"分界点"在哪里,然后如果key<=array[n-1](即数组最后一个数),那key就在第二段里,否则在第一段里。这时候再用标准的二分查找在第一段或第二段里查找即可:
代码如下:
public int search(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }

        int gap = findGap(nums);
        int low, mid, high;
        if (gap == -1) {
            low = 0;
            high = nums.length;
        } else {
            if (target <= nums[nums.length - 1]) {
                low = gap + 1;
                high = nums.length;
            } else {
                low = 0;
                high = gap + 1;
            }
        }

        while (low < high) {
            mid = low + (high - low) / 2;
            if(nums[mid] == target){
                return mid;
            }else if(nums[mid] > target){
                high = mid;
            }else{
                low = mid+1;
            }
        }

        return -1;
    }

// 找到分界点
    private int findGap(int[] nums) {
        int low = 0, mid, high = nums.length;
        while (low < high) {
            mid = low + (high - low) / 2;
            if ((mid + 1) < nums.length && nums[mid] > nums[mid + 1]) {
                return mid;
            }
            if (nums[mid] > nums[nums.length - 1]) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return -1;
    }

二、寻找插入位置
给定一个有序数组和一个元素key,找到一个位置插入key,使得插入后的数组依然有序。
例如:
[1,3,5,6] , 5 → 2
[1,3,5,6] , 2 → 1
[1,3,5,6] , 7 → 4
[1,3,5,6] , 0 → 0

分析:典型的二分查找算法,只是把跳出循环的条件:key == nums[m]变成了nums[m-1]<= key <=nums[m]。即要插入的位置是key的值大于等于前一个数且小于等于后一个数。代码如下(注意边界条件):
public int searchInsert(int[] nums, int target) {
        int low = 0;
        int heigh = nums.length;
        while (low < heigh) {
            int mid = low + (heigh - low) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] > target) {
                if (mid == 0 || nums[mid - 1] < target) {
                    return mid == 0 ? 0 : mid;
                } else {
                    heigh = mid;
                }
            } else {
                if (mid == nums.length - 1 || nums[mid + 1] > target) {
                    return mid == nums.length - 1 ? nums.length : mid + 1;
                }else{
                    low = mid+1;
                }
            }
        }
        return -1;//理论上不会走到这里来
    }

三、找峰值(极大值)问题
给定一个数组,返回该数组峰值所在位置。如果nums[i]是峰值,那么nums[i]>nums[i-1] && nums[i]>nums[i+1]。可以假定nums[0] = nums[n] = -∞。该数组可能存在多个峰值,返回任意一个即可。

分析:把数组按照(i,nums[i])放到坐标轴上,把相临点直线连接,可以得出一个类似的"心电图",目标就是找到心电图某个波峰所在位置。那么怎么用二分查找解决这个问题呢?想象你行走在这个心电图上。如果你在往上爬,那么你的前面一定存在一个峰值,如果你在走下坡,那么你后面一定存在一个峰值。二分查找的思想就可以用上了:每次排除你前面或后面的"一半",直到找到峰值。
代码如下:
public int findPeakElement(int[] nums) {
         if (nums == null || nums.length == 0) {
            return -1;
        }
        int low = 0, mid, high = nums.length;
        while (low < high) {
            mid = low + (high - low) / 2;
            boolean biggerThanPre = mid - 1 < 0 ? true : nums[mid] > nums[mid - 1];
            boolean biggerThanRear = mid + 1 >= nums.length ? true : nums[mid] > nums[mid + 1];
            if (biggerThanPre && biggerThanRear) {
                return mid;
            } else if (!biggerThanRear) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return -1;
    }
四、寻找有序数组里的单一值
给定一个有序数组,除了一个元素只出现了一次外,其他元素都出现了两次。请找出这个只出现过一次的元素。例如:1,1,2,3,3,4,4,8,8。2即为要找的值。
分析:这里又出现了"有序"和"查找",因此想到能不能使用二分查找。仔细分析不难发现,数组的个数一定为奇数。那么一定能找到中点,使得中点前后的数相等。记中点为mid,存在三种情况:
1、nums[mid] != nums[mid -1] && nums[mid] != nums[mid +1] ,返回结果
2、nums[mid] == nums[mid-1],数组可以分为两部分:[low,mid-2]和[mid+1,high],目标元素位于长度为奇数的子数组中
3、nums[mid] == nums[mid+1],数组可以分为两部分:[low,mid -1]和[mid+2,high],目标元素位于长度为奇数的子数组中
注意判断数组越界,low 和 high分别是二分查找中指向低位和高位的指针。
拓展:如果给定的数组不保证有序,又应该使用什么算法?
五、在二维数组中寻找目标元素
给定一个二维数组,每一行从左到右排序并且每一行的第一个元素比其前一行的最后一个元素都大。给定一个目标元素,判断该元素是否在该数组中。
分析:这是一道将二分查找拓展到二维数组的典型题目。思想和在一维数组中查找类似,首先通过二分查找定位到在哪一行,然后在该行中再次使用二分查找定位到某一列。问题拆解为两个小问题:1)找到目标元素所在行 2)从该行中判断目标元素是否在该行中
代码如下:
public  boolean searchMatrix(int[][] matrix, int target) {
    if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
        return false;
    }
    //定位在哪一行
    int[] firstRow = new int[matrix.length];
    for(int i = 0;i<firstRow.length;i++){
        firstRow[i] = matrix[i][0];
    }

    int targetRow = findTargetRow(firstRow,target);
    if(targetRow == -1){
        return false;
    }
    return isFoundTarget(matrix[targetRow],target);
}

private  int findTargetRow(int nums[],int target){
    int low =  0;
    int high = nums.length;
    while (low < high){
        int mid = low + (high - low) /2;
        boolean isTargetLocation = mid < nums.length -1 && target>=nums[mid] && target <nums[mid+1] || mid == nums
                .length -1 && target >= nums[mid];

        if(isTargetLocation){
            return mid;
        }

        if(target > nums[mid]){
            low = mid +1;
        }else {
            high = mid;
        }
    }
    return -1;
}
private  boolean isFoundTarget(int nums[],int target){
    int low = 0;
    int high = nums.length;
    while (low < high){
        int mid = low + (high - low) /2;
        if(nums[mid] == target){
            return true;
        }
        if(nums[mid] > target){
            high = mid;
        }else{
            low = mid +1;
        }
    }
    return false;
}
六、二分查找在几何中的应用
有一个n个点组成的凸多边形,和一个点A,判断该点是否严格在多边形内部
分析:这是一道二分查找在几何中应用的典型例子。我们考虑将凸多边形划分成N个三角区域
如图:

则点在某个三角形内必然在凸多边形内。问题转化为判断点是否在某个三角形内。
首先确定点是否在某两个向量之间,然后确定在某个边(即图中绿色向量)左边还是右边即可。
第一步,判断是否在两个向量之间,变可以用二分查找完成。每次选择中间向量,判断点在该向量左边还是右边,然后排除另一半即可。

以上是一些常见且典型的二分查找思想的应用。当然,还有很多其他的例子,只要能在每次查找的过程中能够"排除"一半元素,均可以大胆使用二分查找。


  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值