Loading

二分搜索个人小结

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

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

在查找 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 }

 

posted @ 2016-11-05 15:02  Shadowdsp  阅读(242)  评论(0编辑  收藏  举报