Rust 练习册 111:构建锦标赛积分榜系统

在体育竞技和游戏竞赛中,统计和展示比赛结果是一项重要而常见的任务。今天我们要探讨的是如何用Rust构建一个锦标赛积分榜系统,它能够解析比赛结果并生成格式化的积分榜表格。这个问题不仅考验我们的字符串处理能力,也涉及数据结构设计和排序算法的应用。

问题背景

在各类体育联赛和竞技比赛中,我们需要一个系统来跟踪各队伍的比赛表现。通常,比赛结果会以特定格式记录,而我们需要将这些原始数据转换为易于理解的积分榜表格。

积分规则通常如下:

  • 胜利(win):3分
  • 平局(draw):1分
  • 失败(loss):0分

积分榜需要包含以下信息:

  • 队伍名称
  • 比赛场次(MP - Matches Played)
  • 胜利场次(W - Wins)
  • 平局场次(D - Draws)
  • 失败场次(L - Losses)
  • 总积分(P - Points)

问题描述

我们的任务是实现这样一个函数:

rust 复制代码
pub fn tally(match_results: &str) -> String {
    unimplemented!(
        "Given the result of the played matches '{}' return a properly formatted tally table string.",
        match_results
    );
}

该函数接收一个包含比赛结果的字符串,返回格式化的积分榜表格字符串。

数据格式分析

根据测试案例,输入数据格式为:

复制代码
队伍1;队伍2;比赛结果

其中比赛结果可以是:

  • win:队伍1胜利,队伍2失败
  • loss:队伍1失败,队伍2胜利
  • draw:平局

多行数据通过换行符分隔。

输出格式为表格形式:

复制代码
Team                           | MP |  W |  D |  L |  P
队伍名称1                      |  场次 | 胜 | 平 | 负 | 积分
队伍名称2                      |  场次 | 胜 | 平 | 负 | 积分
...

解决方案

让我们实现一个完整的解决方案:

rust 复制代码
use std::collections::HashMap;

#[derive(Default)]
struct TeamStats {
    matches_played: u32,
    wins: u32,
    draws: u32,
    losses: u32,
}

impl TeamStats {
    fn points(&self) -> u32 {
        self.wins * 3 + self.draws
    }
}

pub fn tally(match_results: &str) -> String {
    if match_results.is_empty() {
        return "Team                           | MP |  W |  D |  L |  P".to_string();
    }
    
    let mut teams: HashMap<String, TeamStats> = HashMap::new();
    
    // 解析每行比赛结果
    for line in match_results.lines() {
        let parts: Vec<&str> = line.split(';').collect();
        if parts.len() != 3 {
            continue;
        }
        
        let team1 = parts[0];
        let team2 = parts[1];
        let result = parts[2];
        
        // 更新队伍1的统计
        teams.entry(team1.to_string())
            .or_insert_with(TeamStats::default)
            .matches_played += 1;
        
        // 更新队伍2的统计
        teams.entry(team2.to_string())
            .or_insert_with(TeamStats::default)
            .matches_played += 1;
        
        // 根据比赛结果更新胜负平统计
        match result {
            "win" => {
                teams.get_mut(team1).unwrap().wins += 1;
                teams.get_mut(team2).unwrap().losses += 1;
            }
            "loss" => {
                teams.get_mut(team1).unwrap().losses += 1;
                teams.get_mut(team2).unwrap().wins += 1;
            }
            "draw" => {
                teams.get_mut(team1).unwrap().draws += 1;
                teams.get_mut(team2).unwrap().draws += 1;
            }
            _ => {} // 忽略无效结果
        }
    }
    
    // 将队伍统计数据转换为向量并排序
    let mut standings: Vec<(String, TeamStats)> = teams.into_iter().collect();
    standings.sort_by(|a, b| {
        // 首先按积分降序排列
        b.1.points().cmp(&a.1.points())
            // 积分相同时按字母顺序排列
            .then_with(|| a.0.cmp(&b.0))
    });
    
    // 构建输出表格
    let mut result = String::from("Team                           | MP |  W |  D |  L |  P");
    
    for (team_name, stats) in standings {
        result.push('\n');
        result.push_str(&format!(
            "{:<30} | {:>2} | {:>2} | {:>2} | {:>2} | {:>2}",
            team_name,
            stats.matches_played,
            stats.wins,
            stats.draws,
            stats.losses,
            stats.points()
        ));
    }
    
    result
}

