二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。

假设只有 10 个订单,订单金额分别是:8,11,19,23,27,33,45,55,67,98。
现在想知道是否存在金额等于 19 元的订单。如果存在,则返回订单数据,如果不存在则返回 null。
image.png

最简单的情况就是有序数组中不存在重复元素,我们在其中用二分查找值等于给定值的数据。

  1. public int bsearch(int[] a, int n, int value) {
  2. int low = 0;
  3. int high = n - 1;
  4. while (low <= high) {
  5. int mid = (low + high) / 2;
  6. if (a[mid] == value) {
  7. return mid;
  8. } else if (a[mid] < value) {
  9. low = mid + 1;
  10. } else {
  11. high = mid - 1;
  12. }
  13. }
  14. return -1;
  15. }

上述代码有三个需要注意的地方:

  • 循环退出条件

注意是 low <= high,而不是 low < high

  • mid 的取值

实际上 mid = (low + high)/2 写法是有问题的,如果 low 和 high 比较大的话,两者之和就有可能会溢出
改进的方法是 mid = low + (hight - low)/2 或者 mid = low + ((high - low) >> 1)

  • low 和 high 的更新

low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。
比如,当 high=3,low=3 时,如果 a[3]不等于 value,就会导致一直循环不退出。

  1. // 二分查找的递归实现
  2. public int bsearch(int[] a, int n, int val) {
  3. return bsearchInternally(a, 0, n - 1, val);
  4. }
  5. private int bsearchInternally(int[] a, int low, int high, int value) {
  6. if (low > high) return -1;
  7. int mid = low + ((high - low) >> 1);
  8. if (a[mid] == value) {
  9. return mid;
  10. } else if (a[mid] < value) {
  11. return bsearchInternally(a, mid+1, high, value);
  12. } else {
  13. return bsearchInternally(a, low, mid-1, value);
  14. }
  15. }

二分查找依赖的顺序表结构,简单点说就是数组;其次,二分查找针对的是有序数据。
如果数据量太小其实不适合二分查找,直接顺序遍历即可
如果数据量太大其实也适合二分查找,顺序表结构依赖连续的内存空间,数据量太大可能内存空间不足

image.png

查找第一个值等于给定值的元素

比如下面这样一个有序数组,其中,a[5],a[6],a[7]的值都等于 8,是重复的数据。
我们希望查找第一个等于 8 的数据,也就是下标是 5 的元素。
image.png
如果我们用上述二分查找的代码实现,首先拿 8 与区间的中间值 a[4]比较,8 比 6 大,于是在下标 5 到 9 之间继续查找。
下标 5 和 9 的中间位置是下标 7,a[7]正好等于 8,所以代码就返回了。
尽管 a[7]也等于 8,但它并不是我们想要找的第一个等于 8 的元素,因为第一个值等于 8 的元素是数组下标为 5 的元素。

所以需要改造一下代码:

  1. public int bsearch(int[] a, int n, int value) {
  2. int low = 0;
  3. int high = n - 1;
  4. while (low <= high) {
  5. int mid = low + ((high - low) >> 1);
  6. if (a[mid] > value) {
  7. high = mid - 1;
  8. } else if (a[mid] < value) {
  9. low = mid + 1;
  10. } else {
  11. if ((mid == 0) || (a[mid - 1] != value)) return mid;
  12. else high = mid - 1;
  13. }
  14. }
  15. return -1;
  16. }

如果我们查找的是任意一个值等于给定值的元素,当 a[mid]等于要查找的值时,a[mid]就是我们要找的元素。但是,如果我们求解的是第一个值等于给定值的元素,当 a[mid]等于要查找的值时,我们就需要确认一下这个 a[mid]是不是第一个值等于给定值的元素。

我们重点看第 11 行代码。如果 mid 等于 0,那这个元素已经是数组的第一个元素,那它肯定是我们要找的;如果 mid 不等于 0,但 a[mid]的前一个元素 a[mid-1]不等于 value,那也说明 a[mid]就是我们要找的第一个值等于给定值的元素。

如果经过检查之后发现 a[mid]前面的一个元素 a[mid-1]也等于 value,那说明此时的 a[mid]肯定不是我们要查找的第一个值等于给定值的元素。那我们就更新 high=mid-1,因为要找的元素肯定出现在[low, mid-1]之间。

还有一种更简洁的代码,但是不容易理解

  1. public int bsearch(int[] a, int n, int value) {
  2. int low = 0;
  3. int high = n - 1;
  4. while (low <= high) {
  5. int mid = low + ((high - low) >> 1);
  6. if (a[mid] >= value) {
  7. high = mid - 1;
  8. } else {
  9. low = mid + 1;
  10. }
  11. }
  12. if (low < n && a[low]==value) return low;
  13. else return -1;
  14. }

查找最后一个值等于给定值的元素

  1. public int bsearch(int[] a, int n, int value) {
  2. int low = 0;
  3. int high = n - 1;
  4. while (low <= high) {
  5. int mid = low + ((high - low) >> 1);
  6. if (a[mid] > value) {
  7. high = mid - 1;
  8. } else if (a[mid] < value) {
  9. low = mid + 1;
  10. } else {
  11. if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
  12. else low = mid + 1;
  13. }
  14. }
  15. return -1;
  16. }

我们还是重点看第 11 行代码。如果 a[mid]这个元素已经是数组中的最后一个元素了,那它肯定是我们要找的;如果 a[mid]的后一个元素 a[mid+1]不等于 value,那也说明 a[mid]就是我们要找的最后一个值等于给定值的元素。

如果我们经过检查之后,发现 a[mid]后面的一个元素 a[mid+1]也等于 value,那说明当前的这个 a[mid]并不是最后一个值等于给定值的元素。我们就更新 low=mid+1,因为要找的元素肯定出现在[mid+1, high]之间。

查找第一个大于等于给定值的元素

在有序数组中,查找第一个大于等于给定值的元素。比如,数组中存储的这样一个序列:3,4,6,7,10。如果查找第一个大于等于 5 的元素,那就是 6。

  1. public int bsearch(int[] a, int n, int value) {
  2. int low = 0;
  3. int high = n - 1;
  4. while (low <= high) {
  5. int mid = low + ((high - low) >> 1);
  6. if (a[mid] >= value) {
  7. if ((mid == 0) || (a[mid - 1] < value)) return mid;
  8. else high = mid - 1;
  9. } else {
  10. low = mid + 1;
  11. }
  12. }
  13. return -1;
  14. }

如果 a[mid]小于要查找的值 value,那要查找的值肯定在[mid+1, high]之间,所以,我们更新 low=mid+1。

对于 a[mid]大于等于给定值 value 的情况,我们要先看下这个 a[mid]是不是我们要找的第一个值大于等于给定值的元素。如果 a[mid]前面已经没有元素,或者前面一个元素小于要查找的值 value,那 a[mid]就是我们要找的元素。这段逻辑对应的代码是第 7 行。

如果 a[mid-1]也大于等于要查找的值 value,那说明要查找的元素在[low, mid-1]之间,所以,我们将 high 更新为 mid-1。

查找最后一个小于等于给定值的元素

比如,数组中存储了这样一组数据:3,5,6,8,9,10。最后一个小于等于 7 的元素就是 6。

  1. public int bsearch7(int[] a, int n, int value) {
  2. int low = 0;
  3. int high = n - 1;
  4. while (low <= high) {
  5. int mid = low + ((high - low) >> 1);
  6. if (a[mid] > value) {
  7. high = mid - 1;
  8. } else {
  9. if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
  10. else low = mid + 1;
  11. }
  12. }
  13. return -1;
  14. }