二分查找简单入门

379 阅读1分钟

定义介绍

二分查找(Binary Search),也称折半查找,是一种在有序数组中查找特定元素的算法。

该算法的思想是将数组分成两部分,取数组中间的元素与目标元素进行比较,如果中间元素等于目标元素,则查找成功;如果中间元素大于目标元素,则在数组左半部分继续查找;如果中间元素小于目标元素,则在数组右半部分继续查找。每次比较都可以将查找范围减半,因此该算法的时间复杂度为O(log n)。

二分查找要求待查找的数组必须是有序的。如果数组无序,需要先进行排序,使其有序才能使用二分查找算法。该算法在处理大型数据集时比线性查找算法更加高效。

在Go语言中,排序的函数被集成在 "sort" 包下,比如给 int 类型的切片排序的函数是 sort.Ints(nums)

代码模板

做为非常经典的查找算法,当然有现成的模板方便学习理解和方便开发。

 func binarySearch(nums []int, target int) int {
     if len(nums) == 0 {
         return -1
     }
 ​
     left, right := 0, len(nums)-1
     for left <= right {
         // 防止 (left + right) 溢出
         mid := left + (right-left)/2
         if nums[mid] == target {
             return mid
         } else if nums[mid] < target {
             left = mid + 1
         } else {
             right = mid - 1
         }
     }
 ​
     // 没有找到目标元素,返回 -1
     return -1
 }

中间采用了防止溢出的写法,为什么会这样的写法能一定程度上防止溢出呢?

模板中的 left 和 right 都是 int 类型,正常情况下是 mid 等于 left 和 right 两个数先相加再除2,而如果 left + right 这个算式的中间产物已经超出了 int 的最大范围,那么mid的数据也就会出问题了。而如果采用了模板中的写法,则不存在数值增大的中间产物。当然其实第一眼最难理解的还是为什么模板中的写法和一般的写法等价。

这其实是个数学问题,就直接上算式了。

首先,将 (left + right) / 2 按照乘法分配律展开,得到:

left+right2=12left+12right\frac{left + right}{2} = \frac{1}{2}left + \frac{1}{2}right

然后,将 right 减去 left,得到:

left+right2=12left+12(rightleft)+left12left\frac{left + right}{2} = \frac{1}{2}left + \frac{1}{2}(right - left) + left - \frac{1}{2}left

进一步化简,得到:

left+right2=left+12(rightleft)\frac{left + right}{2} = left + \frac{1}{2}(right - left)

当然,这样的防溢出写法比原来的写法多了一步计算,可能会稍微降低一点性能。

比如 JDK 的 Arrays.sort 中就有这么一行代码:

 int mid = (left + right) >>> 1;

由于使用了无符号右移运算符 >>>,一定程度上也能防止计算 mid 的过程中 left 和 right 相加后的结果溢出。