Rust 练习册 32:二分查找与算法实现艺术

在计算机科学中,搜索算法是最基础也是最重要的算法之一。二分查找(Binary Search)作为一种高效的搜索算法,在有序数组中查找特定元素时具有 O(log n) 的时间复杂度,远优于线性查找的 O(n)。在 Exercism 的 "binary-search" 练习中,我们将实现这个经典的算法,这不仅能帮助我们掌握算法设计的基本思想,还能深入学习 Rust 中的数组处理和错误处理技巧。

什么是二分查找?

二分查找是一种在有序数组中查找特定元素的搜索算法。它的工作原理如下:

  1. 首先比较数组中间元素与目标值
  2. 如果中间元素等于目标值,则查找成功
  3. 如果目标值小于中间元素,则在数组左半部分继续查找
  4. 如果目标值大于中间元素,则在数组右半部分继续查找
  5. 重复以上步骤,直到找到目标值或搜索范围为空

让我们先看看练习提供的函数签名:

rust 复制代码
pub fn find(array: &[i32], key: i32) -> Option<usize> {
    unimplemented!(
        "Using the binary search algorithm, find the element '{}' in the array '{:?}' and return its index.",
        key,
        array
    );
}

我们需要实现这个函数,它应该:

  1. 在有序数组中查找指定元素
  2. 如果找到,返回元素的索引
  3. 如果未找到,返回 None

算法实现

1. 基础实现

rust 复制代码
pub fn find(array: &[i32], key: i32) -> Option<usize> {
    let mut left = 0;
    let mut right = array.len();
    
    while left < right {
        let mid = left + (right - left) / 2;
        
        match array[mid].cmp(&key) {
            std::cmp::Ordering::Equal => return Some(mid),
            std::cmp::Ordering::Greater => right = mid,
            std::cmp::Ordering::Less => left = mid + 1,
        }
    }
    
    None
}

这个实现的关键点:

  • 使用左闭右开区间 [left, right)
  • 通过 cmp 方法进行三路比较
  • 避免整数溢出:使用 left + (right - left) / 2 而不是 (left + right) / 2

2. 递归实现

rust 复制代码
pub fn find(array: &[i32], key: i32) -> Option<usize> {
    binary_search_recursive(array, key, 0, array.len())
}

fn binary_search_recursive(array: &[i32], key: i32, left: usize, right: usize) -> Option<usize> {
    if left >= right {
        return None;
    }
    
    let mid = left + (right - left) / 2;
    
    match array[mid].cmp(&key) {
        std::cmp::Ordering::Equal => Some(mid),
        std::cmp::Ordering::Greater => binary_search_recursive(array, key, left, mid),
        std::cmp::Ordering::Less => binary_search_recursive(array, key, mid + 1, right),
    }
}

递归版本更直观地体现了分治思想。

测试用例分析

通过查看测试用例,我们可以更好地理解需求:

rust 复制代码
#[test]
fn finds_a_value_in_an_array_with_one_element() {
    assert_eq!(find(&[6], 6), Some(0));
}

最简单的成功情况。

rust 复制代码
#[test]
fn finds_a_value_in_the_middle_of_an_array() {
    assert_eq!(find(&[1, 3, 4, 6, 8, 9, 11], 6), Some(3));
}

在中间位置找到元素。

rust 复制代码
#[test]
fn identifies_that_a_value_is_not_included_in_the_array() {
    assert_eq!(find(&[1, 3, 4, 6, 8, 9, 11], 7), None);
}

查找不存在的元素。

rust 复制代码
#[test]
fn nothing_is_included_in_an_empty_array() {
    assert_eq!(find(&[], 1), None);
}

空数组的边界情况。

rust 复制代码
#[test]
#[ignore]
#[cfg(feature = "generic")]
fn works_for_str_elements() {
    assert_eq!(find(["a"], "a"), Some(0));
    assert_eq!(find(["a", "b"], "b"), Some(1));
}

泛型支持的测试用例。

泛型实现

