Ziglings 笔记 44: 阶段性测验 5 - 大象与其尾巴
牵手的大象
在 Ziglings 的第 44 个练习(Quiz 5)中,我们迎来了一个有趣的数据结构挑战:循环链表。
题目引用了一首儿歌,描述大象们通过尾巴和鼻子连在一起。在编程中,这正是链表 (Linked List) 的形象比喻。
挑战:构建循环链表
我们需要:
- 创建三个
Elephant结构体实例(A, B, C)。 - 使用指针将它们连接起来:A 的尾巴指向 B,B 指向 C,C 指回 A。
- 编写一个安全的遍历算法,确保不会陷入死循环。
解决方案
这是完整的实现代码:
const std = @import("std");
// 定义链表节点
const Elephant = struct {
letter: u8,
// 自引用指针:指向另一个 Elephant
// 初始化为 undefined,因为在创建时我们还不知道下一个大象在哪里
tail: *Elephant = undefined,
visited: bool = false,
};
pub fn main() void {
// 1. 实例化节点
var elephantA = Elephant{ .letter = 'A' };
var elephantB = Elephant{ .letter = 'B' }; // 补充 Elephant B
var elephantC = Elephant{ .letter = 'C' };
// 2. 建立链接 (A -> B -> C -> A)
// 使用取地址符 & 获取指针
elephantA.tail = &elephantB;
elephantB.tail = &elephantC; // 链接 B 到 C
elephantC.tail = &elephantA; // 闭环:C 回到 A
visitElephants(&elephantA);
std.debug.print("\n", .{});
}
// 遍历函数
fn visitElephants(first_elephant: *Elephant) void {
var e = first_elephant;
// 3. 使用 visited 标记防止死循环
// 这里的 e 是指针,但 e.visited 自动解引用,这是 Zig 的语法糖
while (!e.visited) {
std.debug.print("Elephant {u}. ", .{e.letter});
e.visited = true;
// 移动到下一个节点
e = e.tail;
}
}
核心知识点回顾
1. 指针作为“链接”
这是所有动态数据结构(链表、树、图)的基石。通过在结构体中存储 *SelfType,我们可以在内存中建立非连续数据的逻辑连接。
2. 自动解引用 (Auto-dereference)
在 visitElephants 函数中,e 是一个指针。
但是在访问属性时,我们直接使用了 e.visited 和 e.tail,而不是 C 语言风格的 e->tail 或 (*e).tail。Zig 编译器帮我们处理了指针解引用的繁琐工作,让代码看起来像是在操作普通对象。
3. 算法逻辑
对于循环结构,必须有状态记录(State)来避免无限循环。这里我们简单地修改了结构体内部的 visited 字段。在更复杂的场景(或不可变数据)中,我们可能需要外部的哈希表来记录已访问的节点地址。
后续预告:我们已经彻底搞定了基础的指针和结构体。接下来,我们将进入 Zig 最具特色的功能之一——Optional 类型(?)。它是如何彻底解决空指针异常(Null Pointer Exception)的?下一篇见分晓。