测试案例详解

通过查看测试案例,我们可以更好地理解函数的行为:

rust 复制代码
#[test]
fn just_the_header_if_no_input() {
    let input = "";
    let expected = "Team                           | MP |  W |  D |  L |  P";

    assert_eq!(tournament::tally(&input), expected);
}

当没有输入时,只返回表头。

rust 复制代码
#[test]
fn a_win_is_three_points_a_loss_is_zero_points() {
    let input = "Allegoric Alaskans;Blithering Badgers;win";
    let expected = "".to_string()
        + "Team                           | MP |  W |  D |  L |  P\n"
        + "Allegoric Alaskans             |  1 |  1 |  0 |  0 |  3\n"
        + "Blithering Badgers             |  1 |  0 |  0 |  1 |  0";

    assert_eq!(tournament::tally(&input), expected);
}

展示胜利得3分,失败得0分的基本规则。

rust 复制代码
#[test]
fn a_draw_is_one_point_each() {
    let input = "Allegoric Alaskans;Blithering Badgers;draw\n".to_string()
        + "Allegoric Alaskans;Blithering Badgers;win";
    let expected = "".to_string()
        + "Team                           | MP |  W |  D |  L |  P\n"
        + "Allegoric Alaskans             |  2 |  1 |  1 |  0 |  4\n"
        + "Blithering Badgers             |  2 |  0 |  1 |  1 |  1";

    assert_eq!(tournament::tally(&input), expected);
}

展示平局各得1分的情况。

rust 复制代码
#[test]
fn ties_broken_alphabetically() {
    let input = "Courageous Californians;Devastating Donkeys;win\n".to_string()
        + "Allegoric Alaskans;Blithering Badgers;win\n"
        + "Devastating Donkeys;Allegoric Alaskans;loss\n"
        + "Courageous Californians;Blithering Badgers;win\n"
        + "Blithering Badgers;Devastating Donkeys;draw\n"
        + "Allegoric Alaskans;Courageous Californians;draw";
    let expected = "".to_string()
        + "Team                           | MP |  W |  D |  L |  P\n"
        + "Allegoric Alaskans             |  3 |  2 |  1 |  0 |  7\n"
        + "Courageous Californians        |  3 |  2 |  1 |  0 |  7\n"
        + "Blithering Badgers             |  3 |  0 |  1 |  2 |  1\n"
        + "Devastating Donkeys            |  3 |  0 |  1 |  2 |  1";

    assert_eq!(tournament::tally(&input), expected);
}

展示积分相同时按字母顺序排列的规则。

优化版本

我们可以对实现进行一些优化,提高代码的健壮性和可读性:

rust 复制代码
use std::collections::HashMap;

#[derive(Default, Clone)]
struct TeamStats {
    matches_played: u32,
    wins: u32,
    draws: u32,
    losses: u32,
}

impl TeamStats {
    fn points(&self) -> u32 {
        self.wins * 3 + self.draws
    }
}

#[derive(Debug)]
enum MatchResult {
    Win,
    Loss,
    Draw,
}

impl MatchResult {
    fn from_str(s: &str) -> Option<MatchResult> {
        match s {
            "win" => Some(MatchResult::Win),
            "loss" => Some(MatchResult::Loss),
            "draw" => Some(MatchResult::Draw),
            _ => None,
        }
    }
}

