CS61B Proj1: Dqeue Data Structures
如果想要我的测试代码, 可以前往: github.com/?utm_source…
其中random测试可能有时会不通过, 但是CS61B的AG测试成功了. 而equal测试成功了, 但是CS61B的AG没有测试成功.
介绍
Proj1 主要分为两个部分:
- 数据结构部分
创建LinkedListDeque.java 和 ArrayDeque.java 文件. 对应两种不同的实现.
并且使用Lab3中学到的测试方法验证其正确性.
当本部分完成之后, 可以使用Proj0 CheckPoint 检查其正确性.
- 应用程序部分
创建MaxArrayDeque.java, 并实现能够播放Guitar Hero的声音合成器.
Packages
概念:
包是Java类的集合, 用于协同实现一个目标. 诸如, org.Junit是一个包. Assert是一个类, 其中的assert方法是里面的方法.
方法:
- 创建包时, 我们只需要在文件顶部制定代码是包的一部分即可.
package deque;
- 使用包时, 只需要引用包即可. 包本质上知识计算机中的某个文件夹.
import deque;
The Deque API
概念:
Deqeue: doubled-ended queue. 双端队列.
双端队列是一个具有动态大小的容器, 可以在其两端收缩或者扩展.
我们需要实现的方法:
-
public void addFirst(T item)
-
public void addLast(T item)
-
public boolean isEmpty()
-
public int size()
-
public void printDeque()
-
public T removeFirst()
-
public T removeLast()
-
public T get(int index)
-
public Iterator iterator()
-
public boolean equals(Object o)
要求:
-
我们不应该让Deque接口实现Iterable, 而应该让其子类实现
-
我们需要编写两种不同的子类:
- 链表
- 可以调整大小的数组
-
我们编写的子类, 应该接受任何泛型类型.
Project Tasks
Array Deques 数组双端队列
创建一个ArrayDeque文件.
操作要求:
- addremove必须花费恒定的时间, 调整大小的时间除外
- get, size必须花费很定的时间
- 数组的起始大小为8
- 使用的内存量和项目数量成正比. 调整的阈值为0.25
额外使用的方法:
- public ArrayDeque()
建议:
- 将数组视作为循环数组
- 使用追踪的方式保存数组的前部元素, 后部元素.
下图为CS61B建议的实现demo 其中使用nextFirst 和 nextLast 分别记录下一个增加的元素的数组位置.
因为它是将数组视作为循环数组, 可能出现的问题:
- 在起始位置addfirst
- 在结束位置addLast
- 数组如何扩展或者缩小的起始位置
问题的核心特征: 数字之间的关系是离散的, -1 不能直接到 size + 1, 如何将数字之间的关系设置为连续.
y % x == n 的作用: 将数据中的元素设置为y是在长度为x组中的第n个元素
使用%运算, 我们可以将数字连续起来. -1 为上一组的最后一个元素, x + 1为下一组的第一个元素
可以假想: 位置对于组间的第几个元素才是真正的位置.
实现思路:
- 将底层的的数组的长度设置为x
- 对于第零个元素addFirst给出的位置为数组.length - 1
操作: (当前位置 + 前进或者后退 + 数组.length) % 数组.length
数据结构草图
方法:
1. public ArrayDeque()
首先我们创建实例变量和构造函数
因为第一个元素的移动不是正常的加减运算, 而是设置了两个分组的模运算, 所以我们将其操作建立抽象, 方便操作.
同时因为是纯函数, 也很方便的编写测试函数. 保证正确性. 只需要:
将private修改为public(测试结束后, 在修改为private)
items.length修改为常数, 然后测试其结果是否和期待值一样即可.
public class ArrayDeque<T> implements Iterable<T>, Deque<T> {
private int size;
private int nextFirst;
private int nextLast;
private static final double FACTOR = 0.25;
private T[] items;
private int aNext(int pos, int ele) {
return (pos + items.length + ele) % items.length;
}
private int addFirst() {
return aNext(nextFirst, -1);
}
private int subFirst() {
return aNext(nextFirst, 1);
}
private int subLast() {
return aNext(nextLast, -1);
}
private int addLast() {
return aNext(nextLast, 1);
}
}
public ArrayDeque() {
items = (T[]) new Object[8];
nextFirst = 0;
nextLast = 0;
size = 0;
}
2. public boolean isEmpty()
Empty是建立在size上的抽象, 只需要使用size即可确定其结果.
public boolean isEmpty() {
return size == 0;
}
3. public int size()
public boolean size() {
return size;
}
4. 调整数组大小的辅助方法 addResizing()
接下来是本章中最难的部分, 调整数组的长度.
-
关于数据抽象, 这个函数位于add和remove的下层.
-
关于执行顺序, 这个函数位于一次add和remove之后. 而导致这个操作的行为是nextFirst和nextLast的执行, 也就是判断这个行为的前置行为是next函数的上层函数的实现
graph LR
remove或者add中的next函数 --> resizing函数
next操作的特殊情况:
1. items.length == 1
2. 0 - 1 == items.length - 1
3. 为了将其复杂性, 我们首先要做的是抽象它的操作, 让其行为对于add和remove基本一致.
- 该函数的条件, 和操作的数据
条件condition: remove是太小该size < items.lenghth * 0.25, 而是add则是太大size == items.length
行为suite: 将原来的数组的元素按原来的顺序, 放入新的数组中.
而后完整的顺序可以建立为
graph LR
remove --> next --> resizing --> 1(remove) --> 3(size == items.length) --> 数组变小
add --> next
resizing --> 2(add) --> 4(size < items.lenghth * 0.25) --> 数组变大
而数组变大后, 想要扩大又出现了新的问题, 是addFirst和addLast哪个方法操作的. 两者关于从何处复制有两种截然不同的方法. addFirst则是nextFirst指向的内容是第一个元素, addLast则是指向的内容是最后一个元素.
为了保证解决复杂性, 我们可以放弃一格空间, 该格后面的元素开始的元素, 该格前面是开始的元素.
graph LR
add --> 不需要进行判断通过哪一种操作,统一空间分配
同时, 对于addnext的操作, 我们也需要考虑, 如果在末尾相加会有什么样的结果.
因此对于增加操作, 我们可以写如下代码
// 增加元素的情况
private void addResizing() {
int newSize = (int) (items.length * (1 + FACTOR));
T[] newItems = (T[]) new Object[newSize];
// 三种情况:
// 1. 在开始
if (nextLast == 0) {
System.arraycopy(items, 1, newItems, 0, size);
}
// 2.在末尾
if (nextLast == items.length - 1) {
System.arraycopy(items, 0, newItems, 0, size);
}
// 3. 在中间
else {
System.arraycopy(items, addLast(), newItems, 0, items.length - addLast());
System.arraycopy(items, 0, newItems, items.length -addLast(), nextFirst);
}
items = newItems;
nextLast = size;
nextFirst = items.length - 1;
}
对于复杂逻辑的代码, 我们可以分若干个纯函数完成, 然后编写测试, 验证逻辑是否正确, 现在我们可以编写addd操作, 检验.
5. public void addFirst(T item) 和 addLast
public void addFirst(T item) {
if (nextFirst == nextLast) {
addResizing();
}
items[nextFirst] = item;
nextFirst = addFirst();
size += 1;
}
public void addLast(T item) {
if (nextFirst == nextLast) {
addResizing();
}
items[nextLast] = item;
nextLast = addLast();
size += 1;
}
6. public T removeFirst()
首先我们编写subResizing, 即将数组变小的时候的调整.
同样, 因为next操作导致的可能性有三种. 处于items的开头, 在items的中间, 在items的末尾.
但是我们只需要subFirst()下一个元素变为0, 这个也很容易解决, 有了之前的经验, 我们只需要很简单的将复制数组的长度为items.length - nextFirst - 1 确定即可(当然以上的addResizing方法也可以如此, 但是为了保留之前的解决思路, 我就不更改了)
现在需要考虑的是从存放deque的元素连续, 还是分为两块的. 可以比较nextLast 和 nextFirst来判定,
- 如果是离散的, nextFirst在nextLsat后面
- 如果是连续的, nextLast在nextFirst后面.
// 減少元素的時候的情況
private void subResizing() {
if (size <= 8) {
return;
}
int newSize = (int) (items.length * FACTOR);
T[] newItems = (T[]) new Object[newSize];
// nf需要subFirst, addFirst在开头相减为特殊情况
if (nextFirst == items.length - 1) {
System.arraycopy(items, 0, newItems, 0, size);
} else if (nextFirst > nextLast) {
System.arraycopy(items, subFirst(), newItems, 0, items.length - nextFirst - 1);
System.arraycopy(items, 0, newItems, items.length - nextFirst - 1, nextLast);
} else if (nextFirst < nextLast) {
System.arraycopy(items, subFirst(), newItems, 0, nextLast - nextFirst - 1);
}
items = newItems;
nextLast = size;
nextFirst = items.length - 1;
}
其次就是很简单的逻辑, 编写remove函数
public T removeFirst() {
// 減少的特殊情況
if (size <= 0) {
return null;
}
if (size <= items.length * FACTOR) {
// 減少元素
subResizing();
}
nextFirst = subFirst();
T item = items[nextFirst];
items[nextFirst] = null;
size -= 1;
return item;
}
public T removeLast() {
// 減少的特殊情況
if (size <= 0) {
return null;
}
if (size <= items.length * FACTOR) {
subResizing();
}
nextLast = subLast();
T item = items[nextLast];
items[nextLast] = null;
size -= 1;
return item;
}
7. public T get(int index)
get操作只需要注意, 元素的位置使用的是next方法即可, 以及nextFirst是存放下一个元素的位置
public T get(int index) {
return items[aNext(subFirst(), index)];
}
- public Iterator iterator()
迭代器只要使用一个记录的元素记录位置即可. 此时因为局部作用域的问题, next覆盖, 所以我们修改了原本ArrayDeque中的next为aNext(), 为了记录这个问题, 所以之前的内容就不修改了.
public Iterator iterator() {
return new ArrayDequeIterator();
}
private class ArrayDequeIterator<T> implements Iterator<T> {
public int index;
@Override
public boolean hasNext() {
return index != size;
}
@Override
public T next() {
T item = (T) items[aNext(subFirst(), index)];
index += 1;
return item;
}
}
- public void printDeque()
public void printDeque() {
if (size == 0) {
return;
}
StringBuilder res = new StringBuilder("");
for (T e: this) {
res.append(e + " ");
}
System.out.println(res);
System.out.println("");
}
- public boolean equals(Object o)
虽然很简单
这个逻辑也比较简单, 但是因为要求LinkedListDeque 和 ArrayDeqeue 元素一致相等, 但是cs61b中提供了instanceof的使用文档, 可以帮助我们解决测试实例和类之间的关系.
@Override
public boolean equals(Object o) {
if (!(o instanceof deque.ArrayDeque)) {
return false;
}
if (o == null) {
return false;
}
if (o == this) {
return true;
}
ArrayDeque<?> lld = (ArrayDeque<?>) o;
if (lld.size() != size) {
return false;
}
for (int i = 0; i < size; i++) {
if (lld.get(i) != get(i)) {
return false;
}
}
return true;
}
不同的操作应该如何考虑如何实现.