Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第十五 关系运算

关系运算(Relational Operation)用于比较两个值之间的大小关系,返回一个布尔值(truefalse)。它是条件判断和程序控制流的基础,也是排序、搜索等算法的核心操作。


一、Rust 中的关系运算符

1.1 六种关系运算符

Rust 提供了六种关系运算符,用于比较两个值的大小关系:

运算符含义数学符号示例结果
==等于=5 == 5true
!=不等于5 != 3true
>大于>5 > 3true
>=大于等于5 >= 5true
<小于<3 < 5true
<=小于等于3 <= 5true
fn main() {
    let a = 10;
    let b = 20;

    println!("a == b: {}", a == b);  // false
    println!("a != b: {}", a != b);  // true
    println!("a > b:  {}", a > b);   // false
    println!("a >= b: {}", a >= b);  // false
    println!("a < b:  {}", a < b);   // true
    println!("a <= b: {}", a <= b);  // true
}

1.2 关系运算的优先级

关系运算符的优先级低于算术运算符,高于逻辑运算符:

算术运算符(+ - * / %)> 关系运算符(> >= < <= == !=)> 逻辑运算符(&& ||)
fn main() {
    let x = 3 + 5;       // 先算术:x = 8
    let result = x > 5 && x < 15;  // 再关系:8 > 5 为 true,8 < 15 为 true
    println!("{}", result);  // true

    // 等价于:
    let result2 = (3 + 5) > 5 && (3 + 5) < 15;
    println!("{}", result2);  // true
}

1.3 关系运算符的返回类型

关系运算符的返回值始终是 bool 类型:

fn main() {
    let is_equal: bool = 10 == 10;
    let is_greater: bool = 10 > 5;

    println!("is_equal = {}", is_equal);    // true
    println!("is_greater = {}", is_greater); // true

    // bool 类型可以参与逻辑运算
    let combined = is_equal && is_greater;
    println!("combined = {}", combined);   // true
}

二、PartialOrd 与 Ord Trait

2.1 为什么需要 PartialOrd

在 Rust 的类型系统中,并非所有类型都能进行完整的比较。例如,浮点数中存在 NaN(Not a Number),NaN 与任何值的比较都没有意义。因此,Rust 将比较能力分为两个层次:

Trait含义适用类型
PartialOrd部分有序整数、浮点数、字符串等(允许不可比较的情况)
Ord完全有序整数、字符串等(任意两个值都可以比较)

2.2 PartialOrd Trait

PartialOrd 定义了 partial_cmp 方法,它返回 Option<Ordering>,而不是直接返回 Ordering

use std::cmp::Ordering;

fn main() {
    let a = 3.14;
    let b = 2.71;

    // partial_cmp 返回 Option<Ordering>
    match a.partial_cmp(&b) {
        Some(Ordering::Less) => println!("{} < {}", a, b),
        Some(Ordering::Equal) => println!("{} == {}", a, b),
        Some(Ordering::Greater) => println!("{} > {}", a, b),
        None => println!("无法比较"),
    }
}

Ordering 枚举的三种取值:

取值含义说明
Ordering::Less小于第一个值小于第二个值
Ordering::Equal等于两个值相等
Ordering::Greater大于第一个值大于第二个值

2.3 NaN 的比较问题

NaNPartialOrd 但不是 Ord 的典型例子:

use std::cmp::Ordering;

fn main() {
    let nan = f64::NAN;

    // NaN 与任何值的比较都返回 None
    println!("NaN > 1.0: {:?}", nan.partial_cmp(&1.0));   // None
    println!("NaN < 1.0: {:?}", nan.partial_cmp(&1.0));   // None
    println!("NaN == NaN: {:?}", nan.partial_cmp(&nan));  // None

    // 使用 == 运算符,NaN != NaN 也是 true
    println!("NaN == NaN: {}", nan == nan);  // false
    println!("NaN != NaN: {}", nan != nan);  // true

    // 这就是为什么浮点数只实现了 PartialOrd,没有实现 Ord
}

重要: NaN 是唯一一个“不等于自身“的值,即 NaN != NaNtrue。这是 IEEE 754 浮点数标准的规定。

2.4 Ord Trait

Ord trait 表示完全有序,任意两个值之间一定有确定的大小关系。它定义了 cmp 方法,返回 Ordering(不是 Option<Ordering>):

use std::cmp::Ordering;

