zoukankan      html  css  js  c++  java
  • 二分搜索个人小结

    二分搜索是平时用到很多的东西,但是经常在细节上的地方会出现问题,这里我想说说我的一点理解。

    平时最多的情况就是查询一个数出现在有序数组里的位置,那么

    在查找 num 的时候,一般需要用到四种情况:
    1、找得到 num 返回最左边的与其相等的下标.(YES_LEFT)
    2、找得到 num 返回最右边的与其相等的下标.(YES_RIGHT)
    3、找不到 num 返回最接近它的比它小的数(在 num 的左边).(NO_LEFT)
    4、找不到 num 返回最接近它的比它大的数(在 num 的右边).(NO_RIGHT)

    这是从 http://blog.csdn.net/int64ago/article/details/7425727 学到的

    一般我用的二分搜索是这样的:

    1 int num[];
    2 int binary_search(int l, int r, int val)
    3 {
    4     while(l <= r) {
    5         int mid = (l + r) >> 1;
    6         //这里是判定条件: 让区间二分
    7     }
    8     return (l 或者 r);
    9 }

    其中,这个判定条件还有return语句都是一些很细节的东西,但是决定了这个二分搜索是不是正确的.(精髓所在)

    现在有这样一个有序数组[1, 3, 4, 4, 4, 8, 10],一共有7个数.(为了方便数组下标从 1 开始算)

    现在随便写一个判定:

    NO.1:

    1 if(num[mid] < val) l = mid + 1;
    2 else r = mid - 1;

    这句话就是如果 val 比 num[mid] 大, 那么 l 就变成 mid + 1, 否则 r 变成 mid - 1.(这是一句废话)

    分析一下执行完这个判定之后的情况(分为要查的 val 会不会出现在 num[] 中两种情况):

    (1)(不出现的情况): 查的数 val = 2.

    第一次: l = 1, r = 7, mid = 4. num[4] = 4 >= val.执行 else 语句.

    第二次: l = 1, r = 3, mid = 2, num[2] = 3 >= val.执行 else 语句.

    第三次: l = 1, r = 1, mid = 1, num[1] = 1 < val.执行 if 语句.

    结束循环: l = 2, r = 1.

    这个时候, num[l] = 3, 刚好大于 val 的最左边的数, num[r] = 1, 刚好小于 val 的最右边的数.

    所以,我们假设如果是找不到 val 的话,如果 return l,那么返回 NO_RIGHT 的情况, 如果 return r,那么返回 NO_LEFT 的情况.

    单单通过这样一个样例是没法证明这个假设的正确性的,那么接下来我们仔细想想:

    l 指针和 r 指针经过不断移动后,最终一定会移动到一个重合的位置,即 l == r 的情况,这个时候指向的数, 一定是恰好大于 val 或者恰好小于 val 的.这个时候再通过一次判断来进行调整,如果指向的数已经是大于 val 了,那么执行 else 语句, r 左移,这个时候 l 指向已经是恰好大于 val 的数,r 指向的数也就恰好是小于 val 的数了.如果指向已经是小于 val 了,那么执行 if 语句, l 右移,也还是上面那样的结果.

    因此这个假设是成立的.

    (2)(出现的情况): 查的数 val = 4.

    第一次: l = 1, r = 7, mid = 4, num[4] = 4 >= val.执行 else 语句.

    第二次: l = 1, r = 3, mid = 2, num[2] = 3 < val.执行 if 语句.

    第三次: l = 3, r = 3, mid = 3, num[3] = 4 >= val.执行 else 语句.

    结束循环: l = 3, r = 2.

    这个时候我们看到, num[l] = num[3] = 4, 刚好是找得到 val 并且最左边的位置, num[r] = num[2] = 3 != val, 无意义.

    假设如果 return l, 那么返回的是一个指向的数等于 val 并且是最左边的下标.即 YES_LEFT 的情况.

    继续验证一下:

    和(1)类似,如果 num[mid] >= val 的话, r 会往左移动, 否则 l 右移, 最终一定会移动到一个重合的位置 l == r, 使得 num[l] == num[r]恰好等于 val 并且是在最左边,或者是一个恰好小于 val 的最大数.通过最后一次判定调整之后, 如果这个时候已经恰好等于 val 并且是在最左边,那么会执行 else 语句, r 左移, 否则执行 if 语句, l 右移, 因为序列中存在一个数等于 val, 移动之前的数又是刚好小于 val 的最大的数,这个时候移动完的 l 刚好就是等于 val 了,并且是在最左边.

    所以 return l,就可以找到 YES_LEFT的情况了.

    通过NO.1的代码,四种情况,已经有三种情况可以判定了,还剩下一种 YES_RIGHT 的情况.因此还有下面这样的代码:

    NO.2:

    1 if(num[mid] <= val) l = mid + 1;
    2 else r = mid - 1;

    其实和 NO.1 相比就是把等于提到 if 语句了.

    还是分为序列中出现 val 和不出现 val 的情况.

    (1)(不出现的情况): 这种情况和上面 NO.1 是一样的.故不作讨论了.

    (2)(出现的情况): 继续查 val = 4.

    第一次: l = 1, r = 7, mid = 4, num[4] = 4 <= val.执行 if 语句.

    第二次: l = 5, r = 7, mid = 6, num[6] = 8 > val.执行 else 语句.

    第三次: l = 5, r = 5, mid = 5, num[5] = 4 <= val.执行 if 语句.

    结束循环: l = 6, r = 5.

    这个时候, num[l] = 8, num[r] = 4.如果这个时候返回 r ,那么和我们想要的 YES_RIGHT 情况就是一样的.

    因为如果 num[mid] <= val 的话, l 会右移, 否则 r 左移, 最终也还是一定移动到一个重合的位置 l == r, 使得 num[l] == num[r],并且这个 num[l] 是刚好大于 val 的或者恰好是等于 val 并且在最右边.再通过一次判定调整,如果这个时候已经恰好等于 val,那么肯定执行 if 语句, l 右移,否则执行 else 语句, r 左移,得到 r 指向的数刚好就是等于 val 的最右边的数了.

    综合上面的 NO.1 和 NO.2 代码和 两种 return 语句.

    可以将 YES_RIGHT 和 NO_LEFT 这两种归结成 NO.2 代码:

    1 int Y_R_N_L(int l, int r, int val)
    2 {
    3     while(l <= r) {
    4         int m = (l + r) >> 1;
    5         if(num[m] <= val) l = m + 1;
    6         else r = m - 1;
    7     }
    8     return r;
    9 }

    可以将 NO_RIGHT 和 YES_LEFT 这两种归结成 NO.1 代码:

    1 int N_R_Y_L(int l, int r, int val)
    2 {
    3     while(l <= r) {
    4         int m = (l + r) >> 1;
    5         if(num[m] < val) l = m + 1;
    6         else r = m - 1;
    7     }
    8     return l;
    9 }
  • 相关阅读:
    精通CSS高级Web解决方案(第2版)——读书笔记
    MySQL的知识点总结(一)
    json深拷贝
    Typescript 从Array 继承时碰到的问题
    webpack 笔记
    JavaScript 高效代码
    编写更优雅的 JavaScript 代码
    JavaScript复杂判断优雅写法
    深入浅出Javascript事件循环机制
    关于面向对象封装的思考--持续更新
  • 原文地址:https://www.cnblogs.com/fightfordream/p/6033165.html
Copyright © 2011-2022 走看看