第十五 关系运算
关系运算(Relational Operation)用于比较两个值之间的大小关系,返回一个布尔值(true 或 false)。它是条件判断和程序控制流的基础,也是排序、搜索等算法的核心操作。
一、Rust 中的关系运算符
1.1 六种关系运算符
Rust 提供了六种关系运算符,用于比较两个值的大小关系:
| 运算符 | 含义 | 数学符号 | 示例 | 结果 |
|---|---|---|---|---|
== | 等于 | = | 5 == 5 | true |
!= | 不等于 | ≠ | 5 != 3 | true |
> | 大于 | > | 5 > 3 | true |
>= | 大于等于 | ≥ | 5 >= 5 | true |
< | 小于 | < | 3 < 5 | true |
<= | 小于等于 | ≤ | 3 <= 5 | true |
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 的比较问题
NaN 是 PartialOrd 但不是 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 != NaN为true。这是 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_cmp和f32::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) |
关系运算是编程中最基础的操作之一。理解 PartialOrd 和 Ord 的区别、掌握浮点数比较的正确方法,是写出健壮 Rust 程序的关键。
练习建议:
- 尝试为自定义结构体实现
Ordtrait,支持多字段排序- 编写一个通用的近似相等比较函数
- 用二分查找实现“查找第一个大于等于目标值的元素“