fn main() {
    let a = 42;
    let b = 17;

    // cmp 返回 Ordering(不是 Option)
    match a.cmp(&b) {
        Ordering::Less => println!("{} < {}", a, b),
        Ordering::Equal => println!("{} == {}", a, b),
        Ordering::Greater => println!("{} > {}", a, b),
    }
}

实现了 Ord 的常用类型:

类型说明
i8, i16, i32, i64, i128, isize有符号整数
u8, u16, u32, u64, u128, usize无符号整数
char字符(按 Unicode 码点比较)
String, &str字符串(按字典序比较)
bool布尔值(false < true
Duration时间段

2.5 为自定义类型实现 Ord

use std::cmp::Ordering;

#[derive(Debug)]
struct Student {
    name: String,
    score: u32,
}

// 实现 PartialOrd
impl PartialOrd for Student {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.score.cmp(&other.score))
    }
}

// 实现 PartialEq(Ord 的前提)
impl PartialEq for Student {
    fn eq(&self, other: &Self) -> bool {
        self.score == other.score
    }
}

// 实现 Ord
impl Ord for Student {
    fn cmp(&self, other: &Self) -> Ordering {
        self.score.cmp(&other.score)
    }
}

fn main() {
    let alice = Student { name: "Alice".into(), score: 95 };
    let bob = Student { name: "Bob".into(), score: 87 };

    println!("Alice vs Bob: {:?}", alice.cmp(&bob));  // Greater

    // 可以直接排序
    let mut students = vec![
        Student { name: "Charlie".into(), score: 72 },
        Student { name: "Alice".into(), score: 95 },
        Student { name: "Bob".into(), score: 87 },
    ];
    students.sort();
    for s in &students {
        println!("{}: {}", s.name, s.score);
    }
    // Charlie: 72
    // Bob: 87
    // Alice: 95
}

提示: 在实际开发中,通常使用 #[derive(PartialEq, PartialOrd, Eq, Ord)] 自动派生,前提是所有字段都实现了 Ord


三、浮点数比较的陷阱

3.1 浮点数精度问题

浮点数在计算机中采用 IEEE 754 标准存储,存在精度限制。许多看似简单的十进制小数无法精确表示为二进制浮点数:

fn main() {
    // 0.1 + 0.2 在数学上等于 0.3
    // 但在浮点数中并不相等!
    let sum = 0.1_f64 + 0.2_f64;
    println!("0.1 + 0.2 = {:.20}", sum);       // 0.30000000000000004441
    println!("sum == 0.3: {}", sum == 0.3_f64); // false
}

原因: 0.1 在二进制中是一个无限循环小数(类似十进制中的 1/3 = 0.333…),因此无法精确存储。

3.2 正确的浮点数比较方法

方法一:使用误差范围(epsilon)

fn approx_equal(a: f64, b: f64, epsilon: f64) -> bool {
    (a - b).abs() < epsilon
}

fn main() {
    let a = 0.1_f64 + 0.2_f64;
    let b = 0.3_f64;

    println!("直接比较: a == b → {}", a == b);                          // false
    println!("误差比较: approx_equal(a, b, 1e-10) → {}", approx_equal(a, b, 1e-10)); // true
}

方法二:使用相对误差

当数值很大时,固定误差范围可能不够用,此时应使用相对误差:

fn approx_equal_relative(a: f64, b: f64, max_relative_error: f64) -> bool {
    let abs_diff = (a - b).abs();
    let largest = a.abs().max(b.abs());

    // 处理两个值都接近 0 的情况
    if largest < 1e-10 {
        abs_diff < 1e-10
    } else {
        abs_diff / largest < max_relative_error
    }
}

fn main() {
    let a = 1000000.00001_f64;
    let b = 1000000.00002_f64;

    println!("相对误差比较: {}", approx_equal_relative(a, b, 1e-10)); // true
}

方法三:使用 assert_approx_eq 宏(测试中常用)

// 简单的近似相等断言宏
macro_rules! assert_approx_eq {
    ($a:expr, $b:expr, $eps:expr) => {
        let a = $a as f64;
        let b = $b as f64;
        let diff = (a - b).abs();
        assert!(
            diff < $eps,
            "断言失败: |{} - {}| = {} >= {}",
            a, b, diff, $eps
        );
    };
}

fn main() {
    let result = 0.1_f64 + 0.2_f64;
    assert_approx_eq!(result, 0.3, 1e-10);
    println!("测试通过!");
}

3.3 浮点数排序中的 NaN 处理

由于 NaN 无法参与正常比较,排序时需要特殊处理:

use std::cmp::Ordering;

