Ziglings 笔记 49: 阶段性测验 6 - 双向奔赴的大象

进击的大象

这是 Ziglings 的第六次阶段性测验(Quiz 6)。我们再次拿起了大象链表的例子,但这次升级了!

在此之前,我们的大象只能手拉手排成一行单向行走。如果走到最后一只大象,我们就回不去了。为了解决这个问题,我们需要引入“象鼻”的概念,让大象也能牵住前面伙伴的尾巴。

在计算机科学中,这就是从 单向链表 进化为 双向链表 的过程。

挑战:实现回溯

我们需要在 Elephant 结构体中实现关于 trunk (象鼻) 的方法,使得程序能够:

  1. 正向遍历:A -> B -> C
  2. 反向遍历:C -> B -> A

解决方案

这是补充完整的代码逻辑:

const std = @import("std");

const Elephant = struct {
    letter: u8,
    tail: ?*Elephant = null,  // 指向下一只 (Next)
    trunk: ?*Elephant = null, // 指向上一只 (Prev)
    visited: bool = false,

    // Tail 方法 (Next)
    pub fn getTail(self: *Elephant) *Elephant {
        return self.tail.?;
    }

    pub fn hasTail(self: *Elephant) bool {
        return (self.tail != null);
    }

    // Trunk 方法 (Prev) - 我们的任务是实现这部分
    // 逻辑与 Tail 完全对称
    pub fn getTrunk(self: *Elephant) *Elephant {
        return self.trunk.?; // 同样使用 .? 快速解包
    }

    pub fn hasTrunk(self: *Elephant) bool {
        return (self.trunk != null);
    }
    
    // ... 其他打印方法 ...
};

pub fn main() void {
    var elephantA = Elephant{ .letter = 'A' };
    var elephantB = Elephant{ .letter = 'B' };
    var elephantC = Elephant{ .letter = 'C' };

    // 链接 Tails (正向)
    elephantA.tail = &elephantB;
    elephantB.tail = &elephantC;

    // 链接 Trunks (反向)
    // 注意:A 没有 trunk (null),C 没有 tail (null)
    elephantB.trunk = &elephantA;
    elephantC.trunk = &elephantB;

    visitElephants(&elephantA);
}

fn visitElephants(first_elephant: *Elephant) void {
    var e = first_elephant;

    // 1. 顺藤摸瓜 (Follow Tails)
    while (true) {
        e.print(); // A -> B -> C
        if (e.hasTail()) {
            e = e.getTail();
        } else {
            break; // 停在 C
        }
    }

    // 2. 倒打一耙 (Follow Trunks)
    while (true) {
        e.print(); // C -> B -> A
        if (e.hasTrunk()) {
            e = e.getTrunk();
        } else {
            break; // 停在 A
        }
    }
}

核心知识点回顾

1. 双向链表 (Doubly Linked List)

通过同时维护 next (tail) 和 prev (trunk) 指针,我们在内存中构建了一个可以自由前后移动的数据结构。这在实现诸如浏览器的“前进/后退”功能或文本编辑器的“撤销/重做”栈时非常有用。

2. 结构体方法的封装

我们在 main 函数和 visitElephants 函数中,完全不需要知道 trunk?*Elephant 类型。我们只调用了 hasTrunk()getTrunk()。这种封装让业务逻辑代码非常干净,也更安全。

3. 可选类型的边界控制

?*Elephant 类型强制我们处理 null。在链表中,null 天然充当了哨兵(Sentinel)的角色,告诉我们“到头了”。结合 hasTrunk() 的检查,我们编写出了不会越界崩溃的健壮代码。


后续预告:顺利通过 Quiz 6!我们已经把基本类型玩得很溜了。接下来的练习可能会带我们去探索一些更抽象的概念,比如“什么都没有”到底有几种写法?下一篇我们聊聊 void, undefined 和空结构体。