Bitonic search

简介

    Bitonic search是一个和binary search比较类似的一种查找方法,不过它的过程会显得稍微复杂一点。从某种角度来说,它和我前面这篇文章里讨论过的一种binary search的一个变体很像,不过就是因为一个小小的变化,它们的解决办法就有着极大的差别。我们先来看看问题本身的描述:

    一个bitonic数组是由一个递增的整数序列后面接着一个递减的序列,假设数组里有N个唯一的数字,我们要查询给定的一个数字, 使得查找的时间尽可能的短。


3logN解决方法

    在标出这个子标题的时候,似乎有点未卜先知的味道。我们可以先根据这个问题来分析一下。假定我们给定有这么一个数组:[1, 3, 4, 6, 9, 14, 11, 7, 5, 2, -4, -9] 。那么它们的布局应该成如下的一个形式:

    假设我们水平线的方向表示数组排列的方向,它们的数值对应这上面折线的高度,那么比如有一个点是这个数组中最大的。如果我们事先找到了这个最大的点,假设它的下标是k。那么从数组的0到k这部分是一个严格递增的序列,同样从k+1到数组末尾是一个递减的序列。如果我们熟悉binary search的话,现在应该就找到一个思路了。剩下的就是我们在这个两边的序列里分别去搜索指定的值。

    那么概括起来我们这个方法的思路如下:

    1. 找到数组最大的那个值。既然我们期望能尽可能快的找到。直接遍历搜索的时间复杂度到了O(N)。显然不太合适。不过我们也可以利用binarysearch的思路,每次对数组取中间值,如果这个值比它左边的大,表示它在左边递增的序列,如果它比左边的小,表示它在右边递减的序列。只有当它比左右两边的都大时,才能说明它就是我们找到的最大值。

    2. 找到最大值之后我们就以它为分界线,对给定的值在它两边的区域分别进行binarysearch。为什么要两边分开呢?一个是对于一个值它可能存在于左边序列,也可能存在于右边序列。另外,我们默认的binarysearch默认的是要求数组升序排列的。对于后面降序的数组我们要对这个方法做一点修改。

    下面是我们对它们各部分的实现:

public static int findPeak(int[] a) {
        int l = 0;
        int r = a.length -1;
        int mid;
        while(l <= r) {
            mid = l + (r - l) / 2;
            if(a[mid] > a[mid - 1] && a[mid] > a[mid + 1])
                return mid;
            else if(a[mid] > a[mid - 1])
                l = mid + 1;
            else if(a[mid] > a[mid + 1])
                r = mid - 1;
        }

        return -1;
    }

    这部分的实现和binarysearch很像,只是每次需要判断的时候设置一下l, r的情形不同。通过这一步findPeak之后我们就得到这个最大值所在的下标了。然后就是分别搜索实现的代码,这里分为对升序数组的搜索和降序数组的搜索:

 public static int ascBinarySearch(int[] a, int l, int r, int v) {
        if(a == null || a.length == 0)
            return -1;
        int mid;
        while(l <= r) {
            mid = l + (r - l) / 2;
            if(v > a[mid])
                l = mid + 1;
            else if(v < a[mid])
                r = mid - 1;
            else
                return mid;
        }

        return -1;
    }

 public static int desBinarySearch(int[] a, int l, int r, int v) {
        if(a == null || a.length == 0)
            return -1;
        int mid;
        while(l <= r) {
            mid = l + (r - l) / 2;
            if(v > a[mid])
                r = mid - 1;
            else if(v < a[mid])
                l = mid + 1;
            else
                return mid;
        }

        return -1;
    }

   这两个方法里第一个就是典型的binarysearch实现,第二个针对降序需要设置l, r的值的形式不一样。也就没什么其他特殊的了。

    当然,从完整实现的角度来看,我们还需要结合前面几个步骤的代码:

public static int bitonicSearch(int[] a, int v) {
        int peak = findPeak(a);
        if(peak != -1) {
            int first = ascBinarySearch(a, 0, peak, v);
            int last = desBinarySearch(a, peak + 1, a.length - 1, v);
            if(first != -1)
                return first;
            if(last != -1)
                return last;
        }
        
        return -1;
    }

    这里唯一值得注意的地方就是如果我们查找的对象找不到,在后面都统一返回负数了。从前面所有的步骤来看,第一步我们查找最大值,用的时间为logN,第二步要在两个区域里查找给定的数字,每个地方查找的时间分别为logN,所以整体的时间加起来大致上为3logN。

    这就是我们提到的一种解决方法。嗯,看起来已经相当完美了。当然,从追求完美的角度来说,我们还有没有更加快的解决方法呢?前面的方法里,我们还花了不少时间去查找这个最大的值,能不能不用查这个最大值然后也能解决呢?实际上,还有一个更猛的。

 

2logN解决方法

     我们再换一个角度来思考这个问题。前面的问题实质上是需要我们找到最大值,然后根据两边的段来查找。现在我们来看看怎么不用找最大值来解决它。针对这种布局的数列,如果我们取它们中间的元素,则这个中间的元素可能处于两种情况:

1. 中间元素处在左边递增的序列中。

