详解 二分查找法 Binary Search 和 变种 floor和ceil

目录

一、二分查找法 Binary Search

二、二分查找法的变种 floor和ceil

三、总结


一、二分查找法 Binary Search

二分查找法是一种非常常用的查找方式,但是二分查找法有一定的限制条件,只能在一个有序的数列中,才能使用二分查找法(排序的作用,排序在很多时候是作为其他算法的子过程,处理有序数组比处理无序数组容易很多)。那么二分查找法怎么操作呢?

比如说有一个排好序的数组,现在要查找某一个元素。先来比较数组中间元素v的大小和需要查找元素的大小。如果中间元素v等于需要查找的元素,直接就找到了这个元素。否者的话整个数组被分成了三部分,<v部分、>v部分和=v部分。

 

如果我们要查找的元素小余元素v,则在<v部分继续查找。如果我们要查找的元素大余元素v,则在>v部分继续查找。

我们可以想象整个查找过程又构成了一颗二叉树,在下一次某部分的时候,又一分为二来比较,如果找到了就直接退出,否者的话就在两部分中继续查找下去。整个二分查找法的时间复杂度是O(logn)级别的。

二分查找法的思想是非常简单的,是在1946年提出的。不过有意思的是这样的一个二分查找法实现出来并不容易,声称据第一个没有bug的二分查找法在1962年才出现。

下面来看一下具体的代码实现,看看这个听起来非常简单思路,实现起来到底有什么坑。怎么绕过这些坑实现一个正确的二分查找法。

/**
	 * 二分查找法,在有序数组arr中,查找target
	 * 如果找到target,返回相应的索引index
	 * 如果没有找到target,返回-1
	 * @param arr
	 * @param target
	 * @return
	 */
	public static int find(Comparable[] arr, Comparable target){
		
		//在arr[l...r]之间查找target,l和r是闭区间的左右边界。
		int l = 0, r = arr.length - 1;
		
		//循环到子数组至少有一个元素
		while(l <= r){
			//int mid = (l + r) / 2; 
			//为了防止极端情况下的整形溢出,使用下面的逻辑求出mid。
			int mid = l + (r - l) / 2;
			
			if(arr[mid].compareTo(target) == 0){
				return mid;
			}else if(arr[mid].compareTo(target) > 0){
				//在arr[l...mid-1]之中超找target
				r = mid - 1;
			}else{
				//在arr[mid+1...r]之中超找target
				l = mid + 1;
			}
		}
		
		return -1;
	}

我们现在已经实现了一个版本的二分查找法,这种二分查找法也是最为经典的二分查找法最为经典的实现。我们可以发现这是一个非常递归的思路,感兴趣的可以用递归来实现这个二分查找法,比较一下递归法和非递归法在思维上的差异。

使用递归的方式实现二分查找法,递归实现通常思维起来更容易,因为我们每一次不需要考虑全局,只需要考虑一个子问题,想好递归关系,想清楚在最基础的层面是怎么样做的,就能写出递归的函数来。

不过递归也存在一些缺点,相比于非递归实现,递归在性能上会略差一些。当然这种差异是常数级别的,不管是递归还是非递归,时间复杂度都是O(logn)。

二、二分查找法的变种 floor和ceil

这里有两个非常重要的,也是应用非常广的两个函数 floor和ceil。

我们之前寻找的二分查找法,通常是假设数组中是没有重复元素的。当然我们刚才的算法,即使数组中有重复的元素,对于排好序的数组中,我们依然能找到相应元素的索引,只不过这个元素在数组中可能会出现很多次,上面实现的二分查找并不能保证找到的元素索引具体是哪个索引。

 

但是相应的floor和ceil这两个函数应该能保证,调用floor方法找元素v的话,应该能保证找到的元素v在整个数组中第一次出现的位置。而调用ceil方法找元素v的话,应该能保证找到的元素v在整个数组中最后一次出现的位置。

这两个函数还有一个优势,就是当我们在数组中查找的元素不存在话,我们上面的实现直接返回了-1,但是floor和ceil这两个函数它们的返回值,应该是这样的情况。比如在如下数组中要查找42的话,我们可以看到42在这个数组中不存在。floor函数返回的应该是最后一个41元素的索引,而ceil函数返回的应该是第一个43元素的索引。

