09月04, 2019

二分法查询算法 Python 实现

1. 前提

二分法查询,要求目标集合必须是有序的,无序集合需要先进行排序。 二分法查询的原理一言蔽之“一次缩小一半的搜索范围”。

理解算法原理和掌握算法是不一样的,动手实现才是最终的检验标准。

一起逐步实现一个二分法查找函数 binary_search 吧。 二分法查找需要两个参数:

  • 目标数组,命名为 theList,不能叫 list,会与 python 内置的 list 命名冲突。
  • 要查找的值,命名为 target。

2. 一轮查找

基于循环的二分法查找,实际上是不断的调用这样的过程:确认查找范围中点与目标值之间的关系。实现二分法查找的第一步,实现一个简单的功能,确定目标值 target 与 查找范围中点 mid 之间的关系是下面哪一种:

  • target == mid
  • target 在 mid 左侧
  • target 在 mid 右侧

当然,如果查找范围中已经没有内容可查,就意味着数组中不存在 target,这个特殊的边界条件,决定的是循环何时结束,我们最后再讨论。

2.1 初始化

title

首先我们指定查找范围,从 left 开始,到 right(包含)结束。
最开始查找范围当然是整个数组,特别注意,数组的最大索引是 len - 1

lenth = len(theList)
left = 0
right = length - 1

2.2 计算中点索引

title

计算中点的索引 midIndex。一种比较自然的方式是: left + (right - left)/2 或者采用下面的方式,注意 python 里的除法结果是浮点数,也就是浮点数,就算小数部分为 0, 结果也是浮点数,比如 2.0。使用 int() 将 (left + right)/2 转化为整数,采用的是向下取整————直接丢弃小数部分。

midIndex = int((left + right)/2)

找到中点索引 midIndex 的目标是访问 mid 的值。

2.3 当中点就是目标值 target 的情况

title

为了后面编写代码方便,我们把 midIndex 位置的值保存在 midValue 中。

如果目标 target 与 midValue 相等,也就是找到了目标,函数直接返回中间点的索引 midIndex 就可以退出函数了。

midValue = theList[midIndex]
if midValue == target:
    return midIndex

2.4 当目标值大于中间值

也就是说,目标值 target 在中间值右侧部分。 直接调整 left 到中间索引的下一个位置,下一轮就可以在中间点右侧区域查找 target 了。

title

if target > midValue:
    left = midIndex + 1
    # 跳转到 2.2 步骤

然后要开始下一轮循环。 可能会用到 continue,这样就可以不用 elif 语句了,完整代码中有演示。

2.5 当目标值小于中间值

即目标值 target 在中间值左侧部分。类似上一种情况,直接调整 right 到中间索引的前一位置,下一轮查找就会在中间点左侧区域查找 target 了。

title

if target < midValue:
    right = midIndex - 1
    # 跳转到 2.2 步骤

我们同样需要进入下一轮搜索,但是因为这是最后一种情况了,即使什么都不做,代码也会进入下一轮循环。

3. 终止条件

循环结束的可能有两种:

  • 找到 target,返回 target 的索引
  • 里里外外都找了,但是就是没有 target, 返回 None

第一种情况,在 2.3 步骤已经实现了。

第二种情况,需要在循环控制条件中实现。

title

可以想象, 从 left --> right 是搜索范围,left 不断的向右移动, right 不断向左移动, 如果一直找不到 target,最终 left 会跑到 right 右侧,也就意味着已经没有可以查找区间了,就应当结束循环。

# 只要 left 没有跑到 right 右侧,就继续下一轮查找
while right >= left:
    # loop

4. 完整代码

# theList 是要查找的列表
# target 是要查找的元素
# 如果在 theList 中找到 target 值,返回target所在的索引
# 没找到则返回 None
def binary_search(theList, target):
    # 设置初始检索范围, 0 ~ 最大索引, 初始化检索区域左侧索引 left 为 0,从头开始检索
    left = 0
    # 初始化检索区域右侧索引 right 为 theList 的最大索引,也就是 len(theList) - 1
    right = len(theList) - 1


    # 只要 右侧索引大于等于左侧索引,也就是检索区域还有宽度,就执行循环
    while right >= left:
        # 计算区域中间元素的索引,保存在 minIndex 变量
        midIndex = int((left + right)/2)
        # 读取 theList 中间索引对应的值 到 mid 变量
        mid = theList[midIndex]

        # 如果 target 等于目标值,找到目标
        if mid == target:
            # 返回中间索引
            return midIndex
        # 否则 如果 target 大于中间值,也就是目标在中间值的 右侧
        elif target > mid:
            # 更新检索区域的左侧索引为中间索引的下一位置 
            left = midIndex + 1
        # 否则,target 小于中间值,目标值在中间值的左侧
        else:
            # 更新检索区域的右侧索引为中间索引的前一位置
            right = midIndex - 1

    # 如果运行到这里,也就是没有在循环中返回,即没有找到 target,返回 None
    return None

如果能利用 continue,代码会更整洁

# theList 是要查找的列表
# target 是要查找的元素
# 如果在 theList 中找到 target 值,返回target所在的索引
# 没找到则返回 None
def binary_search(theList, target):
    # 设置初始检索范围, 0 ~ 最大索引, 初始化检索区域左侧索引 left 为 0,从头开始检索
    left = 0
    # 初始化检索区域右侧索引 right 为 theList 的最大索引,也就是 len(theList) - 1
    right = len(theList) - 1


    # 只要 右侧索引大于等于左侧索引,也就是检索区域还有宽度,就执行循环
    while right >= left:
        # 计算区域中间元素的索引,保存在 minIndex 变量
        midIndex = int((left + right)/2)
        # 读取 theList 中间索引对应的值 到 mid 变量
        mid = theList[midIndex]

        # 如果 target 等于目标值,找到目标
        if mid == target:
            # 返回中间索引
            return midIndex

        # 否则 如果 target 大于中间值,也就是目标在中间值的 右侧
        if target > mid:
            # 更新检索区域的左侧索引为中间索引的下一位置 
            left = midIndex + 1
            continue

        # 否则,target 小于中间值,目标值在中间值的左侧
        # 更新检索区域的右侧索引为中间索引的前一位置
        right = midIndex - 1

    # 如果运行到这里,也就是没有在循环中返回,即没有找到 target,返回 None
    return None

可以用下面的代码测试

# 可以用下面的list 测试自己的函数对不对
testData = [3,5,8,15,30]

#在 testData 中查找 5
index = binary_search(testData, 5)

print(index)  # 应该输出 1

#在 testData 中查找 15
index = binary_search(testData, 15)

print(index)  # 应该输出 3

#在 testData 中查找 9
index = binary_search(testData, 9)

print(index)  # 应该输出 None

本文链接:http://www.thinkinpython.com/post/binary_search_python_version.html

-- EOF --