fn update_team_stats(teams: &mut HashMap<String, TeamStats>, team_name: &str) {
    teams.entry(team_name.to_string())
        .or_insert_with(TeamStats::default)
        .matches_played += 1;
}

fn record_match_result(
    teams: &mut HashMap<String, TeamStats>,
    team1: &str,
    team2: &str,
    result: MatchResult,
) {
    update_team_stats(teams, team1);
    update_team_stats(teams, team2);
    
    match result {
        MatchResult::Win => {
            teams.get_mut(team1).unwrap().wins += 1;
            teams.get_mut(team2).unwrap().losses += 1;
        }
        MatchResult::Loss => {
            teams.get_mut(team1).unwrap().losses += 1;
            teams.get_mut(team2).unwrap().wins += 1;
        }
        MatchResult::Draw => {
            teams.get_mut(team1).unwrap().draws += 1;
            teams.get_mut(team2).unwrap().draws += 1;
        }
    }
}

pub fn tally(match_results: &str) -> String {
    if match_results.is_empty() {
        return "Team                           | MP |  W |  D |  L |  P".to_string();
    }
    
    let mut teams: HashMap<String, TeamStats> = HashMap::new();
    
    // 解析每行比赛结果
    for line in match_results.lines() {
        let parts: Vec<&str> = line.split(';').collect();
        if parts.len() != 3 {
            continue;
        }
        
        let team1 = parts[0];
        let team2 = parts[1];
        let result_str = parts[2];
        
        if let Some(result) = MatchResult::from_str(result_str) {
            record_match_result(&mut teams, team1, team2, result);
        }
    }
    
    // 将队伍统计数据转换为向量并排序
    let mut standings: Vec<(String, TeamStats)> = teams.into_iter().collect();
    standings.sort_by(|a, b| {
        b.1.points().cmp(&a.1.points())
            .then_with(|| a.0.cmp(&b.0))
    });
    
    // 构建输出表格
    let mut result = String::from("Team                           | MP |  W |  D |  L |  P");
    
    for (team_name, stats) in standings {
        result.push('\n');
        result.push_str(&format!(
            "{:<30} | {:>2} | {:>2} | {:>2} | {:>2} | {:>2}",
            team_name,
            stats.matches_played,
            stats.wins,
            stats.draws,
            stats.losses,
            stats.points()
        ));
    }
    
    result
}

Rust语言特性运用

在这个实现中,我们运用了多种Rust语言特性:

  1. HashMap: 用于存储队伍统计数据
  2. 模式匹配: 使用[match]处理比赛结果
  3. 字符串格式化: 使用[format!]宏创建格式化输出
  4. 迭代器: 使用[lines()]和[split()]处理输入数据
  5. Option类型: 安全地处理可能失败的操作
  6. 生命周期: 理解字符串切片的生命周期
  7. 结构体: 定义[TeamStats]来组织数据
  8. 排序: 使用[sort_by]和[then_with]进行复合排序

算法复杂度分析

让我们分析实现的复杂度:

  • 时间复杂度 : O(n + m log m),其中n是比赛场次数量,m是队伍数量
    • 解析比赛结果:O(n)
    • 排序队伍:O(m log m)
  • 空间复杂度: O(m),用于存储队伍统计数据

实际应用场景

锦标赛积分榜系统在许多实际场景中都有应用:

  1. 体育联赛: 足球、篮球等联赛积分榜
  2. 电子竞技: 游戏比赛排名系统
  3. 企业竞赛: 内部编程竞赛或游戏活动
  4. 教育评估: 学生竞赛成绩统计
  5. 数据分析: 比赛数据可视化预处理

扩展功能

我们可以为这个系统添加更多功能:

rust 复制代码
impl TeamStats {
    // 计算胜率
    fn win_rate(&self) -> f64 {
        if self.matches_played == 0 {
            0.0
        } else {
            self.wins as f64 / self.matches_played as f64
        }
    }
    
