查找2-二分查找

二分查找是基于基础的数组数据结构的算法,本文将按照二分查找的基础问题与变体问题来学习二分查找的代码实现与应用。

之前的文章里面写过一个有关二分查找变体问题的几种情况,基础二分查找的对象主要是数组,通常有以下几个需求:

  1. 查找第一个与给定值相等的值的位置。
  2. 查找第一个大于给定值的位置。
  3. 查找最后一个等于给定值的位置。
  4. 查找最后一个小于给定值的位置。

与给定值相等

这个就包含了两种情况,一种是第一个等于给定值的位置,一种是最后一个等于给定值的位置。在前面的排序算法里面,插入排序那一节写了一个查找第一个大于给定值的二分查找,其实那个变一下就可以作为查找第一个等于给定值位置的代码。

下面的代码是查找第一个/最后一个等于给定值的实现:

static int binsearch_find_first_equal(int *sort_array, int size, int find_val)
{
    int left_pos = 0, right_pos = size - 1;
    int cur_pos = 0, find_pos = -1;

    while (left_pos <= right_pos) {
        cur_pos = left_pos + ((right_pos-left_pos)>>1);
        if (sort_array[cur_pos] >= find_val) {
            if (sort_array[cur_pos] == find_val)
                find_pos = cur_pos;
            right_pos = cur_pos - 1;
        } else
            left_pos = cur_pos + 1;
    }
    return find_pos;
}

static int binsearch_find_last_equal(int *sort_array, int size, int find_val)
{
    int left_pos = 0, right_pos = size - 1;
    int cur_pos = 0, find_pos = -1;

    while (left_pos <= right_pos) {
        cur_pos = left_pos + ((right_pos-left_pos)>>1);
        if (sort_array[cur_pos] > find_val) {
            right_pos = cur_pos - 1;
        } else {
            if (sort_array[cur_pos] == find_val)
                find_pos = cur_pos;
            left_pos = cur_pos + 1;
        }
    }
    return find_pos;
}

注意它们的区别主要是循环体内部的判断条件,一个find_pos的赋值是在大于等于的条件分支内,一个是在小于等于的条件分支内。而且需要注意的是,循环体的退出条件是left_pos > right_pos,这才能够保证找到的位置是正确的位置。

代码其实比较容易理解了,都是按照最直白的方式来写的,如果是查找第一个等于给定值的元素,那么在找到第一个相等的元素之后应该继续往前面搜索,不能够停下来,所以才需要放在大于等于的那个条件分支里面,查找最后一个就是反过来了。

查找第一个等于给定值位置:

  1. 分支条件是大于等于、小于。
  2. 赋值在大于等于分支内部的等于情况下执行。
  3. 结束条件是left_pos > right_pos

查找最后一个等于给定值位置:

  1. 分支条件是大于、小于等于。
  2. 赋值在小于等于分支内部的等于情况下执行。
  3. 结束条件是left_pos > right_pos

查找第一个大于给定值位置

为了看得更加清晰,值摘录关键部分的代码:

while (left_pos <= right_pos) {
    cur_pos = left_pos + ((right_pos-left_pos)>>1);
    if (sort_array[cur_pos] > find_val) {
        find_pos = cur_pos;
        right_pos = cur_pos - 1;
    } else
        left_pos = cur_pos + 1;
}

可以看到,这个时候分了两个分支,一个是大于,一个是小于等于,我们的赋值是在大于的那个分支里面。此时条件如下:

  1. 分支条件是大于、小于等于。
  2. 赋值在大于分支内部的执行。
  3. 结束条件是left_pos > right_pos

查找最后一个小于给定值位置

while (left_pos <= right_pos) {
    cur_pos = left_pos + ((right_pos-left_pos)>>1);
    if (sort_array[cur_pos] >= find_val) {
        right_pos = cur_pos - 1;
    } else {
        find_pos = cur_pos;
        left_pos = cur_pos + 1;
    }
}

这个时候分支条件又改变了,我们的赋值是在小于的那个分支里面。此时条件如下:

  1. 分支条件是大于等于、小于。
  2. 赋值在小于分支内部的执行。
  3. 结束条件是left_pos > right_pos

其它

还有查找第一个大于等于给定值的位置,最后一个小于等于给定值的位置,从前面的规律来看,我们只需要改变分支条件与赋值位置即可,这两种情况无非是需要在上面最后两种情况基础上改变等于条件所在的分支即可,自行就可以很简单的实现出来。

还是要强调下,以上都是针对已经按照从小到大顺序排好序的数据来说的,所以不会有查找第一个小于等于给定值的情况,因为要不就是没有,要不就是第0项,这个没有意义,查找最后一个大于等于给定值的情况也是类似。

写到这里,我又想到,能不能查找带范围的索引区间,比如查找小于等于X+n,大于等于X-n的值的区间,这个当然也可以,我用了一种比较直白的,可能效率不高的实现:

while (left_pos <= right_pos) {
    cur_pos = left_pos + ((right_pos-left_pos)>>1);
    if (sort_array[cur_pos] >= find_val-2) {
        find_l_pos = cur_pos;
        right_pos = cur_pos - 1;
    } else {
        left_pos = cur_pos + 1;
    }
}

left_pos = 0, right_pos = size - 1;
while (left_pos <= right_pos) {
    cur_pos = left_pos + ((right_pos-left_pos)>>1);
    if (sort_array[cur_pos] <= find_val+2) {
        find_r_pos = cur_pos;
        left_pos = cur_pos + 1;
    } else {
        right_pos = cur_pos - 1;
    }
}

以上 n 的取值为 2,采用了两次查找的方式来找到对应的区间索引值,暂时没有想好更加简洁的实现,先这样吧。

End

二分查找适合用于查找近似值,以及带区间的值,使用别的散列表或者二叉树就比较难以实现,不过跳表看起来也是比较适合用于查找带区间的值的。

二分查找的代码主要在于终止条件,区间更新条件,分支条件设置这三个点上,只要保证它们三个是对的,代码就能够轻易实现。所以要写二分查找的代码,先搞搞清楚那三个条件,然后再去实际写代码。

后面的算法或者数据结构可能就不会再去亲自试用代码实现了,因为难度越来越大,耗时越来越长,主要以掌握其原理与应用场景为主。

Github代码


想做的事情就去做吧
发布了128 篇原创文章 · 获赞 263 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/u013904227/article/details/90738870