2. 中间元素处在右边递减的序列中。

    我们针对这两种情况一一来分析:

中间元素在左边递增序列

    这种情况下,对应的图如下:

    在上图中,我们假定中间的竖直的线为中间线,那么这种情况下的查找我们也需要进一步的讨论。

    当取得中间值元素,如果我们要查找的元素比我们当前的元素大,那么根据这个情况,我们知道它肯定不会在中间线分割的左边部分,它只可能在右边。这个时候它的分布很可能还是包含在右边整个的区域里的。我们如果要查找它的话,需要进一步递归的去找。

    如果我们要找的这个元素比中间元素小呢?这个时候就比较有意思了。因为它还是可能出现在左边,也可能出现在右边。这个时候,对于左边来说,我们直接使用通用的binarySearch查就可以了。对于右边的呢?因为要查的元素比目标元素小,可是右边的这个串并不是严格递减的,它中间有一部分是递增的。我们的二叉查找能奏效吗?我们再来进一步的分析一下。

    如果我们按照descending的序列情况来做查找,对于上面的序列来说,假定我们下一次再取中间值的时候,这个目标值还是小于中间值,那么,我们肯定会从中间值的右边去找,这样还是会向着递减序列的范围去趋近。假定目标值小于这次取的中间值,那么这个目标值肯定已经不在原来左边递增的序列上了,因为那个时候左边的序列值只会比目标值更大。所以说,这样子去做二叉查找还是可行的。

    概括起来的话,前面的过程可以用代码描述如下:

if(a[mid] > a[mid - 1]) {  //如果在递增序列
            if(v > a[mid]) { //目标值大于中间值,继续递归
                return bitonicSearch2(a, mid + 1, r, v);
            } else {  //目标值小于中间值,需要查找左边的递增序列和右边的递减序列
                System.out.printf("l: %d, r: %d, mid: %d", l, r, mid);
                int asc = ascBinarySearch(a, l, mid, v);
                System.out.println("asc: " + asc);
                int des = desBinarySearch(a, mid + 1, r, v);
                System.out.println("des: " + des);
                if(asc != -1)
                    return asc;
                if(des != -1)
                    return des;
                return -1;
            }
        }

     为了方便看到当前划分的情况,增加了一些输出的语句。这里的ascBinarySearch,desBinarySearch和前面的实现是一样的。

 

中间元素在右边递减序列

    这种情况实际上就和前面的很类似了,它对应的情况如下图:

    我们也可以得到类似的结论,当我们的目标值比中间值大的时候,我们需要对它左边的范围进行递归。否则在它的左边做递增的二叉查找,右边做递减的二叉查找。这部分的伪代码如下:

 

if(v > a[mid]) {
                return bitonicSearch2(a, l, mid, v);
            } else {
                int asc = ascBinarySearch(a, l, mid, v);
                int des = desBinarySearch(a, mid + 1, r, v);
                if(asc != -1)
                    return asc;
                if(des != -1)
                    return des;
                return -1;
            }

 

    前面贴出来的只是一个代码的片段,完整的代码如下:

public static int bitonicSearch2(int[] a, int l, int r, int v) {
        if(l > r)
            return -1;
        int mid = l + (r - l) / 2;
        if(a[mid] == v)
            return mid;
        if(a[mid] > a[mid - 1]) {
            if(v > a[mid]) {
                return bitonicSearch2(a, mid + 1, r, v);
            } else {
                System.out.printf("l: %d, r: %d, mid: %d", l, r, mid);
                int asc = ascBinarySearch(a, l, mid, v);
                System.out.println("asc: " + asc);
                int des = desBinarySearch(a, mid + 1, r, v);
                System.out.println("des: " + des);
                if(asc != -1)
                    return asc;
                if(des != -1)
                    return des;
                return -1;
            }
        } else {
            if(v > a[mid]) {
                return bitonicSearch2(a, l, mid, v);
            } else {
                int asc = ascBinarySearch(a, l, mid, v);
                int des = desBinarySearch(a, mid + 1, r, v);
                if(asc != -1)
                    return asc;
                if(des != -1)
                    return des;
                return -1;
            }
        }
    }

    我们来看看整体的代码时间复杂度,每次执行一次要么就直接递归到原来的一半,要么就规约到两个部分的binarySearch。所以它的时间复杂度只有2logN。

 

总结

    就是简简单单的一个binarySearch,通过它针对不同数组特性的变化可以牵涉出很多的变种。针对数组的递增递减特性来灵活的选择binarySearch实现,同时结合递归的方法,在很多时候还是一种有效的方式。在这里,尤其重要的是要针对不同的场景进行分析,只要把这些理解透了,后面的代码也就比较自然的出来了。

 

参考材料

http://stackoverflow.com/questions/19372930/given-a-bitonic-array-and-element-x-in-the-array-find-the-index-of-x-in-2logn

http://www.amazon.com/gp/product/032157351X/ref=s9_simh_gw_p14_d2_i2?pf_rd_m=ATVPDKIKX0DER&pf_rd_s=center-7&pf_rd_r=1WEQHVV0DA1TWXGSEKSH&pf_rd_t=101&pf_rd_p=1688200482&pf_rd_i=507846

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值