Ziglings 笔记 58: 阶段性测验 7 - 隐士的地图 (图算法)

穿越森林

这是 Ziglings 的第七次阶段性测验(Quiz 7)。我们面对的是一个相当复杂的程序:一个为隐士规划森林旅行路线的导航系统。

从计算机科学的角度看,这实际上是在一个有向加权图 (Directed Weighted Graph) 上运行最短路径算法

挑战:修复导航系统

虽然代码结构已经搭好,但在关键的逻辑部分留下了几个“坑”,主要涉及复杂的指针操作和类型解包。

解决方案

以下是修复后的关键代码片段:

1. 联合体的 Switch 捕获

TripItem 中,我们需要正确打印 Place 或 Path。

const TripItem = union(enum) {
    place: *const Place,
    path: *const Path,

    fn printMe(self: TripItem) void {
        switch (self) {
            // 修复点:添加 |p| 来捕获 payload
            .place => |p| print("{s}", .{p.name}),
            .path => |p| print("--{}->", .{p.dist}),
        }
    }
};

2. 指针解引用迷宫

getEntry 中,我们需要在一个可选指针数组中查找特定地点。

fn getEntry(self: *HermitsNotebook, place: *const Place) ?*NotebookEntry {
    for (&self.entries, 0..) |*entry, i| {
        if (i >= self.end_of_entries) break;

        // 修复点:entry 是指向 ?NotebookEntry 的指针
        // 1. entry.* 解引用拿到 ?NotebookEntry
        // 2. .? 确保它不为 null
        // 3. .place 访问字段
        if (place == entry.*.?.place) return &entry.*.?;
    }
    return null;
}

3. 函数错误返回

getTripTo 中,我们可能会抛出错误,所以必须修改函数签名。

// 修复点:返回类型改为 !void (Error Union)
fn getTripTo(self: *HermitsNotebook, trip: []?TripItem, dest: *Place) !void {
    const destination_entry = self.getEntry(dest);
    
    // 如果找不到目的地,返回错误
    if (destination_entry == null) {
        return TripError.Unreachable;
    }
    // ...
}

核心知识点回顾

1. 图的表示

程序使用了 邻接表 (Adjacency List) 的变体。 Place 结构体包含一个 paths 切片,列出了所有从该地点出发的路径。这是一种非常高效的图存储方式。

2. 算法逻辑 (SPFA / BFS)

隐士的笔记本其实就是一个优先队列的简化版。

  1. 将起点加入笔记本。
  2. 取出当前最近的地点。
  3. 检查它的所有邻居:如果通过当前地点去邻居更近,就更新邻居的距离。
  4. 重复直到队列为空。

3. 指针的威力

整个程序大量使用了指针(*const Place, *const Path)。这意味着我们在传递数据时没有任何拷贝开销。无论地图有多大,地点和路径结构体在内存中都只有一份,我们只是在传递它们的地址。这是高性能系统编程的典型特征。


后续预告:我们已经完成了所有关于基础类型的学习。接下来的练习将进入一个全新的领域:如何在运行时请求更多的内存?下一篇我们将探讨堆分配 (Heap Allocation) 和内存分配器 (Allocators)。