CS61B SP21 Lab3

474 阅读6分钟

Introduction

本实验的目的:

  1. 我们将测试AList 和 SLList 运行各种方法时, 所花费的时间.
  2. 了解调试器的三个新的功能: 条件断点, 恢复按钮, 执行断点.

List61B的测试部分

内容: timingtest包中的代码.

计时AList

AList操作的数据是一整块空间, 我们随时可能会遇到空间不够, 创造新空间的情况. 每一次调整空间, 我们有两种方法:

  1. 加法调整
  2. 乘法调整

在本包中, 我们使用的是加法调整大小的策略.

本部分的实验, 我们的目的是编写代码, 测试不同情况的ALList实例中AddList方法的大小. (AddLast 遇到空间不足, 会建立新的数组分配空间.)

image.png

其中总共有4列:

  1. 第一列N: 数据结构的大小(即存储的元素的个数 == size)
  2. 第二列s: 完成所有的操作所需要的时间
  3. 第三列#ops: 调用addLast的次数
  4. 第四列microsec/op: 在ops次操作下, 平均每次调用花费多长时间.

tips:

  1. 其中N和#ops是重复的, 因为我们会从构造一个数据结构开始实验. 所以说调用了多少次addLast == 存储了多少元素.
  2. 每个机器关于运行时间都会有差异.

结论1: addLast不是恒定时间, 每次调用会随着列表的大小而产生很大的变化. 128000次调用平均时间374ms, 1000次调用平均时间0.2ms.

任务: 在TimeAList类中添加一个函数public void timeAListConstruction函数, 该函数会生成表格.

提供的方法:
printTimingTable(AList<Integer> Ns, AList<Double> times, AList<Integer> opCounts)
参数:

NS列表: 第一列(元素个数).

times列表: 第二列(花费时间).

opCounts列表: 第三列(操作次数).

而第四列会自动计算.

结果:

打印时间测试表, 以0位基准, i行对应列表中的第i个元素.

关于存储的时间, 可以使用Stopwatch类计算.

使用方法:

  1. 建立Stopwatch实例的时候, 计时开始
  2. 使用Stopwatch实例的elapsedTIme()方法时, 计时结束
Stopwatch sw = new Stopwatch();  // 开始计时
int fib41 = fib(41);
double timeInSeconds = sw.elapsedTime();  // 计时结束
完成该任务的过程:
  1. 构造AList实例
  2. 开始计时
  3. 执行次数
  4. 得到计时的时间
  5. 将元素数量, 次数, 执行的时间全部添加到NS, times, opCount列表中
编写的测试实例:
private final static int thousand = 1000;

public static void timeAListConstruction() {
    // TODO: YOUR CODE HERE

    AList Ns = new AList<Integer>();
    AList times = new AList<Double>();
    AList opCounts = new AList<Integer>();

    for (int i = 1 * thousand; i <= 1000 * thousand; i += 100 * thousand) {
        addItem(i, Ns, times, opCounts);
    }

    printTimingTable(Ns, times, opCounts);
}

/**
 *  增加n个元素, Ns收集元素个数, times收集时间, opCount收集操作次数.
 * */
public static void addItem(int n, AList<Integer> Ns, AList<Double> times, AList<Integer> opCounts){
    AList test = new AList();

    Stopwatch sw = new Stopwatch();

    for (int i = 0; i < n; i += 1) {
        test.addLast(i);
    }

    Double elapsed_time = sw.elapsedTime();
    Ns.addLast(n);
    opCounts.addLast(n);
    times.addLast(elapsed_time);
}
对应不同的情况的表现:
  1. 对于resize(size * 1.5)

image.png

  1. 对于resize(size + 1000)

image.png

结论:

如果需要进行多次的插入操作, 使用加法调整数组大小, 花费时间巨大. 而如果使用乘法进行调整, 花费时间则会小很多.

计时SLList的getLast

有时, 方法运行时, 会对数据结构有所依赖.

方法:

private final static int THOUSAND = 1000;

public static void timeGetLast() {
    // TODO: YOUR CODE HERE

    AList Ns = new AList<Integer>();
    AList times = new AList<Double>();
    AList opCounts = new AList<Integer>();


    for (int i = 10000; i <= 100 * THOUSAND; i += 10 * THOUSAND) {
        getItem(i, Ns, times, opCounts);
    }


    printTimingTable(Ns, times,opCounts);
}
/**
 *  增加n个元素 == 对nth元素进行getLast
 *  Ns收集元素个数. opCount收集增加的元素的个数.
 *  times收集花费的时间.
 * */
public static void getItem(int n, AList<Integer> Ns, AList<Double> times, AList<Integer> opCounts ){
    /*create an SLList*/
    SLList list = new SLList<Integer>();

    for(int i = 0; i < n; i++){
        list.addLast(1);
    }
    
    Stopwatch sw = new Stopwatch();
    for(int i = 0; i < 10 * THOUSAND; i++){
        list.getLast();
    }
    double timeInSeconds = sw.elapsedTime();


    Ns.addLast(n);
    times.addLast(timeInSeconds);
    opCounts.addLast(n);
}

image.png

结论:

