Project 1C Deque61B Enhancements
截止时间:2 月 20 日星期二晚上 11:59(太平洋时间)
常见问题解答
每个作业的顶部都会提供一个常见问题解答的链接。您也可以通过在网址末尾添加“/faq”来访问该页面。Project 1C 的常见问题解答位于此处。
介绍
在 Project 1A 中,我们构建了 LinkedListDeque61B,在 Project 1B 中,我们构建了 ArrayDeque61B。现在我们将看到一个不同的实现:MaxArrayDeque61B!这部分项目将增强您之前实现的 ArrayDeque61B 和 LinkedListDeque61B,并将它们整合到一个新的数据结构应用中。
在 Project 1C 结束时,您将完成以下操作:
- 为
LinkedListDeque61B.java和ArrayDeque61B.java编写iterator()、equals()和toString()方法。 - 实现
MaxArrayDeque61B.java。 - 完成
GuitarHero任务。
本节假设您已经观看并完全理解了直到迭代器、对象方法讲座(第 11 讲)的所有讲座。
风格
和 Project 1B 一样,我们仍然会严格检查代码风格。您必须遵循风格指南,否则您将在自动评分器上受到处罚。
您应该使用 CS 61B 插件在本地检查代码风格,而且这是允许的。如果您因为没有检查代码风格而受到速度限制,我们将不会解除该限制。
获取骨架文件
按照作业工作流程指南中的说明获取骨架代码并在 IntelliJ 中打开它。对于本项目,我们将在 proj1c 目录中工作。
您会在您的代码仓库中看到一个 proj1c 目录,目录结构如下所示:
proj1c
├── src
│ ├── deque
│ │ ├── ArrayDeque61B.java
│ │ ├── Deque61B.java
│ │ └── LinkedListDeque61B.java
│ └──gh2
│ ├── GuitarHeroLite.java
│ ├── GuitarPlayer.java
│ ├── GuitarString.java
│ └── TTFAF.java
│
└── tests
├── MaxArrayDeque61BTest.java
└── TestGuitarString.java
如果您遇到任何错误,请先停止操作,仔细阅读 git WTFs 尝试自行解决,或者向助教 (OH) 或 Ed 寻求帮助。相比于盲目尝试 Git 命令,这样做能帮您避免很多麻烦。如果您发现自己想使用 Google 搜索到的命令(例如
force push),请务必避免!即使 Stack Overflow 上的帖子 强烈建议 你使用
force push,也 绝对不要 这样做!
您还可以观看 Hug 教授的演示,了解如何入门,以及如果您遇到一些 git 问题,可以观看此视频。
对象方法
如果您愿意,您可以按照这个简短的 视频指南 中的步骤来帮助您设置 Project 1C!
为了实现以下方法,您应该首先将您在 Project 1A 和 Project 1B 中实现的 LinkedListDeque61B 和 ArrayDeque61B 复制并粘贴到 proj1c 目录中的相关文件中。
请将 package deque; 放在两个文件的顶部。否则,您的代码将无法编译。
iterator()
我们的 Deque61B 接口的一个不足之处是它不支持迭代。也就是说,以下代码无法通过编译,并提示 “foreach not applicable to type” 错误。
Deque61B<String> lld1 = new LinkedListDeque61B<>();
lld1.addLast("front");
lld1.addLast("middle");
lld1.addLast("back");
for (String s : lld1) {
System.out.println(s);
}
类似地,如果我们尝试编写测试来验证 Deque61B 是否包含特定元素,也会遇到编译错误,例如:“Cannot resolve method containsExactly in Subject”。
public void addLastTestBasicWithoutToList() {
Deque61B<String> lld1 = new LinkedListDeque61B<>();
lld1.addLast("front"); // after this call we expect: ["front"]
lld1.addLast("middle"); // after this call we expect: ["front", "middle"]
lld1.addLast("back"); // after this call we expect: ["front", "middle", "back"]
assertThat(lld1).containsExactly("front", "middle", "back");
}
同样的问题在于,我们的数据项无法被迭代。Truth 库通过迭代我们的对象来工作(如第一个例子所示),但我们的 LinkedListDeque61B 类不支持迭代。
为了解决这个问题,您应该首先修改 Deque61B 接口,修改后的声明应如下所示:
public interface Deque61B<T> extends Iterable<T> {
接下来,请使用第 11 讲中介绍的技术来实现 iterator() 方法。
任务:根据讲座内容,在
LinkedListDeque61B类和ArrayDeque61B类中实现iterator()方法。
在此不允许调用 toList 方法。
equals()
考虑以下代码:
@Test
public void testEqualDeques61B() {
Deque61B<String> lld1 = new LinkedListDeque61B<>();
Deque61B<String> lld2 = new LinkedListDeque61B<>();
lld1.addLast("front");
lld1.addLast("middle");
lld1.addLast("back");
lld2.addLast("front");
lld2.addLast("middle");
lld2.addLast("back");
assertThat(lld1).isEqualTo(lld2);
}
如果我们运行这段代码,我们会发现测试失败,并显示以下消息:
expected: [front, middle, back]
but was : (non-equal instance of same class with same string representation)
问题在于,Truth 库正在调用 LinkedListDeque61B 类的 equals 方法。默认实现如下面的代码所示(参见链接):
public boolean equals(Object obj) {
return (this == obj);
}
也就是说,equals 方法默认只是检查两个对象的内存地址是否相同。我们希望能够从元素和顺序的角度来判断两个 Deque61B 对象是否相等,因此需要重写 equals 方法。
请在 ArrayDeque61B 类和 LinkedListDeque61B 类中重写 equals 方法。有关编写 equals 方法的指导,请参考讲座幻灯片或讲座代码仓库。
注意:您可能会问,为什么我们要在两个类中实现相同的方法,而不是在
Deque61B接口中提供一个default方法。接口不允许提供覆盖Object方法的default方法。更多信息请参见 https://stackoverflow.com/questions/24595266/why-is-it-not-allowed-add-tostring-to-interface-as-default-method。不过,一种变通方法是在
Deque61B接口中提供一个default且不是Object类的辅助方法,然后让实现类来调用它。
在 LinkedListDeque61B 类和 ArrayDeque61B 类中重写 equals() 方法。
重要提示:您不应该使用
getClass,而且也没有必要在equals方法中进行任何类型转换。也就是说,不要使用(ArrayDeque61B) o这样的写法。这种equals方法既过时又过于复杂,请使用instanceof运算符来代替。注意:由于超出本课程范围的原因,
instanceof运算符在处理泛型类型时,行为会有些特殊。例如,如果要检查lst是否为List<Integer>的实例,应该使用lst instanceof List<?>,而不是lst instanceof List<Integer>。遗憾的是,这种方法无法检查元素的具体类型,但已经是我们能做到的最佳方案了。
重要提示:请务必在重写方法时使用 @Override 注解。学生代码中一个常见的错误是尝试重写 equals(ArrayList<T> other) 方法,而不是 equals(Object other) 方法。如果您犯了这个错误,可选的 @Override 注解可以防止代码通过编译。@Override 是一个很好的安全保障。
您不能在此处调用 toList 方法。
toString()
请看下面的代码示例,它会打印出一个 LinkedListDeque61B 对象。
Deque61B<String> lld1 = new LinkedListDeque61B<>();
lld1.addLast("front");
lld1.addLast("middle");
lld1.addLast("back");
System.out.println(lld1);
这段代码的输出类似于 deque.proj1a.LinkedListDeque61B@1a04f701。这是因为 print 语句会默认调用 LinkedListDeque61B 类的 toString 方法。因为你没有重写这个方法,所以会使用默认的实现,如下面的代码所示 (无需理解其具体原理)。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
相应地,由于你也没有重写 hashCode 方法,它会直接返回对象的内存地址,就像上面例子中的 1a04f701 一样。
任务:在 LinkedListDeque61B 和 ArrayDeque61B 类中重写 toString() 方法,使 System.out.println(lld1) 的输出结果为 [front, middle, back]。
提示:Java 的
List接口的实现具有toString方法。提示:有一个单行解决方案(参见提示 1)。
提示:您对
LinkedListDeque61B和ArrayDeque61B的实现应该完全相同。
对象方法测试
虽然我们没有提供这三个对象方法的测试文件,但强烈建议你参考项目 1A 和 1B 中学到的测试技巧,自行编写测试用例。你可以自由地组织这些测试,因为我们不会对它们进行评判。一种可行 (也推荐) 的方式是在 tests 目录下创建 LinkedListDeque61BTest 和 ArrayDeque61BTest 两个文件,类似于 1A 和 1B 中的做法。
MaxArrayDeque61B
在完成 ArrayDeque61B 的实现和测试后,接下来你需要实现 MaxArrayDeque61B。MaxArrayDeque61B 具有 ArrayDeque61B 具有的所有方法,但它还有 2 个附加方法和一个新的构造函数:
public MaxArrayDeque61B(Comparator<T> c):创建一个具有给定Comparator的MaxArrayDeque61B。(您可以为此导入java.util.Comparator。)public T max():返回 deque 中的最大元素,该元素由之前提供的Comparator决定。如果MaxArrayDeque61B为空,则只需返回null。public T max(Comparator<T> c):返回 deque 中的最大元素,该元素由参数Comparator c管理。如果MaxArrayDeque61B为空,则只需返回null。
MaxArrayDeque61B 可以通过使用构造函数中给定的 Comparator<T> 来告诉你自身中的最大元素,也可以使用与构造函数中给定的 Comparator<T> 不同的任意 Comparator<T>。
我们不关心这个类的 equals(Object o) 方法的具体实现,你可以根据自己的理解自由定义。我们不会对此方法进行测试。
对于测试,您可以在自己的测试文件中使用 Comparator.naturalOrder()。此 Comparator 使用 naturalOrder()。如果您的泛型类型是 Integer,您可以使用以下示例创建 MaxArrayDeque61B:
MaxArrayDeque61B<Integer> m = new MaxArrayDeque61B<Integer>(Comparator.naturalOrder());
如果您发现自己首先在
MaxArrayDeque61B文件中复制整个ArrayDeque61B实现,那么你的实现方式可能与预期不符。这是一个关于编写简洁代码的练习,而冗余是我们与复杂性作斗争时最糟糕的敌人之一!有关提示,请重新阅读上面本节的第二句话。
任务:根据上面的 API 填写 MaxArrayDeque61B.java 文件。
对这些附加方法没有运行时要求,我们只关心答案的正确性。有时,MaxArrayDeque61B 中可能存在多个元素都相等,因此都是最大值:在这种情况下,您可以返回其中任何一个,它们将被认为是正确的。
强烈建议你为这部分也编写测试! 您可能会创建多个 Comparator<T> 类来测试您的代码:这就是重点!练习使用 Comparator 对象来做一些有用的事情(找到最大元素),并练习编写您自己的 Comparator 类。您不会提交这些测试,但我们仍然强烈建议您为了自己而制作它们。
你在这一部分实现的 MaxArrayDeque61B 不会在后续部分用到,这部分是一个独立的练习。
吉他英雄
在本项目的这一部分中,我们将创建另一个包,用于使用我们刚刚制作的 deque 包生成合成乐器。我们将有机会使用我们的数据结构来实现一种算法,该算法允许我们模拟拨动吉他弦。
gh2 包
gh2 包只有一个您将编辑的主要组件:
GuitarString类,它使用Deque61B<Double>来实现 Karplus-Strong 算法,以合成吉他弦的声音。
我们已经为您提供了 GuitarString 的骨架代码,您可以在此代码中使用您在本项目第一部分创建的 deque 包。
GuitarString
我们的目标是完成 GuitarString 文件的编写,该文件需要使用 deque 包来模拟拨动琴弦的声音。请注意,此文件中使用了 "buffer" 一词,在这里它与 "deque" 是同义词。
我们将使用 Karplus-Strong 算法,该算法很容易用 Deque61B 实现。它仅包含以下三个步骤:
- 将
Deque61B中的每个元素替换为随机噪声(范围在 -0.5 到 0.5 之间的double值)。 - 播放
Deque61B队首的double值。 - 删除
Deque61B队首的double值,并计算它与下一个double值(提示:使用removeFirst()和get()获取)的平均值,然后将该平均值乘以 0.996 的能量衰减因子(我们将这个结果称为newDouble)。然后,将newDouble添加到Deque61B的后面。返回到步骤 2(并永远重复)。
举例来说,如果 Deque61B 如图所示,我们会播放 0.2,将其移除,然后将它与 0.4 结合,计算得到 0.2988,最后将 0.2988 添加回去。

您可以使用 StdAudio.play 方法播放 double 值。例如,StdAudio.play(0.333) 将告诉扬声器的振膜将其自身延伸到其总行程的 1/3,StdAudio.play(-0.9) 将告诉它将其小心脏向后拉伸到几乎可以达到的程度。扬声器振膜的运动会引起空气振动。如果您能制造出和谐的振动模式,这些振动就会被您的大脑感知为悦耳的声音,这要归功于数十亿年的进化。有关更多信息,请参见此页面。如果您只是执行 StdAudio.play(0.9) 并且不再播放任何内容,则图像中显示的振膜将仅静止地位于向前 9/10 的位置。
请完成 GuitarString.java 的编写,使其能够实现 Karplus-Strong 算法。请注意,您需要在 GuitarString 构造函数中,用零值初始化 Deque61B 缓冲区。部分初始化过程将由 GuitarString 类的客户端来完成。您只需要完成标有 TODO 的任务。
不要在
GuitarString.java中调用StdAudio.play。这会导致自动评分器中断。GuitarPlayer.java已经为您执行此操作。
和往常一样,请确保添加必要的库文件,否则 IntelliJ 将无法识别 StdAudio。
例如,提供的 TestGuitarString 类提供了一个示例测试 testPluckTheAString,该测试尝试在吉他弦上播放 A 音符。如果运行该测试,则在运行此测试时应该听到 A 音符。如果没有,则应尝试 testTic 方法并从那里进行调试。考虑向 GuitarString.java 添加 print 或 toString 方法,这将有助于您了解两次滴答之间发生的情况。
注意:虽然我们在这里提到了 Deque61B,但并未明确指定要使用的 Deque61B 具体实现。这是因为我们只需要 addLast、removeFirst 和 get 这些操作,而所有实现了 Deque61B 接口的类都支持这些操作。因此,您可以自由选择 LinkedListDeque61B 或 ArrayDeque61B 来作为具体的实现方案。强烈建议您思考一下选择 LinkedListDeque61B 和 ArrayDeque61B 各自的优缺点,并与朋友讨论哪种方案更合适,或者它们是否都能满足需求。
为什么它有效
Karplus-Strong 算法能够有效运作,主要归功于两个关键组成部分:环形缓冲区的反馈机制和平均运算。
- 环形缓冲区反馈机制——环形缓冲区模拟了能量在其中来回传递的介质,即一根两端固定的弦。环形缓冲区的长度决定了所得声音的基频。从声音的角度来看,反馈机制只会增强基频及其谐波(频率为基频的整数倍)。能量衰减因子(本例中为 0.996)模拟了波在弦上来回传播时能量的轻微损耗。
- 平均运算。平均运算相当于一个缓和的低通滤波器(它会滤除较高频率,同时允许较低频率通过,因此而得名)。正因其位于反馈路径中,它能逐渐衰减较高谐波,同时保留较低谐波,这与拨动吉他弦所发出的声音非常相似。
GuitarHeroLite
现在,您也可以使用 GuitarHeroLite 类了。运行该类会提供一个图形界面,让用户(也就是您!)可以通过 gh2 包中的 GuitarString 类进行交互式声音播放。
提交
提交项目时,请先添加并提交您的文件,然后将其推送到远程仓库。之后,前往 Gradescope 上对应的作业页面进行提交。
此作业的自动评分器将具有以下速度限制方案:
- 从项目发布到截止日期期间,您将拥有 4 个令牌,每个令牌每 24 小时刷新一次。
评分
类似于 Project 0,此项目也分为多个独立的组成部分;您必须完全正确地实现每个部分,才能获得相应的分数。
LinkedListDeque61B对象方法 (20%):正确实现LinkedListDeque61B中的iterator、equals和toString方法。ArrayDeque61B对象方法 (20%):正确实现ArrayDeque61B中的iterator、equals和toString方法。MaxArrayDeque61B功能 (5%):确保您的MaxArrayDeque61B正确运行Deque61B接口中的所有方法。MaxArrayDeque61BMax (35%):在MaxArrayDeque61B中正确实现max。GuitarString(20%):正确实现GuitarString客户端类。
总而言之,Project 1c 价值 10 分。
鸣谢
- 环形缓冲区图来自 Wikipedia。
- 本次作业改编自 Kevin Wayne 的 Guitar Heroine 作业,链接为 [http://nifty.stanford.edu/2012/wayne-guitar-heroine/]。