CS61B Proj1 Deque2: ArrayDeque

149 阅读8分钟

CS61B Proj1: Dqeue Data Structures

如果想要我的测试代码, 可以前往: github.com/?utm_source…

其中random测试可能有时会不通过, 但是CS61B的AG测试成功了. 而equal测试成功了, 但是CS61B的AG没有测试成功.

介绍

Proj1 主要分为两个部分:

  1. 数据结构部分

创建LinkedListDeque.java 和 ArrayDeque.java 文件. 对应两种不同的实现.

并且使用Lab3中学到的测试方法验证其正确性.

当本部分完成之后, 可以使用Proj0 CheckPoint 检查其正确性.

  1. 应用程序部分

创建MaxArrayDeque.java, 并实现能够播放Guitar Hero的声音合成器.

Packages

概念:

包是Java类的集合, 用于协同实现一个目标. 诸如, org.Junit是一个包. Assert是一个类, 其中的assert方法是里面的方法.

方法:

  1. 创建包时, 我们只需要在文件顶部制定代码是包的一部分即可.
package deque;
  1. 使用包时, 只需要引用包即可. 包本质上知识计算机中的某个文件夹.
import deque;

The Deque API

概念:

Deqeue: doubled-ended queue. 双端队列.

双端队列是一个具有动态大小的容器, 可以在其两端收缩或者扩展.

我们需要实现的方法:

  1. public void addFirst(T item)

  2. public void addLast(T item)

  3. public boolean isEmpty()

  4. public int size()

  5. public void printDeque()

  6. public T removeFirst()

  7. public T removeLast()

  8. public T get(int index)

  9. public Iterator iterator()

  10. public boolean equals(Object o)

要求:

  1. 我们不应该让Deque接口实现Iterable, 而应该让其子类实现

  2. 我们需要编写两种不同的子类:

    1. 链表
    2. 可以调整大小的数组
  3. 我们编写的子类, 应该接受任何泛型类型.

Project Tasks

Array Deques 数组双端队列

创建一个ArrayDeque文件.

操作要求:

  1. addremove必须花费恒定的时间, 调整大小的时间除外
  2. get, size必须花费很定的时间
  3. 数组的起始大小为8
  4. 使用的内存量和项目数量成正比. 调整的阈值为0.25

额外使用的方法:

  1. public ArrayDeque()

建议:

  1. 将数组视作为循环数组
  2. 使用追踪的方式保存数组的前部元素, 后部元素.

下图为CS61B建议的实现demo 其中使用nextFirst 和 nextLast 分别记录下一个增加的元素的数组位置.

image.png

因为它是将数组视作为循环数组, 可能出现的问题:

  1. 在起始位置addfirst
  2. 在结束位置addLast
  3. 数组如何扩展或者缩小的起始位置

问题的核心特征: 数字之间的关系是离散的, -1 不能直接到 size + 1, 如何将数字之间的关系设置为连续.

y % x == n 的作用: 将数据中的元素设置为y是在长度为x组中的第n个元素

使用%运算, 我们可以将数字连续起来. -1 为上一组的最后一个元素, x + 1为下一组的第一个元素

可以假想: 位置对于组间的第几个元素才是真正的位置.

实现思路:

  1. 将底层的的数组的长度设置为x
  2. 对于第零个元素addFirst给出的位置为数组.length - 1

操作: (当前位置 + 前进或者后退 + 数组.length) % 数组.length

数据结构草图

image.png

方法:

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()

接下来是本章中最难的部分, 调整数组的长度.

  1. 关于数据抽象, 这个函数位于add和remove的下层.

  2. 关于执行顺序, 这个函数位于一次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基本一致.

  1. 该函数的条件, 和操作的数据

条件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则是指向的内容是最后一个元素.

image.png

为了保证解决复杂性, 我们可以放弃一格空间, 该格后面的元素开始的元素, 该格前面是开始的元素.

image.png

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来判定,

  1. 如果是离散的, nextFirst在nextLsat后面
  2. 如果是连续的, 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)];
}
  1. 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;
    }
}
  1. 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("");
}
  1. 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;
}

不同的操作应该如何考虑如何实现.