zoukankan      html  css  js  c++  java
  • 深入分析二分查找及其变体

    1—一般二分查找

    一般的二分查找代码如下:

    int search(int A[], int n, int target)
    {
    int low = 0, high = n-1;
    while(low <= high)
    {
    // 注意:若使用(low+high)/2求中间位置容易溢出
    int mid = low+((high-low)>>1);
    if(A[mid] == target)
    return mid;
    else if(A[mid] < target)
    low = mid+1;
    else // A[mid] > target
    high = mid-1;
    }
    return -1;
    }

    上面的二分查找非常的朴实,上述二分查找的作用当然就是:找到数组A[]中等于target的元素。但是这个查找元素隐含了一个条件:这个数组的元素是不包含重复元素的。这个限制可以说是非常的大。我们来看一下,假设存在重复元素,按照上述找法,找的是谁

    7 7 7 7 8 10;7

    即假设我们找7,显然第一次就找到了,这个“7”在A[2]的位置,也就是我们按照上述思路找,能找到。但并不是最开始的7或者结尾的7.

    2—找到有重复元素数组第一个索引元素

    假设我们要找到最开始的7,应该如何修改代码

    呢?

    先上代码:

    int searchFirstPos(int A[], int n, int target)
    {
    if(n <= 0) return -1;
    int low = 0, high = n-1;
    while(low < high)
    {
    int mid = low+((high-low)>>1);
    if(A[mid] < target)
    low = mid+1;
    else // A[mid] >= target
    high = mid;
    }
    if(A[low] != target)
    return -1;
    else
    return low;
    }

    这个代码,为何会找到最开始的7呢?我们看看发生了什么

    还是:

    7 7 7 7 8 10;7

    第一次后,high ->A[2],循环没结束

    第二次后,high->A[1],循环没结束

    第三次后,high->A[0],循环结束

    循环结束条件喂 low == high!

    我们再看一个例子:

    5 7 7 7 7 8 10;7

    第一次后,high ->A[3],循环没结束

    第一次后,high ->A[1],循环没结束

    第三次后,low->A[1],循环结束。

    再看一个例子

    2 5 7 8 9 10 12 13 13 14;13

    第一次后,low ->A[5],循环没结束

    第二次后,high ->A[7],循环没结束

    第三次后,low ->A[6],循环没结束

    第三次后,low ->A[7],循环结束

    总结:

    也就说:找出现的第一个值,其必然结果是low == high的时候,但是为何是第一个,而不是最后一个呢?

    这个主要取决于下面这行代码:

    else // A[mid] >= target
    high = mid;

    也就是说即使A[mid] == target,我们也会使得high == target,换句话而言,即使A[high] == target;

    我们也会让high向第一个出现查找值的索引位置靠拢!!!

    if(A[mid] < target)
    low = mid+1;

    不过仍然是借鉴了传统二分查找的思想,mid的值小了,就让low=mid+1;

    真正要指向第一个,要做的就是:让high向第一个靠拢!!!!

    3—找到重复元素数组最后一个元素

    上述中说到找第一个元素,要让high向第一个靠拢,而这里要找最后一个元素,则要low向最后一个元素靠拢。

    先上代码:

    int searchLastPos(int A[], int n, int target)
    {
    if(n <= 0) return -1;
    int low = 0, high = n-1;
    while(low < high)
    {
    /*
    这里中间位置的计算就不能用low+((high-low)>>1)了,因为当low+1等于high
    且A[low] <= target时,会死循环;所以这里要使用low+((high-low+1)>>1),
    这样能够保证循环会正常结束。
    */
    int mid = low+((high-low+1)>>1);
    if(A[mid] > target)
    high = mid-1;
    else // A[mid] <= target
    low = mid;
    }
    if(A[high] != target)
    return -1;
    else
    return high;
    }

    这里需要注意的是下面这行代码:

    int mid = low+((high-low+1)>>1);

    假设仍然是:

    int mid = low+((high-low)>>1);

    我们看会发生什么?

    以下述序列为例;

    1 2 7 7 7 8 9 13;7

    第一次:mid->A[3],low-->A[3];

    第二次:mid->A[5],high-->A[4];

    第三次:mid->A[3],low->A[3].....而此时low < high,出现死循环;

    可以再举出其他例子,但是结果表明,问题总是出现在最后一步,也就是最后一步总有higg-low =1; 且mid一直等于low,这使得循环一直为死循环。

    究其原因,是因为:/2导致的向下取整。而high-low+1可以保证向上取整!!!

    那我们为何要这么做呢?主要原因在于:

    if(A[mid] > target)
    high = mid-1;

    即,只要A[mid]>target,high的的值总会减小。也就是说,即使我们向上取整,最终也会使得high指向正确的位置,low也会因为向上取整的原因,最终使得low和high收敛到同一个位置(比如low->A[3]=7,high->A[4]=7.),而low则不同,low刷新成mid,但最后一步有可能不收敛,mid的值不再刷新时候,low的值也不刷新,从而导致low和high不会收敛到同一个位置。

    4—给定一个有序(非降序)数组A,若target在数组中出现,返回其第一个位置,若不存在,返回它应该插入的位置

    我们稍做分析就知道,对于代码:

    上述问题1、2、3无论是哪个问题,当找不到target时候,low==high等于target应该处于的位置是恒成立的。因此,这道题的代码:

    int searchPos(int A[], int n, int target)
    {
    if(n <= 0) return -1;
    int low = 0, high = n-1;
    while(low < high)
    {
    int mid = low+((high-low)>>1);
    if(A[mid] < target)
    low = mid+1;
    else // A[mid] >= target
    high = mid;
    }
       return low;
    }

    5—给定一个有序(非降序)数组A,可含有重复元素,求绝对值最小的元素的位置

    这个问题也很简单,仅仅给出思路:

    绝对值最小的数当然是0,这个问题转化为:找数组中0的位置,若没找到0,那么最终low==high指向的位置的数或者low-1(或者high-1)

    指向的数就是最小的。

    6—一个有序(升序)数组,没有重复元素,在某一个位置发生了旋转后,求target在变化后的数组中出现的位置,不存在则返回-1

    0 1 2 4 5 6 7 可能变成 2 4 5 6 7 0 1

    很明显的特征在于:有序数组旋转后,存在着两个有序部分。可能我们会想到对这两部分分别进行二分查找,这个思路总体上是没有问题的。但是问题在于我们如何知道这个数据转折点在哪?又或许我们是否有必要知道呢?

    当然了,我们可以按照下面这个思路去处理问题:

    第一步:寻找那个数据转折点(比如上述序列中就是7)

    第二步,判断target所属区间(转折点前还是后)

    第三步:二分查找

    当然了,这个思路是完全ok的,也可以按照这个思路去处理,事实上,我开始也是这么做的。但是实际情况是,我们根本没有必要这么做,没必要去找那个数据转折点的位置。

    因为这个数组旋转一次后,我们只需要关注旋转后的数组的中间元素,一个很重要的特点是:中间元素两边的子数组至少有一个是有序的。因此我们可以判断target是否在这个有序子数组中。从而决定target的搜索区间。

    先上代码:

    int searchInRotatedArray(int A[], int n, int target) 
    {
    int low = 0, high = n-1;
    while(low <= high)
    {
    int mid = low+((high-low)>>1);
    if(A[mid] == target)
    return mid;
    if(A[mid] >= A[low])
    {
    // low ~ mid 是升序的
    if(target >= A[low] && target < A[mid])
    high = mid-1;
    else
    low = mid+1;
    }
    else
    {
    // mid ~ high 是升序的
    if(target > A[mid] && target <= A[high])
    low = mid+1;
    else
    high = mid-1;
    }
    }
    return -1;
    }

    这段代码可以说是相当完美!

    来分析一下:

    整体而言,这段代码仍然采用二分查找法。也许我们会心有余悸,但是仔细分析发现,非常的巧妙。

    if(A[mid] >= A[low]) 

    这个判断是用来表明:前半段是否是是升序有序子序列。如果是的话:

    if(target >= A[low] && target < A[mid])
    high = mid-1;

    如果同时要找的数大于首个数,而小于中间元素,那么要找的数就位于有序序列之间。自然也就执行了:

    high = mid-1;

    该算法的精华在于:

    else

    这个else是指不满足于上述if条件的所有可能。自然也包括了二分查找的另一半target > A[mid]。但是其作用不仅仅是这个。那他的作用是什么呢?

    我们看,要想所查找target位于有序数组中,他需要满足:

    A[mid] >= A[low] && target >= A[low] && target < A[mid]

    或者:

    A[mid] < A[low] && target > A[mid] && target <= A[high]

    不满足上述条件的时候,发生了:

    low = mid+1;

    或者:

    high = mid-1;

    这样的意义何在呢?没错就是通过改变low和high的索引,改变了mid的位置,最终也就是随着迭代的进行,使得target总可以处于一个有序子数组中,并找到它。也就说,最重要的代码:就是那个

    else

    请再深入推敲上上述代码。尤其是else的作用。对这个问题进一步思考:如果这个数组存在重复元素,那么还能进行二分查找吗?显然不能,

  • 相关阅读:
    Design:目录
    前端框架:template
    Template-ArtTemplate:artTemplate.js
    开发框架:AdminLTE
    开发框架:目录
    杂项:短网址
    httpd
    Java实现洛谷 P1428 小鱼比可爱
    Java实现洛谷 P1428 小鱼比可爱
    java实现洛谷P1308统计单词数
  • 原文地址:https://www.cnblogs.com/shaonianpi/p/12457982.html
Copyright © 2011-2022 走看看