为了支持不同类型的元素,我们可以实现泛型版本:

rust 复制代码
pub fn find<T: Ord>(array: &[T], key: T) -> Option<usize> {
    let mut left = 0;
    let mut right = array.len();
    
    while left < right {
        let mid = left + (right - left) / 2;
        
        match array[mid].cmp(&key) {
            std::cmp::Ordering::Equal => return Some(mid),
            std::cmp::Ordering::Greater => right = mid,
            std::cmp::Ordering::Less => left = mid + 1,
        }
    }
    
    None
}

通过添加 T: Ord 约束,我们的函数可以用于任何实现了 Ord trait 的类型。

使用标准库实现

Rust 标准库已经提供了二分查找功能,我们可以直接使用:

rust 复制代码
pub fn find(array: &[i32], key: i32) -> Option<usize> {
    array.binary_search(&key).ok()
}

对于泛型版本:

rust 复制代码
use std::cmp::Ord;

pub fn find<T: Ord>(array: &[T], key: T) -> Option<usize> {
    array.binary_search(&key).ok()
}

性能优化版本

考虑边界条件的优化实现:

rust 复制代码
pub fn find(array: &[i32], key: i32) -> Option<usize> {
    // 空数组的快速处理
    if array.is_empty() {
        return None;
    }
    
    let mut left = 0;
    let mut right = array.len() - 1;
    
    // 提前检查边界值
    if array[left] > key || array[right] < key {
        return None;
    }
    
    // 使用闭区间 [left, right]
    while left <= right {
        let mid = left + (right - left) / 2;
        
        match array[mid].cmp(&key) {
            std::cmp::Ordering::Equal => return Some(mid),
            std::cmp::Ordering::Greater => {
                if mid == 0 {
                    return None; // 防止下溢
                }
                right = mid - 1;
            },
            std::cmp::Ordering::Less => left = mid + 1,
        }
    }
    
    None
}

错误处理和边界情况

在实际应用中,我们需要考虑更多边界情况:

rust 复制代码
pub fn find(array: &[i32], key: i32) -> Option<usize> {
    // 处理空数组
    if array.is_empty() {
        return None;
    }
    
    let mut left = 0;
    let mut right = array.len();
    
    // 使用标准的左闭右开区间实现
    while left < right {
        // 使用安全的中点计算防止溢出
        let mid = left + (right - left) / 2;
        
        match array[mid].cmp(&key) {
            std::cmp::Ordering::Equal => return Some(mid),
            std::cmp::Ordering::Greater => right = mid,
            std::cmp::Ordering::Less => left = mid + 1,
        }
    }
    
    None
}

扩展功能

基于基础实现,我们可以添加更多功能:

rust 复制代码
pub struct BinarySearch;

impl BinarySearch {
    /// 标准二分查找
    pub fn find<T: Ord>(array: &[T], key: &T) -> Option<usize> {
        let mut left = 0;
        let mut right = array.len();
        
        while left < right {
            let mid = left + (right - left) / 2;
            
            match array[mid].cmp(key) {
                std::cmp::Ordering::Equal => return Some(mid),
                std::cmp::Ordering::Greater => right = mid,
                std::cmp::Ordering::Less => left = mid + 1,
            }
        }
        
        None
    }
    