    // 计算进球数和失球数(需要扩展数据结构)
    // fn goal_difference(&self) -> i32 {
    //     self.goals_for as i32 - self.goals_against as i32
    // }
}

// 支持从文件读取数据
pub fn tally_from_file(file_path: &str) -> std::io::Result<String> {
    use std::fs;
    let contents = fs::read_to_string(file_path)?;
    Ok(tally(&contents))
}

// 支持输出为JSON格式
pub fn tally_as_json(match_results: &str) -> String {
    // 实现JSON格式输出
    // ...
}

错误处理改进

增强错误处理能力:

rust 复制代码
#[derive(Debug)]
pub enum TournamentError {
    InvalidFormat,
    InvalidResult,
}

impl std::fmt::Display for TournamentError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            TournamentError::InvalidFormat => write!(f, "Invalid match format"),
            TournamentError::InvalidResult => write!(f, "Invalid match result"),
        }
    }
}

impl std::error::Error for TournamentError {}

pub fn tally_safe(match_results: &str) -> Result<String, TournamentError> {
    // 返回Result类型,提供更好的错误处理
    // ...
}

与其他实现方式的比较

Python实现

python 复制代码
def tally(match_results):
    if not match_results:
        return "Team                           | MP |  W |  D |  L |  P"
    
    teams = {}
    for line in match_results.splitlines():
        parts = line.split(';')
        if len(parts) != 3:
            continue
            
        team1, team2, result = parts
        # ... 更新统计逻辑
        
    # ... 排序和格式化输出

JavaScript实现

javascript 复制代码
function tally(matchResults) {
    if (!matchResults) {
        return "Team                           | MP |  W |  D |  L |  P";
    }
    
    const teams = new Map();
    // ... 处理逻辑
    
    // ... 排序和格式化输出
}

Rust的实现相比其他语言,具有内存安全、无垃圾回收、编译时错误检查等优势。

总结

通过这个练习,我们学习到了:

  1. 如何解析和处理结构化文本数据
  2. 使用HashMap存储和查询数据的技巧
  3. 复合排序规则的实现方法
  4. 字符串格式化和表格输出的技术
  5. 错误处理和边界条件的考虑
  6. Rust在数据处理方面的强大能力

锦标赛积分榜问题虽然看似简单,但它涉及了数据解析、存储、排序和格式化输出等多个方面。通过这个练习,我们不仅掌握了具体的实现技巧,也加深了对Rust语言特性的理解。

在实际应用中,这样的系统可以轻松扩展以支持更复杂的功能,如进球统计、主客场记录、历史数据分析等。Rust的安全性和性能优势使得它成为构建这类系统的优秀选择。

这个练习也展示了Rust在处理现实世界问题时的表达能力,通过类型系统和模式匹配,我们可以编写出既安全又清晰的代码。

相关推荐
蓑衣夜行2 小时前
QtWebEngine 自动重启方案
开发语言·c++·qt·web·qwebengine
lsx2024062 小时前
XQuery 实例详解
开发语言
hefaxiang2 小时前
猜数字小游戏--用分支和循环实现
c语言·开发语言
小清兔2 小时前
一个unity中URP的环境下旋转天空盒的脚本(RotationSky)
开发语言·数据库·学习·程序人生·unity·c#·游戏引擎
小裕哥略帅2 小时前
Springboot中全局myBaits插件配置
java·spring boot·后端
San30.2 小时前
从原型链到“圣杯模式”:JavaScript 继承方案的演进与终极解法
开发语言·javascript·原型模式
MX_93593 小时前
Spring中Bean注入方式和注入类型
java·后端·spring
乌托邦2号3 小时前
Qt5之中文字符串转换
开发语言·qt
申阳3 小时前
Day 22:SpringBoot4 + Tauri 2.0(VUE) 登录功能前后端联调
前端·后端·程序员