有序数组的查找

题目描述

给定一个有序的数组,查找某个数是否在数组中,请编程实现。

分析与解法

一看到数组本身已经有序,我想你可能反应出了要用二分查找,毕竟二分查找的适用条件就是有序的。那什么是二分查找呢?

二分查找可以解决(预排序数组的查找)问题:只要数组中包含T(即要查找的值),那么通过不断缩小包含T的范围,最终就可以找到它。其算法流程如下:

  • 一开始,范围覆盖整个数组。
  • 将数组的中间项与T进行比较,如果T比数组的中间项要小,则到数组的前半部分继续查找,反之,则到数组的后半部分继续查找。
  • 如此,每次查找可以排除一半元素,范围缩小一半。就这样反复比较,反复缩小范围,最终就会在数组中找到T,或者确定原以为T所在的范围实际为空。

对于包含N个元素的表,整个查找过程大约要经过log(2)N次比较。

此时,可能有不少读者心里嘀咕,不就二分查找么,太简单了。

然《编程珠玑》的作者Jon Bentley曾在贝尔实验室做过一个实验,即给一些专业的程序员几个小时的时间,用任何一种语言编写二分查找程序(写出高级伪代码也可以),结果参与编写的一百多人中:90%的程序员写的程序中有bug(我并不认为没有bug的代码就正确)。

也就是说:在足够的时间内,只有大约10%的专业程序员可以把这个小程序写对。但写不对这个小程序的还不止这些人:而且高德纳在《计算机程序设计的艺术 第3卷 排序和查找》第6.2.1节的“历史与参考文献”部分指出,虽然早在1946年就有人将二分查找的方法公诸于世,但直到1962年才有人写出没有bug的二分查找程序。

你能正确无误的写出二分查找代码么?不妨一试,关闭所有网页,窗口,打开记事本,或者编辑器,或者直接在本文评论下,不参考上面我写的或其他任何人的程序,给自己十分钟到N个小时不等的时间,立即编写一个二分查找程序。

要准确实现二分查找,首先要把握下面几个要点:

  • 关于right的赋值
    • right = n-1 => while(left <= right) => right = middle-1;
    • right = n => while(left < right) => right = middle;
  • middle的计算不能写在while循环外,否则无法得到更新。

以下是一份参考实现:

  1. int BinarySearch(int array[], int n, int value)
  2. {
  3. int left = 0;
  4. int right = n - 1;
  5. //如果这里是int right = n 的话,那么下面有两处地方需要修改,以保证一一对应:
  6. //1、下面循环的条件则是while(left < right)
  7. //2、循环内当 array[middle] > value 的时候,right = mid
  8. while (left <= right) //循环条件,适时而变
  9. {
  10. int middle = left + ((right - left) >> 1); //防止溢出,移位也更高效。同时,每次循环都需要更新。
  11. if (array[middle] > value)
  12. {
  13. right = middle - 1; //right赋值,适时而变
  14. }
  15. else if(array[middle] < value)
  16. {
  17. left = middle + 1;
  18. }
  19. else
  20. return middle;
  21. //可能会有读者认为刚开始时就要判断相等,但毕竟数组中不相等的情况更多
  22. //如果每次循环都判断一下是否相等,将耗费时间
  23. }
  24. return -1;
  25. }

总结

编写二分查找的程序时

  • 如果令 `left <= right,则right = middle - 1;
  • 如果令left < right,则 right = middle;`

换言之,算法所操作的区间,是左闭右开区间,还是左闭右闭区间,这个区间,需要在循环初始化。且在循环体是否终止的判断中,以及每次修改left, right区间值这三个地方保持一致,否则就可能出错。