如果使用的是链表形式, 随着链表的变大, 对于最后一个元素的存取效率会降低很多.

如果想要让getLast加快, 可以增加一个对于最后一个结点的引用. 即可以直接找到最后一个结点.

Randomized Comparison Tests

对于这一次的实验, 确定使用的是randomtest包中的代码, 而不是timingtest包中的代码.

使用的测试方法: 比较测试

我们有同一个类的两种实现. 一种正确, 一种正在开发, 未得验证. 比较两者, 发现错误.

提供的类:

  1. ALIstNoReasizing: 不支持任何调整大小的操作, 只拥有1000个元素, 且永远无法容纳超过1000个元素.
  2. BuggyAList: 可以根据数据量调整大小.

测试方法:

简单测试:

两个类各增加三个元素, 然后移出最后一个元素, 查看结果是否相同.

该方法测试强度不够, 不足以发现问题所在.

public class testThreeAddThreeRemove {

    private BuggyAList test;
    private AListNoResizing correct;

    @Test
    public void add() {
        test = new BuggyAList();
        correct = new AListNoResizing();

        for (int i = 0; i < 3; i += 1) {
            test.addLast(i);
            correct.addLast(i);
        }

        assertEquals(correct.removeLast(), test.removeLast());
    }
}

随机函数调用

对两个类进行随机调用 比较多次, 并且使用JUnit的方法验证它们是否使用返回相同的值.

begguer特点
  1. resume | 恢复:

作用: 继续执行, 直到下一次的断点处 image.png

  1. conditional breakpoints | 条件断点

作用: 给断点增加条件, 增加到满足该条件的断点处. image.png 右击断点, 填写condition

StdRandom.uniform(start, end): 返回[start, end)内的随机整数.

  1. Execution Breakpoints | 执行断点

作用: 当遇到异常时, 我们停止代码, 并可视化代码崩溃的情况.

工作流程:

  1. 点击 Run -> View Breakpoints

image.png

  1. 勾选 any excepttion -> 然后在Condition中输入 具体的异常(可以从JUnit给出的异常问题中复制粘贴)

image.png

任务1:
  1. 随机的数值从0和1 变成 0, 1, 2, 3
  2. 添加getLast方法 和 removeLast方法 的 测试.
考虑因素:

因为数据结构的限制, getLast 和 removeLast方法执行时, size必须大于0, 否则会报错.

    int operationNumber = StdRandom.uniform(0, 4);
        // addLast
    if (operationNumber == 0) {
        int randVal = StdRandom.uniform(0, 100);
        L.addLast(randVal);
        System.out.println("addLast(" + randVal + ")");
        // 1: size
    } else if (operationNumber == 1) {
        int size = L.size();
        System.out.println("size: " + size);
        // 2: getLast
    } else if (L.size() > 0 && operationNumber == 2) {
        int res = L.getLast();
        System.out.println("getLast: " + res);
        // 3: removeLast
    } else if (L.size() > 0 && operationNumber == 3) {
        int res = L.removeLast();
        System.out.println("removeLast: " + res);
    }

}
任务2:
  1. 在测试中增加BuggyList的方法, 并对其和AListNoRresizing的方法所得结果进行比较, 找出最后的问题到底是什么
  2. 可以使用执行断点的方法找出是什么导致的异常.
@Test
// i can't think of position where error happens.
public void randomizedTest() {
    AListNoResizing<Integer> L = new AListNoResizing<>();
    BuggyAList<Integer> B = new BuggyAList<>();

    int N = 5000;
    for (int i = 0; i < N; i += 1) {
        int operationNumber = StdRandom.uniform(0, 4);
            // addLast
        if (operationNumber == 0) {
            int randVal = StdRandom.uniform(0, 100);
            L.addLast(randVal);
            B.addLast(randVal);
            System.out.println("addLast(" + randVal + ")");
            // 1: size
        } else if (operationNumber == 1) {
            int size = L.size();
            int size1 = B.size();
            System.out.println("size: " + size);
            assertEquals(size, size1);
            // 2: getLast
        } else if (L.size() > 0 && operationNumber == 2) {
            int res = L.getLast();
            int res1 = B.getLast();
            System.out.println("getLast: " + res);
            assertEquals(res, res1);
            // 3: removeLast
        } else if (L.size() > 0 && operationNumber == 3) {
            int res = L.removeLast();
            int res1 = B.removeLast();
            System.out.println("removeLast: " + res);
            assertEquals(res, res1);
        }
    }
}

通过执行断点发现的异常:

image.png remove导致的resize方法出现了错误.

新建的数组不足以将原有的数组的元素复制到新的数组中.

经过排查发现:

本来想要size < 底层数组的长度1/4时, 提高空间利用率, 减少分配的数组容量. 将数组改变成原来的数组的1/4

但是在编写代码的过程中, 新数组的长度, 写成了size / 4. 即需要使用的数组空间的1/4.

修改后的代码:

    public Item removeLast() {
        if ((size < items.length / 4) && (size > 4)) {
//            resize(size / 4);
            resize(items.length / 4);
        }
        Item x = getLast();
        items[size - 1] = null;
        size = size - 1;
        return x;
    }