    /// 查找第一个大于等于 key 的元素位置
    pub fn lower_bound<T: Ord>(array: &[T], key: &T) -> usize {
        let mut left = 0;
        let mut right = array.len();
        
        while left < right {
            let mid = left + (right - left) / 2;
            
            if array[mid] < *key {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        left
    }
    
    /// 查找第一个大于 key 的元素位置
    pub fn upper_bound<T: Ord>(array: &[T], key: &T) -> usize {
        let mut left = 0;
        let mut right = array.len();
        
        while left < right {
            let mid = left + (right - left) / 2;
            
            if array[mid] <= *key {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        left
    }
    
    /// 查找元素的所有出现位置
    pub fn find_all<T: Ord>(array: &[T], key: &T) -> Vec<usize> {
        let lower = Self::lower_bound(array, key);
        let upper = Self::upper_bound(array, key);
        
        (lower..upper).collect()
    }
}

算法复杂度分析

二分查找的复杂度分析:

  1. 时间复杂度:O(log n)

    • 每次迭代都将搜索空间减半
    • 最多需要 log₂(n) 次比较
  2. 空间复杂度

    • 迭代版本:O(1)
    • 递归版本:O(log n)(递归调用栈)

实际应用场景

二分查找在实际开发中有广泛的应用:

  1. 数据库索引:B+树等数据结构的基础
  2. 版本控制:Git 中查找特定提交
  3. 游戏开发:在有序列表中查找资源
  4. 搜索引擎:在倒排索引中查找文档
  5. 数值计算:求解单调函数的根

与其他搜索算法的比较

rust 复制代码
// 线性查找 - O(n)
pub fn linear_search<T: PartialEq>(array: &[T], key: &T) -> Option<usize> {
    for (i, item) in array.iter().enumerate() {
        if item == key {
            return Some(i);
        }
    }
    None
}

// 二分查找 - O(log n)
pub fn binary_search<T: Ord>(array: &[T], key: &T) -> Option<usize> {
    let mut left = 0;
    let mut right = array.len();
    
    while left < right {
        let mid = left + (right - left) / 2;
        
        match array[mid].cmp(key) {
            std::cmp::Ordering::Equal => return Some(mid),
            std::cmp::Ordering::Greater => right = mid,
            std::cmp::Ordering::Less => left = mid + 1,
        }
    }
    
    None
}

对于大型有序数据集,二分查找的优势非常明显。

总结

通过 binary-search 练习,我们学到了:

  1. 算法设计:掌握了二分查找的核心思想和实现技巧
  2. 边界处理:学会了如何正确处理各种边界情况
  3. 泛型编程:理解了如何编写通用的算法实现
  4. 性能优化:了解了防止整数溢出等优化技巧
  5. 标准库使用:熟悉了 Rust 标准库提供的相关功能
  6. 扩展功能:学会了实现 lower_bound、upper_bound 等变种算法

这些技能在实际开发中非常有用,特别是在处理有序数据、实现高效搜索和进行算法优化时。二分查找作为一个经典算法,涉及到了算法设计的许多核心概念,是学习 Rust 算法实现的良好起点。

通过这个练习,我们也看到了 Rust 在算法实现方面的强大能力,以及如何用安全且高效的方式表达算法逻辑。这种结合了安全性和性能的语言特性正是 Rust 的魅力所在。

相关推荐
程序员-King.2 小时前
day158—回溯—全排列(LeetCode-46)
算法·leetcode·深度优先·回溯·递归
星火开发设计2 小时前
C++ 数组:一维数组的定义、遍历与常见操作
java·开发语言·数据结构·c++·学习·数组·知识
月挽清风3 小时前
代码随想录第七天:
数据结构·c++·算法
TTGGGFF3 小时前
控制系统建模仿真(一):掌握控制系统设计的 MAD 流程与 MATLAB 基础运算
开发语言·matlab
小O的算法实验室3 小时前
2026年AEI SCI1区TOP,基于改进 IRRT*-D* 算法的森林火灾救援场景下直升机轨迹规划,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
2501_944424123 小时前
Flutter for OpenHarmony游戏集合App实战之贪吃蛇食物生成
android·开发语言·flutter·游戏·harmonyos
小郭团队3 小时前
2_1_七段式SVPWM (经典算法)算法理论与 MATLAB 实现详解
嵌入式硬件·算法·硬件架构·arm·dsp开发
充值修改昵称4 小时前
数据结构基础:从二叉树到多叉树数据结构进阶
数据结构·python·算法
Deepoch4 小时前
Deepoc数学大模型:发动机行业的算法引擎
人工智能·算法·机器人·发动机·deepoc·发动机行业
Lhuu(重开版4 小时前
JS:正则表达式和作用域
开发语言·javascript·正则表达式