目录
前言
在实际场景中,选择合适的查找算法对于提高程序的效率和性能至关重要,本节课主要讲解"二分查找"的适用场景及代码实现。
二分查找
二分查找(Binary Search)是一种在有序数组中查找特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且根据二分查找的性质,调整查找范围的上界或下界,重复这个过程直到找到要查找的元素,或者范围为空。
代码示例
下面我们使用Go语言实现一个二分查找
1. 算法包
创建一个 pkg/search.go
touch pkg/search.go
(如果看过上节课的线性查找,则已存在该文件,我们就不需要再创建了)
2. 二分查找代码
打开 pkg/search.go 文件,代码如下
package pkg
import "fmt"
// LinearSearch 线性查找
...
// BinarySearch 二分查找
func BinarySearch(arr []int, target int) int {
left, right := 0, len(arr)-1
for left <= right {
mid := left + (right-left)/2 // 防止溢出
if arr[mid] == target {
return mid
} else if arr[mid] < target {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
3. 模拟程序
打开 main.go 文件,代码如下:
package main
import (
"demo/pkg"
"fmt"
)
func main() {
// 定义一个切片(数组),这里我们模拟 10 个元素
arr := []int{7, 16, 23, 36, 41, 58, 61, 79, 86, 99}
fmt.Println("arr的长度:", len(arr))
fmt.Println("arr的值为:", arr)
target := 16
index := pkg.BinarySearch(arr, target)
if index != -1 {
fmt.Printf("找到目标值 %d , 在索引 %d \n", target, index)
} else {
fmt.Printf("没有找到目标值 %d \n", target)
}
}
4. 运行程序
go run main.go
在我们定义的切片(数组)第二个就是我们的目标值:16,并且找到了索引值为 1
那么二分查找法,是循环了多少次找到目标值的呢,我们接下来做一个循环测试
循环次数
假如目标值正好在数组中的第1位
循环次数 3 次
假如目标值正好在数组中的第5位
循环次数 1 次
假如目标值正好在数组中的第9位
循环次数 3 次
假如目标值不在数组中
循环次数 4 次
假如有 1000 条数据
因为上面的几次,都只有 10 条数据,目前最高的循环次数只有 4 次,那么我们加到 1000 条数据,进行测试
假如是 1000 条数据中的最后一位
循环次数 10 次
假如是 1000 条数据中的第 750 位
循环次数 2 次
...
可以发现,1000 条数据,找到最末尾的一个时,最多循环10次,而找中间的只用到了 2 次。
是因为二分查找的基本思想是:通过不断地将数组分成两半,并判断目标值可能存在的区间,从而逐步缩小搜索范围,直到找到目标值或明确目标值不存在数组中。
附:如果数组不是有序的
二分查找的适用场景是需要 有序数组,那么如果我们的数组不是有序的,使用二分查找会如何呢?
定义一个不是有序的数组
[]int{408, 902, 757, 859, 382, 353, 964, 473, 392, 369}
可以看到,408之后是902,902是比408大的,是没问题的,但下一位是757,就比902小了,这样的数组并不是排序好的,我们来进行测试
可以看到,我们循环了 4次,也就是10条数据中,循环的最大次数,而显示的是:没有找到 757
之所以找不到,是因为二分查找第一次切割,找到中间的索引为 4,值是 382,而 382 是小于 757 的,所以就向右半部分继续切割,但我们能看到,757 这个值,是在左半部分,所以在这里就已经不可能会找到 757 了。
实现逻辑详细说明
1. 初始化
- 首先,确定搜索范围的左右边界,即 left 为 0(数组起始位置),right 为 len(arr) - 1(数组末尾位置)
2. 循环条件
- 当 left <= right 时,表示搜索范围内还可能有目标值,继续搜索;否则,表示搜索范围内已不包含目标值,退出循环
3. 计算中间位置
- 使用 mid := left + (right - left) / 2 来计算中间位置,这样可以防止 left + right 直接相加时可能发生的整数溢出问题
4. 判断中间元素与目标值的关系
- 如果 arr[mid] == target,说明找到了目标值,直接返回 mid
- 如果 arr[mid] < target,说明目标值在当前中间位置的右侧(因为数组是有序的),所以更新左边界 left = mid + 1,继续在右半部分搜索
- 如果 arr[mid] > target,说明目标值在当前中间位置的左侧,所以更新右边界 right = mid - 1,继续在左半部分搜索
5. 结束循环
- 如果循环结束时仍未找到目标值(即 left > right ),则说明目标值不在数组中,返回 -1
二分查找的适用场景
二分查找算法因其高效的搜索效率(时间复杂度为 O(log n))而适用于以下场景
1. 有序组数
二分查找的前提是数据已经有序排列。如果数据未排序,则需要先对数据进行排序,这可能会增加额外的计算开销
2. 数据量大
当数据量非常大时,使用二分查找可以显著减少搜索时间,相比线性搜索(O(n)时间复杂度)更有效率
3. 静态数据集
二分查找通常用于静态数据集或至少数据集在搜索期间不会更改的场景。如果数据集频繁更改(如插入或删除操作),则可能需要额外的数据结构(如平衡二叉搜索树)来维护数据的有序性
4. 精确匹配
当需要精确匹配某个元素时,二分查找非常适用。如果需要模糊匹配或查找最近似元素,则可能需要不同的算法或数据结构
5. 范围查询
虽然标准的二分查找算法是查找单个元素,但可以通过修改算法来查找在某个范围内的所有元素。这种扩展后的二分查找可以用于处理范围查询问题