fn main() {
    let mut data = vec![3.5, f64::NAN, 1.2, f64::NAN, 2.8, 0.5];

    // 方法一:分离 NaN 后排序
    let mut numbers: Vec<f64> = data.iter().cloned().filter(|&x| !x.is_nan()).collect();
    let nans: Vec<f64> = data.iter().cloned().filter(|&x| x.is_nan()).collect();

    numbers.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));

    let sorted = [numbers, nans].concat();
    println!("排序结果: {:?}", sorted);
    // [0.5, 1.2, 2.8, 3.5, NaN, NaN]

    // 方法二:使用 total_cmp(Rust 1.62+,将 NaN 排在最后)
    let mut data2 = vec![3.5, f64::NAN, 1.2, f64::NAN, 2.8, 0.5];
    data2.sort_by(f64::total_cmp);
    println!("total_cmp 排序: {:?}", data2);
    // [0.5, 1.2, 2.8, 3.5, NaN, NaN]
}

提示: Rust 1.62 引入了 f64::total_cmpf32::total_cmp,提供了浮点数的完全有序比较,NaN 被视为大于所有非 NaN 值。

3.4 特殊浮点值的比较

fn main() {
    let inf = f64::INFINITY;
    let neg_inf = f64::NEG_INFINITY;
    let nan = f64::NAN;

    // 正无穷
    println!("INFINITY > 1000: {}", inf > 1000.0);        // true
    println!("INFINITY == INFINITY: {}", inf == inf);      // true

    // 负无穷
    println!("NEG_INFINITY < -1000: {}", neg_inf < -1000.0); // true

    // NaN 的特殊性
    println!("NaN > 0:   {}", nan > 0.0);    // false
    println!("NaN < 0:   {}", nan < 0.0);    // false
    println!("NaN == 0:  {}", nan == 0.0);   // false
    println!("NaN != NaN: {}", nan != nan);   // true(唯一不等于自身的值)

    // 判断是否为特殊值
    println!("NaN.is_nan(): {}", nan.is_nan());             // true
    println!("INFINITY.is_infinite(): {}", inf.is_infinite()); // true
    println!("0.0.is_finite(): {}", 0.0_f64.is_finite());   // true
}

四、不同类型的比较

4.1 整数比较

整数比较是精确的,不存在精度问题:

fn main() {
    let a: i32 = 42;
    let b: i32 = -17;

    println!("a > b:  {}", a > b);   // true
    println!("a == b: {}", a == b);  // false

    // 不同大小的整数可以比较(Rust 会自动转换)
    let small: u8 = 10;
    let large: i32 = 100;
    // println!("{}", small > large);  // 错误!类型不同不能直接比较

    // 需要显式转换
    println!("{}", (small as i32) > large);  // false
}

4.2 字符比较

字符按 Unicode 码点进行比较:

fn main() {
    let a = 'A';
    let b = 'a';
    let c = '中';

    println!("'A' < 'a': {}", a < b);  // true(大写字母的码点小于小写字母)
    println!("'A' 码点: {}", a as u32);  // 65
    println!("'a' 码点: {}", b as u32);  // 97
    println!("'中' 码点: {}", c as u32);  // 20013

    // 字符可以排序
    let mut chars = vec!['z', 'A', '中', 'a', '0'];
    chars.sort();
    println!("排序结果: {:?}", chars);
    // ['0', 'A', 'a', 'z', '中']
}

4.3 字符串比较

字符串按字典序(lexicographical order)比较:

fn main() {
    let a = "apple";
    let b = "banana";
    let c = "Apple";

    println!("\"apple\" < \"banana\": {}", a < b);   // true
    println!("\"Apple\" < \"apple\": {}", c < a);     // true(大写字母码点更小)

    // 中文字符串也按 Unicode 码点比较
    let d = "北京";
    let e = "上海";
    println!("\"北京\" < \"上海\": {}", d < e);  // true

    // 比较前缀
    let prefix = "hello world";
    println!("\"hello\" < \"hello world\": {}", "hello" < prefix);  // true(短字符串更小)
}

4.4 元组和数组比较

元组和数组支持字典序比较(前提是元素类型可比较):

fn main() {
    // 元组比较:先比较第一个元素,相同则比较第二个
    let t1 = (1, 2);
    let t2 = (1, 3);
    let t3 = (2, 1);
    println!("(1,2) < (1,3): {}", t1 < t2);  // true
    println!("(1,2) < (2,1): {}", t1 < t3);  // true

    // 数组比较:逐元素比较
    let arr1 = [1, 2, 3];
    let arr2 = [1, 2, 4];
    println!("{:?} < {:?}: {}", arr1, arr2, arr1 < arr2);  // true

    // 不同长度的数组不能直接比较
    let arr3 = [1, 2];
    // println!("{}", arr3 < arr1);  // 错误!长度不同
}

