CS61B项目练习笔记(三)-Proj1
The Deque API
Deque(通常发音为“deck”)是双端队列的不规则首字母缩写。双端队列是具有动态大小的序列容器,可以在两端(其前端或后端)扩展或收缩。
具体而言,任何 deque 实现都必须具有以下操作:
public void addFirst(T item)
:将某个类型的T
项目添加到 deque 的前面。你可以假设这item
从来都不是null
.public void addLast(T item)
:将类型的T
项目添加到 deque 的背面。你可以假设这item
从来都不是null
.public boolean isEmpty()
:如果 deque 为空,则返回,false
否则返回true
。public int size()
:返回 deque 中的项数。public void printDeque()
:从头到尾打印 deque 中的项目,用空格分隔。打印完所有项目后,打印出新行。public T removeFirst()
:删除并返回 deque 前面的项目。如果不存在此类项,则返回null
。public T removeLast()
:删除并返回 deque 背面的项目。如果不存在此类项,则返回null
。public T get(int index)
:获取给定索引处的项,其中 0 是前面,1 是下一个项,依此类推。如果不存在此类项,则返回null
。不得改动deque!
此外,我们还希望我们的两个 Deque 实现这两个特殊方法:
public Iterator<T> iterator()
:我们将要创建的 Deque 对象是可迭代的(即Iterable<T>
),因此我们必须提供此方法来返回迭代器。public boolean equals(Object o)
:返回参数o
是否等于 Deque。o
如果它是 Deque,并且如果它以相同的顺序包含相同的内容(由泛型T
equals
方法支配)则被认为是相等的。(添加 2/12:您需要为此使用instance of
关键字。阅读here了解更多信息)
添加 2/12:您不应该实现接口
Deque
,而应该只实现Iterable
两个实现LinkedListDeque
和ArrayDeque
.如果您执行前者,我们的自动评分器将为您提供 API 错误。您将在第 11 讲 (2/12) 中了解 an
Iterator
是什么,所以现在不要担心。这个项目旨在随着您从讲座和讨论中学到更多东西而一点一点地完成,这是练习您在本课程中学到的所有东西的绝佳机会。您的类应该接受任何泛型类型(而不仅仅是整数)。有关创建和使用泛型数据结构的信息,请参阅 lecture 5。请务必密切注意最后一张幻灯片中关于泛型的经验法则。
在此项目中,您将为 Deque 接口提供两种实现:一种由链表提供支持,另一种由调整大小数组提供支持
项目任务
Linked List Deque
作为第一个 deque 实现,您将构建基于 LinkedListDeque
链表的类。
您的操作受以下规则的约束:
add
并且remove
操作不得涉及任何循环或递归。单个此类操作必须花费“恒定时间”,即执行时间不应取决于 deque 的大小。这意味着您不能使用遍历 deque 的所有/大多数元素的循环。get
必须使用迭代,而不是递归。size
必须花费恒定的时间。LinkedListDeque
循环使用 for-each 循环所需的时间应与项目数成正比。- 不要保留对不再在 deque 中的项目的引用。程序在任何给定时间使用的内存量必须与项数成正比。例如,如果将 10,000 个项目添加到 deque,然后删除 9,999 个项目,则生成的内存使用量应等于 1 个项目的 deque,而不是 10,000。请记住,Java 垃圾收集器将在且仅当没有指向该对象的指针时为我们“删除”内容。
实现上面“Deque API”部分中列出的所有方法。
此外,您还需要实现:
public LinkedListDeque()
:创建一个空的链表取消。public T getRecursive(int index)
:与 get 相同,但使用递归。
虽然这听起来很简单,但有许多设计问题需要考虑,您可能会发现实现比您预期的更具挑战性。请务必查阅有关双向链表的讲座,特别是关于哨兵节点的幻灯片:两个哨兵拓 two sentinel topology扑和圆形哨兵拓扑circular sentinel topology。我更喜欢循环方法。
答案:
1 | package deque; |
Array Deque
对于此实现,您的操作受以下规则的约束:
add
并且remove
必须花费恒定的时间,除非在调整大小操作期间。get
并且size
必须花费恒定的时间。- 数组的起始大小应为 8。
- 程序在任何给定时间使用的内存量必须与项数成正比。例如,如果将 10,000 个项目添加到 deque,然后删除 9,999 个项目,则不应仍使用长度为 10,000ish 的数组。对于长度为 16 或更大的数组,使用系数应始终至少为 25%。这意味着,在执行将数组中的元素数控制在数组长度的 25% 以下的删除操作之前,应减小数组的大小。对于较小的阵列,使用系数可以任意降低。
实现上面“Deque API”部分中列出的所有方法。
此外,您还需要实现:
public ArrayDeque()
:创建一个空数组 deque。
您将需要以某种方式跟踪哪些数组索引包含 Deque 的正面和背面元素。我们强烈建议您在本练习中将数组视为循环数组。换言之,如果你的前面项位于零位置,而 u addFirst
,则新的前面应该循环回数组的末尾(因此 deque 中的新前面项将是基础数组中的最后一个项)。与非循环方法相比,这将导致更少的头痛。有关更多详细信息,请参阅项目 1 project 1 demo slides 演示幻灯片。
答案
1 | package deque; |
Deque Interface(接口)
回想一下,我们通过以下方法定义了 Deque
API 或行为:
1 | public void addFirst(T item) |
由于程序将依赖于此行为,因此提供什么 Deque
实现对它来说并不重要,或者 LinkedListDeque
, ArrayDeque
并且应该同时适用于两者。为了实现这一点,我们将使用接口的强大功能。
在包含上述所有方法的新 Deque.java
文件中创建一个接口。在 IntelliJ 中,使用“New → Java Class”。IntelliJ 会假设你想要一个类,所以一定要把 class
关键词换成 interface
。不要忘记声明 Deque
接口是包的一部分 deque
!
修改你的 LinkedListDeque
或 ArrayDeque
,以便它们通过添加到 implements Deque<T>
声明类存在的行来实现 Deque
接口。
1 | public class ArrayDeque<T> implements Deque<T> |
向覆盖方法的每个 Deque
方法添加 @Override
标记。
现在,在接口中 Deque
,给出 isEmpty()
一个 default
实现,如果 size()
是 0
,则返回 true
.由于 your LinkedListDeque
和 实现接口 Deque
,给定默认 isEmpty()
实现,您可以从之前实现的 LinkedListDeque
和 ArrayDeque
ArrayDeque
中删除该方法。
现在,在实现 Deque
接口并从 LinkedListDeque
和 ArrayDeque
实现中删除 isEmpty()
方法后,代码将在完整的自动分级器上编译。
1 | package deque; |
Guitar Hero
在项目的这一部分中,我们将创建另一个包,用于使用我们刚刚制作的 deque
包生成合成乐器。我们将有机会使用我们的数据结构来实现一种算法,该算法允许我们模拟吉他弦的弹拨。
The GH2 Package
该 gh2
包只有一个主要组件,您将对其进行编辑:
GuitarString
,该类使用Deque<Double>
来实现 Karplus-Strong 算法 来合成吉他弦声音。
我们为您提供了框架代码 GuitarString
,您将在其中使用在本项目第一部分中创建的 deque
包。
GuitarString
我们想要完成 GuitarString
文件,它应该使用包 deque
来复制拨弦的声音。我们将使用 Karplus-Strong 算法,该算法很容易通过 Deque
.
The Karplus-Algorithm is simply the following three steps:
Karplus算法只是以下三个步骤:
- 用随机噪声(
double
值介于 -0.5 和 0.5 之间)替换 aDeque
中的每个项目。 - 去掉
Deque
最前面的double值,并和Deque
下一个double (提示:使用removeFirst()
和get()
)取平均值 然后 乘以能量衰减因子 0.996(我们称之为整个量newDouble
)。然后,添加到newDouble
到Deque
的最后 . - 播放您在步骤2中退出队列的double (newDouble)。回到步骤2(并不断重复)。
或者从视觉上看,如果如 Deque
顶部所示,我们将删除 0.2,将其与 0.4 合并形成 0.2988,添加 0.2988,然后播放 0.2。
您可以使用该 StdAudio.play
方法播放值 double
。例如 StdAudio.play(0.333)
,会告诉扬声器的振膜将自己伸展到其总范围的 1/3, StdAudio.play(-0.9)
会告诉它几乎向后伸展它的小心脏,几乎尽可能远。扬声器振膜的运动会取代空气,如果你以很好的模式取代空气,这些干扰将被你的意识解释为令人愉悦,这要归功于数十亿年的进化。有关详细信息,请参阅此页面。如果你只是这样做 StdAudio.play(0.9)
,再也不玩任何东西,那么图像中显示的隔膜将只是静止不动地前进的 9/10。
完成 GuitarString.java
,以便实现 Karplus-Strong 算法的第 1 步和第 2 步。请注意,您必须在 GuitarString
构造函数中用零填充缓冲区 Deque
。第 3 步将由 GuitarString
类的客户端完成。
1 | package gh2; |
GuitarHero
非作业,单纯好玩
考虑创建一个类似于 GuitarHeroLite
的程序 GuitarHero
,但支持 37Hz 到 110Hz 的半音阶上总共 880 个音符。使用以下 37 个键来表示键盘,从最低音符到最高音符:
1 | String keyboard = "q2we4r5ty7u8i9op-[=zxdcfvgbnjmk,.;/' "; |
这种键盘排列模仿钢琴键盘:“白键”位于键盘的 qwerty 和 zxcv 行上,“黑键”位于键盘的 12345 和 asdf 行上。
字符串键盘的第 i 个字符对应于 的频率,因此字符“q”为 110Hz,“i”为 220Hz,“v”为 440Hz,“”为 880Hz。甚至不要考虑包含 37 个单独的 GuitarString 变量或 37 路 if 语句!相反,创建一个包含 37 GuitarString
个对象的数组,并用于 keyboard.indexOf(key)
确定键入了哪个键。确保如果按下的键与您的 37 个音符之一不对应,您的程序不会崩溃。
1 | package gh2; |
Even More Fun 更有趣
这部分作业没有评分,只是为了好玩。
- Harp 字符串:在包中
gh2
创建一个 Harp 类。在将新值排入队列之前翻转新值的符号tic()
会将声音从吉他式变为竖琴式。您可能希望使用衰减因子来提高真实感,并将缓冲区大小调整两倍,因为tic()
自然共振频率会因变化而减半。 - Drums:在包中
gh2
创建一个 Drum 类。在将新值排入队列之前,以 0.5 的概率翻转新值的符号tic()
将产生鼓声。衰减系数为 1.0(无衰减)将产生更好的声音,您需要调整使用的频率集。 - 吉他在 6 根物理弦之一上弹奏每个音符。为了模拟这种情况,您可以将
GuitarString
实例分成 6 组,当一个字符串被摘取时,将该组中的所有其他字符串清零。 - 钢琴带有一个阻尼踏板,可用于使琴弦静止。您可以通过在按住某个键(例如 Shift)的迭代中更改衰减因子来实现此目的。
- 虽然我们使用了平等的气质,但当音乐音程跟随正音调系统中的小部分时,耳朵会发现它更令人愉悦。例如,当音乐家使用铜管乐器演奏完美的五度和声时,频率之比为 3/2 = 1.5,而不是 27/12 ∼ 1.498。编写一个程序,其中每对连续的音符都只有语调。
Why It Works 为什么有效
使Karplus-Strong算法起作用的两个主要组件是环形缓冲区反馈机制和平均操作。
- The ring buffer feedback mechanism. The ring buffer models the medium (a string tied down at both ends) in which the energy travels back and forth. The length of the ring buffer determines the fundamental frequency of the resulting sound. Sonically, the feedback mechanism reinforces only the fundamental frequency and its harmonics (frequencies at integer multiples of the fundamental). The energy decay factor (.996 in this case) models the slight dissipation in energy as the wave makes a round trip through the string.
环形缓冲区反馈机制。环形缓冲器模拟能量来回传播的介质(两端系在一起的绳子)。环形缓冲器的长度决定了所产生声音的基频。在声音上,反馈机制仅增强基频及其谐波(基频整数倍的频率)。能量衰减因子(在本例中为 .996)模拟了波在弦上往返时能量的轻微耗散。 - The averaging operation. The averaging operation serves as a gentle low-pass filter (which removes higher frequencies while allowing lower frequencies to pass, hence the name). Because it is in the path of the feedback, this has the effect of gradually attenuating the higher harmonics while keeping the lower ones, which corresponds closely with how a plucked guitar string sounds.
平均操作。平均操作用作温和的低通滤波器(它去除较高频率,同时允许较低频率通过,因此得名)。因为它在反馈的路径中,所以具有逐渐衰减高次谐波同时保持低次谐波的效果,这与弹拨吉他弦的声音非常吻合。
总结:
通过项目一,我学到了以下东西:
- 如何构造出一个Deque API
- package的用法
- 接口的创立
- 数列的一个使用实例
- 更高效的调试方法
困难复盘点:
Linked List Deque
这个部分比较困难的是add
和remove
方法,刚开始会错意了,以为环状列表中,最后一个不指向哨兵节点导致一开始尝试错误。
后面比较麻烦的是prev
和next
的指向问题,我借助了可视化+调试慢慢得完善好。
Array Deque
经常遇到NullPointerException
或者OutIndex
这些问题,于是使用了异常断点this instanceof NullPointerException
+可视化工具来和条件断点和恢复按钮判断出出错点
最困难的地方莫过于resize方法
分扩大列表和缩小列表这两种情况来讨论。我的解决方法是先建立一个新的列表然后借助System.arraycopy(src, srcPos, dest, destPos, length)
来快速得复制,其中复制的参数我根据可视化的列表图像和nextFirst
和item.length
慢慢得摸索,最终完成。
其中扩大列表应该分有多种扩增方法,我最开始是直接在后面扩增开了,导致remove
方法无法找到解决方法,最后选择在nextFirst
和nextLast
中间扩增开,这样子remove的时候,nextFirst
的值会慢慢的增加,然后成功删除掉首尾的值。