题目描述

✅ 1396. 设计地铁系统

image-20260929082534928

image-20260929082535086

image-20260929082535232

image-20260929082535488

题意分析

系统记录乘客的进站和出站事件,并查询某个起点站到终点站的历史平均乘车时间。一段完整行程由同一乘客的一次进站和随后的一次出站配对,尚未出站的行程不参与统计。

路线有方向,起点到终点与反向路线分别计算。题目保证调用有效:乘客不会同时处于两段行程中,出站时间晚于对应进站时间,查询路线也已经至少完成过一次行程。

解法:哈希表记录行程

核心思路

[!blue]

需要保存两种生命周期不同的信息。active 按乘客编号记录还未结束的行程,包括起点站和进站时刻;stats 按有向路线记录所有已完成行程的累计耗时与次数。前者用于配对事件,后者用于回答历史统计查询。

进站只更新在途记录。出站时,用乘客编号取出他的起点和开始时间,计算 出站时刻 - 进站时刻,然后把这段耗时加入对应路线的总和、把次数加一,同时移除在途记录。这样每次完成事件都只结算一段行程,乘客之后也可以重新开始新的行程。

平均耗时只取决于总和与次数,不需要保存每条历史明细。查询时返回 total / count,但必须先转为浮点数再除,才能保留小数;也不能把原平均值与新耗时直接二等分,因为旧平均值可能代表多次行程。

路线键要同时保留起点、终点和先后顺序。Java 使用 起点 + # + 终点,题目站名只含英文字母和数字,因此分隔符不会与站名内容混淆;Go 使用起终点结构体作为键,直接表达两个字段。

累计耗时可能超过一次行程的整数范围,所以使用 64 位保存。Go 的统计值存为结构体,读取 map 得到的是副本,修改后必须写回;Java map 中保存对象引用,更新对象字段即可保留统计结果。

解题步骤

  1. 创建按乘客编号索引的在途表,以及按有向路线索引的汇总表。
  2. checkIn 保存当前乘客的起点站和时刻。
  3. checkOut 读取并删除在途记录,求出本次耗时,更新对应路线的总耗时与次数。
  4. getAverageTime 找到路线统计,将总耗时转换为浮点后除以次数并返回。

代码实现

class UndergroundSystem {
    private static class CheckInInfo {
        final String station;
        final int time;

        CheckInInfo(String station, int time) {
            this.station = station;
            this.time = time;
        }
    }

    private static class Stat {
        long total;
        int count;
    }

    private final Map<Integer, CheckInInfo> active = new HashMap<>();
    private final Map<String, Stat> stats = new HashMap<>();

    public void checkIn(int id, String stationName, int t) {
        active.put(id, new CheckInInfo(stationName, t));
    }

    public void checkOut(int id, String stationName, int t) {
        // 完成行程时取得并移除在途记录,只结算已出站行程。
        CheckInInfo info = active.remove(id);
        String key = routeKey(info.station, stationName);
        Stat s = stats.computeIfAbsent(key, k -> new Stat());

        // 总耗时与次数增量维护,不保存每条历史明细。
        s.total += t - info.time;
        s.count++;
    }

    public double getAverageTime(String startStation, String endStation) {
        Stat s = stats.get(routeKey(startStation, endStation));

        // 先转浮点再除,保留平均值的小数。
        return (double) s.total / s.count;
    }

    private String routeKey(String startStation, String endStation) {
        // 路线有方向,使用分隔符避免不同起终站的直接拼接碰撞。
        return startStation + "#" + endStation;
    }
}
type checkInInfo struct {
    station string
    time    int
}

type stat struct {
    total int64
    count int64
}

type route struct {
    start string
    end   string
}

type UndergroundSystem struct {
    active map[int]checkInInfo
    stats  map[route]stat
}

func Constructor() UndergroundSystem {
    return UndergroundSystem{
        active: make(map[int]checkInInfo),
        stats:  make(map[route]stat),
    }
}

func (u *UndergroundSystem) CheckIn(id int, stationName string, t int) {
    u.active[id] = checkInInfo{station: stationName, time: t}
}

func (u *UndergroundSystem) CheckOut(id int, stationName string, t int) {
    info := u.active[id]
    // 取得进站信息后移除在途记录,只结算已完成行程。
    delete(u.active, id)
    // 用有方向的起终站组合做键,不合并反向路线。
    key := route{start: info.station, end: stationName}
    s := u.stats[key]
    s.total += int64(t - info.time)
    s.count++
    // map 取出的结构体是副本,更新后要写回。
    u.stats[key] = s
}

func (u *UndergroundSystem) GetAverageTime(startStation string, endStation string) float64 {
    s := u.stats[route{start: startStation, end: endStation}]
    // 先转浮点再除,保留平均值的小数。
    return float64(s.total) / float64(s.count)
}

复杂度分析

设站名长度上界为 $L$,过程中最多同时在途人数为 $P$,已完成的不同有向路线数为 $R$。

  • 时间复杂度:单次操作期望为 $O(L)$,包括路线键构造及字符串哈希;只看固定长度站名时为期望常数时间。
  • 空间复杂度:$O((P+R)L)$,保存未完成行程与每条路线的汇总,不随同一路线的历史行程条数线性增长。

关键点总结

[!green]

  • 在途表配对进出站,路线表保留历史总和与次数,两者职责不同。
  • 一次行程完成时才计入平均值,反向路线不能合并。
  • 总和用宽整数,除法前转浮点;Go 更新结构体副本后需要写回 map。

易错点总结

[!yellow]

  • 先做整数除法再转浮点,小数部分已经被截断,无法恢复。
  • 起终站直接无分隔拼接,可能让不同的两段名称组合得到相同键。
  • 把两方向路线归为同一组,会混淆不同出发站的历史耗时。
  • 累计耗时用 32 位保存,重复行程累加后可能溢出。
  • Go 修改统计结构体后不写回 map,查询仍会读到旧结果。
  • 出站后不移除在途记录,会破坏在途表只表示未完成行程的含义。

相似题目

题目 难度 关联与区别
1661. 每台机器的进程平均运行时间 简单 同样先配对开始与结束事件求时长,再按类别累计总时长与次数得到平均值。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2023/21055444
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!