五、关系运算的数学应用

5.1 比较大小

数学中经常需要比较两个表达式的大小,Rust 可以帮助我们快速验证:

题目1: 比较 $85 \times 85$ 与 $84 \times 86$ 的大小

fn main() {
    let a = 85 * 85;
    let b = 84 * 86;
    println!("85×85 = {}", a);  // 7225
    println!("84×86 = {}", b);  // 7216
    println!("85×85 {} 84×86", if a > b { ">" } else if a < b { "<" } else { "=" });
    // 85×85 > 84×86
}

数学原理: 设 $a = n \times n$,$b = (n-1)(n+1) = n^2 - 1$,所以 $a > b$。

题目2: 比较 $0.1e^{0.1}$、$1/9$、$-\ln 0.9$ 的大小

use std::f64::consts::E;

fn main() {
    let a = 0.1 * E.powf(0.1);
    let b = 1.0_f64 / 9.0_f64;
    let c = -(0.9_f64).ln();

    println!("0.1·e^0.1  = {:.15}", a);
    println!("1/9        = {:.15}", b);
    println!("-ln(0.9)   = {:.15}", c);

    // 使用 total_cmp 进行浮点数比较
    let mut values = [("0.1·e^0.1", a), ("1/9", b), ("-ln(0.9)", c)];
    values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

    for (name, val) in &values {
        println!("{} = {:.15}", name, val);
    }
}

题目3: 比较 $a = \frac{31}{32}$、$b = \cos(\frac{1}{4})$、$c = 4\sin(\frac{1}{4})$ 的大小

fn main() {
    let a = 31.0_f64 / 32.0_f64;
    let b = (0.25_f64).cos();
    let c = 4.0 * (0.25_f64).sin();

    println!("31/32      = {:.15}", a);
    println!("cos(1/4)   = {:.15}", b);
    println!("4sin(1/4)  = {:.15}", c);

    let mut values = [("31/32", a), ("cos(1/4)", b), ("4sin(1/4)", c)];
    values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

    for (name, val) in &values {
        println!("{} = {:.15}", name, val);
    }
}

题目4: 比较 $a = \log_{3}{2}$、$b = \log_{5}{3}$、$c = \frac{2}{3}$ 的大小

fn main() {
    // log_3(2) = ln(2) / ln(3)
    let a = 2.0_f64.ln() / 3.0_f64.ln();
    // log_5(3) = ln(3) / ln(5)
    let b = 3.0_f64.ln() / 5.0_f64.ln();
    let c = 2.0_f64 / 3.0_f64;

    println!("log_3(2) = {:.15}", a);
    println!("log_5(3) = {:.15}", b);
    println!("2/3      = {:.15}", c);

    let mut values = [("log_3(2)", a), ("log_5(3)", b), ("2/3", c)];
    values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());

    for (name, val) in &values {
        println!("{} = {:.15}", name, val);
    }
}

5.2 常用数学比较方法

方法说明示例
单调性法利用函数的增减性同底指数/对数直接看增减
中间值法与 0、1 等特殊值比较$e^x > 1 + x$
作差法判断 $a - b$ 的符号$(n+1)^2 - n^2 = 2n + 1 > 0$
作商法判断 $\frac{a}{b}$ 与 1 的关系(正数时)$\frac{85^2}{84 \times 86} = \frac{7225}{7216} > 1$
估值/放缩利用已知不等式$e^x > 1 + x$,$\ln x < x - 1$

六、关系运算的实际应用

6.1 区间判断

fn classify_score(score: i32) -> &'static str {
    if score >= 90 {
        "优秀"
    } else if score >= 80 {
        "良好"
    } else if score >= 70 {
        "中等"
    } else if score >= 60 {
        "及格"
    } else {
        "不及格"
    }
}

fn main() {
    println!("95 分: {}", classify_score(95));   // 优秀
    println!("75 分: {}", classify_score(75));   // 中等
    println!("55 分: {}", classify_score(55));   // 不及格
}

6.2 三数排序

fn sort_three(a: i32, b: i32, c: i32) -> (i32, i32, i32) {
    let mut arr = [a, b, c];
    arr.sort();
    (arr[0], arr[1], arr[2])
}