这里只是将floor和ceil这两个函数的定义讲了出来,本篇就不详细的讲解这两个函数的实现,感兴趣的可以自己实现一下floor和ceil这两个函数。在实现的时候一定要注意,定义清楚你所使用的变量具体是什么含义。这两个方法的实现相对难一些,下面是具体的代码实现,能具体想清楚整个流程对大家是很有帮助的。

// 非递归的二分查找算法
public class BinarySearch {

    // 我们的算法类不允许产生任何实例
    private BinarySearch() {}

    // 二分查找法,在有序数组arr中,查找target
    // 如果找到target,返回相应的索引index
    // 如果没有找到target,返回-1
    public static int find(Comparable[] arr, Comparable target) {

        // 在arr[l...r]之中查找target
        int l = 0, r = arr.length-1;
        while( l <= r ){

            //int mid = (l + r)/2;
            // 防止极端情况下的整形溢出,使用下面的逻辑求出mid
            int mid = l + (r-l)/2;

            if( arr[mid].compareTo(target) == 0 )
                return mid;

            if( arr[mid].compareTo(target) > 0 )
                r = mid - 1;
            else
                l = mid + 1;
        }

        return -1;
    }

    // 二分查找法, 在有序数组arr中, 查找target
    // 如果找到target, 返回第一个target相应的索引index
    // 如果没有找到target, 返回比target小的最大值相应的索引, 如果这个最大值有多个, 返回最大索引
    // 如果这个target比整个数组的最小元素值还要小, 则不存在这个target的floor值, 返回-1
    static int floor(Comparable[] arr, Comparable target){

        // 寻找比target小的最大索引
        int l = -1, r = arr.length-1;
        while( l < r ){
            // 使用向上取整避免死循环
            int mid = l + (r-l+1)/2;
            if( arr[mid].compareTo(target) >= 0 )
                r = mid - 1;
            else
                l = mid;
        }

        assert l == r;

        // 如果该索引+1就是target本身, 该索引+1即为返回值
        if( l + 1 < arr.length && arr[l+1] == target )
            return l + 1;

        // 否则, 该索引即为返回值
        return l;
    }


    // 二分查找法, 在有序数组arr中, 查找target
    // 如果找到target, 返回最后一个target相应的索引index
    // 如果没有找到target, 返回比target大的最小值相应的索引, 如果这个最小值有多个, 返回最小的索引
    // 如果这个target比整个数组的最大元素值还要大, 则不存在这个target的ceil值, 返回整个数组元素个数n
    static int ceil(Comparable[] arr, Comparable target){

        // 寻找比target大的最小索引值
        int l = 0, r = arr.length;
        while( l < r ){
            // 使用普通的向下取整即可避免死循环
            int mid = l + (r-l)/2;
            if( arr[mid].compareTo(target) <= 0 )
                l = mid + 1;
            else // arr[mid] > target
                r = mid;
        }

        assert l == r;

        // 如果该索引-1就是target本身, 该索引+1即为返回值
        if( r - 1 >= 0 && arr[r-1] == target )
            return r-1;

        // 否则, 该索引即为返回值
        return r;
    }

    // 测试我们用二分查找法实现的floor和ceil两个函数
    // 请仔细观察在我们的测试用例中,有若干的重复元素,对于这些重复元素,floor和ceil计算结果的区别:)
    public static void main(String[] args){

        Integer arr[] = new Integer[]{1, 1, 1, 2, 2, 2, 2, 2, 4, 4, 5, 5, 5, 6, 6, 6};
        for( int i = 0 ; i <= 8 ; i ++ ){

            int floorIndex = floor(arr, i);
            System.out.println("the floor index of " + i + " is " + floorIndex + ".");
            if( floorIndex >= 0 && floorIndex < arr.length )
                System.out.println("The value is " + arr[floorIndex] + ".");
            System.out.println();

            int ceilIndex = ceil(arr, i);
            System.out.println("the ceil index of " + i + " is " + ceilIndex + ".");
            if( ceilIndex >= 0 && ceilIndex < arr.length )
                System.out.println("The value is " + arr[ceilIndex] + ".");
            System.out.println();

            System.out.println();
        }

    }
}

 

三、总结

二分查找法是非常常用一种查找算法,实现起来也比较简单,需要注意一些边界取值的问题。可以用递归和非递归的方式实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值