fn main() {
    let (min, mid, max) = sort_three(42, 17, 85);
    println!("最小: {}, 中间: {}, 最大: {}", min, mid, max);
    // 最小: 17, 中间: 42, 最大: 85
}

6.3 版本号比较

fn compare_versions(v1: &str, v2: &str) -> std::cmp::Ordering {
    let parts1: Vec<u32> = v1.split('.').map(|s| s.parse().unwrap()).collect();
    let parts2: Vec<u32> = v2.split('.').map(|s| s.parse().unwrap()).collect();

    for (a, b) in parts1.iter().zip(parts2.iter()) {
        match a.cmp(b) {
            std::cmp::Ordering::Equal => continue,
            other => return other,
        }
    }

    // 前缀相同时,更长的版本号更大
    parts1.len().cmp(&parts2.len())
}

fn main() {
    println!("1.2.3 vs 1.2.4: {:?}", compare_versions("1.2.3", "1.2.4"));  // Less
    println!("2.0.0 vs 1.9.9: {:?}", compare_versions("2.0.0", "1.9.9"));  // Greater
    println!("1.0 vs 1.0.0: {:?}", compare_versions("1.0", "1.0.0"));      // Less
}

6.4 查找最大值和最小值

fn find_min_max(data: &[i32]) -> (i32, i32) {
    assert!(!data.is_empty());
    let mut min = data[0];
    let mut max = data[0];

    for &val in data.iter().skip(1) {
        if val < min {
            min = val;
        }
        if val > max {
            max = val;
        }
    }

    (min, max)
}

fn main() {
    let data = [23, 56, 11, 89, 42, 67, 5, 78];
    let (min, max) = find_min_max(&data);
    println!("最小值: {}, 最大值: {}", min, max);
    // 最小值: 5, 最大值: 89

    // 也可以使用标准库方法
    let min2 = data.iter().min().unwrap();
    let max2 = data.iter().max().unwrap();
    println!("最小值: {}, 最大值: {}", min2, max2);
}

6.5 二分查找

关系运算是二分查找的核心:

fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
    let mut left = 0usize;
    let mut right = arr.len();

    while left < right {
        let mid = left + (right - left) / 2;
        if arr[mid] == target {
            return Some(mid);
        } else if arr[mid] < target {
            left = mid + 1;
        } else {
            right = mid;
        }
    }

    None
}

fn main() {
    let sorted = [5, 12, 23, 34, 45, 56, 67, 78, 89, 100];

    match binary_search(&sorted, 45) {
        Some(idx) => println!("找到 45,索引为 {}", idx),
        None => println!("未找到 45"),
    }

    match binary_search(&sorted, 50) {
        Some(idx) => println!("找到 50,索引为 {}", idx),
        None => println!("未找到 50"),
    }
}

七、关系运算与模式匹配

在 Rust 中,模式匹配(match)经常与关系运算结合使用:

fn describe_temperature(temp: f64) -> &'static str {
    match temp {
        t if t < 0.0 => "冰点以下",
        t if t < 15.0 => "寒冷",
        t if t < 25.0 => "舒适",
        t if t < 35.0 => "炎热",
        _ => "酷暑",
    }
}

fn main() {
    println!("-5°C:  {}", describe_temperature(-5.0));   // 冰点以下
    println!("10°C:  {}", describe_temperature(10.0));  // 寒冷
    println!("20°C:  {}", describe_temperature(20.0));  // 舒适
    println!("30°C:  {}", describe_temperature(30.0));  // 炎热
    println!("40°C:  {}", describe_temperature(40.0));  // 酷暑
}

注意: match 中的 if 守卫(guard)允许我们在模式匹配中使用关系运算,使代码更加灵活。


八、总结

概念要点
关系运算符==!=>>=<<=,返回 bool
PartialOrd部分有序,partial_cmp 返回 Option<Ordering>,适用于浮点数
Ord完全有序,cmp 返回 Ordering,适用于整数、字符串等
浮点数比较避免直接 ==,使用误差范围或 total_cmp
NaN不等于自身,partial_cmp 返回 None
自定义比较实现 PartialOrd + PartialEq(或 Ord + Eq

关系运算是编程中最基础的操作之一。理解 PartialOrdOrd 的区别、掌握浮点数比较的正确方法,是写出健壮 Rust 程序的关键。

练习建议:

  1. 尝试为自定义结构体实现 Ord trait,支持多字段排序
  2. 编写一个通用的近似相等比较函数
  3. 用二分查找实现“查找第一个大于等于目标值的元素“