CS61B Lec6、7、8 Lab3

120 阅读8分钟

Lec6

DLList 双重链表

可以通过增加last变量指向尾部节点来加快addLast的速度 使其无需遍历整个列表就可以增加尾部节点

public class SLList {
    private IntNode sentinel;
    private IntNode last;
    private int size;    

    public void addLast(int x) {
        last.next = new IntNode(x, null);
        last = last.next;
        size += 1;
    }
    ...
}

但是如果想删除尾部节点removeLast 就没那么容易了 需要找到倒第二个节点指向null 由此类推还要找到倒第三个节点 屏幕截图 2024-02-16 235414.png 解决办法就是在IntNode前面加上一个前指针 也称为双重链表

public class IntNode {
    public IntNode prev;
    public int item;
    public IntNode next;
}

然后再添加一个哨兵节点
image.png
或者使前后指针指向同一个节点
image.png

泛型

如果想在列表中保存其他类型的数据 比如DLList d2 = new DLList("hello");
就要使用泛型 在类声明时类的名称后添加一个<引用类型> 这样就允许添加其他任何引用类型的数据

public class DLList<引用类型> {
    private IntNode sentinel;
    private int size;

    public class IntNode {
        public IntNode prev;
        public 引用类型 item;
        public IntNode next;
        ...
    }
    ...
}

然后实例化 比如

DLList<String> d2 = new DLList<>("hello");
d2.addLast("world");

需要注意的是 泛型仅适用于引用数据类型 如果是基本数据类型 则在<>内使用Integer Double Character Boolean Long Short Byte Float代替基本数据类型 比如

DLList<Integer> d1 = new DLList<>(5);
d1.insertFront(10);

image.png

Arrays 数组

创建数组的三种方法

  • x = new int[3];创建一个长度为3的数组 默认填充0
  • y = new int[]{1, 2, 3, 4, 5};创建包含五个特定元素的长度为5的数组
  • int[] z = {9, 10, 11, 12, 13};创建的同时声明包含五个特定元素的长度为5的数组

复制数组arraycopy

System.arraycopy(b, 0, x, 3, 2);
  • 要用作源的数组
  • 在源数组中从何处开始
  • 要用作目标的数组
  • 在目标阵列中从何处开始
  • 要复制的项目数

二维数组

int[][] matrix;
matrix = new int[4][];
matrix = new int[4][4];

image.png

Lec7

AList 数组

使用数组构建一个支持addLast getLast get size的AList类

  • 要插入的下一项的位置始终为addLast[size]
  • AList中的项数始终为 size
  • 列表中最后一项的位置始终为addLast[size - 1]
public class AList {
    private int[] items;
    private int size;

    /** Creates an empty list. */
    public AList() {
        items = new int[100];
        size = 0;
    }

    //调整数组的大小为任意容量
    //在多次调整数组大小的情况下 可能要花费很多时间以及造成内存浪费
    private void resize(int capacity){
        int[] a = new int[capacity];
        System.arraycopy(items, 0, a, 0, size);
        items = a;
    }

    /** Inserts X into the back of the list. */
    public void addLast(int x) {
        if(size == items.length){//如果数组越界就要修改数组大小
            resize(size + 1);
        }
        items[size] = x;
        size +=1;
    }

    /** Returns the item from the back of the list. */
    public int getLast() {
        return items[size - 1];
    }
    /** Gets the ith item in the list (0 is the front). */
    public int get(int i) {
        return items[i];
    }

    /** Returns the number of items in the list. */
    public int size() {
        return size;
    }

    /** Deletes item from back of the list and
     * returns deleted item. */
    public int removeLast() {
        int x = getLast();
        items[size - 1] = 0;//不是必要的
        size -= 1;
        return x;
    }
} 

数组中类似泛型的操作

Glorp[] items = (Glorp []) new Object[8];

Lec8

使SLList和AList具有相同功能的方法

重载

方法同名但参数列表不同时(返回列表无所谓) java会根据参数类型判断调用哪个方法 但也会造成代码臃肿 不便修改的问题

public static String longest(SLList<String> list)
public static String longest(AList<String> list)

接口 实现 重写

  • 接口interface 一种抽象类 只包含常量和方法的定义 具体方法实现要加default关键字
    public class AList<Item> {...}
  • 实现implements 拥有接口中所指定的方法
    public class AList<Item> implements List61B<Item>{...}
  • 重写override 在子类中把父类的方法重写一遍(方法名参数列表返回类型都相同) @Override

image.png

image.png

public interface List61B<Item> {//创建List61B
    public void addFirst(Item x);//声明抽象方法
    public void add Last(Item y);
    public Item getFirst();
    public Item getLast();
    public Item removeLast();
    public Item get(int i);
    public void insert(Item x, int position);
    public int size();
    default public void print() {//接口中具体方法要加default关键字
        for (int i = 0; i < size(); i += 1) {
            System.out.print(get(i) + " ");
        }
        System.out.println();
    }
}

public class SSList<Item> implements List61B<Item>{//SSList继承List61B
    @Override//重写print方法
    public void print() {
        for (Node p = sentinel.next; p != null; p = p.next) {
            System.out.print(p.item + " ");
        }
    }
}

下面的代码运行时 将创建SLList 并且其地址存储在someList变量中 然后将字符串“elk”插入到 addFirst引用的SLList中

public static void main(String[] args) {
    List61B<String> someList = new SLList<String>();
    someList.addFirst("elk");
}

若同一类中有同名方法 Java会检参数的静态类型并调用相符的方法 但不适用于重载方法

public static void peek(List61B<String> list) {
    System.out.println(list.getLast());
}
public static void peek(SLList<String> list) {
    System.out.println(list.getFirst());
}

SLList<String> SP = new SLList<String>();
List61B<String> LP = SP;//静态类型根据当前引用的对象的类型而变化

peek(SP);//调用第二个peek方法
peek(LP);//调用第一个peek方法

Lab3

计时测试

在这部分中 我们要进进行三部分的计时测试 包括

  1. 使用加法调整策略的AList的addLast方法
  2. 使用乘法调整策略的AList的addLast方法
  3. SLList的getLast方法

加法AList

我们的目标是为加法型AList的addLast方法添加不同规格的数据并进行计时 打印一个表格 如下图
image.png

  • 第一栏N给出数据结构的大小(它包含多少个元素)
  • 第二栏time (s)给出完成所有操作所需的时间
  • 第三栏ops给出调用addLast的次数(同N 调用多少次就有多少个元素)
  • 第四栏microsec/op给出完成每次调用平均花费的微秒

tips

  • 不同规格的数据添加新元素时所花费的时间并不相同 规格越大所需时间越多
  • 调用printTimingTable(AList<Integer> Ns, AList<Double> times, AList<Integer> opCounts)方法可打印表格 分别代表前三列 最后一列自动计算得出
  • 使用Stopwatch类计算时间
Stopwatch sw = new Stopwatch(); // 开始计时 
timeInSeconds = sw.elapsedTime(); // 计时结束

实现代码

public static void timeAListConstruction() {
        // TODO: YOUR CODE HERE
        //创建三个数组分别储存数据结构的大小 所需时间 调用次数
        AList<Integer> Ns = new AList<>();
        AList<Double> times = new AList<>();
        AList<Integer> opCounts = new AList<>();

        for(int i = 1000; i <=128000; i *=2) {
            //分别创建大小为1000-128000的AList
            Ns.addLast(i);
            opCounts.addLast(i);
            AList L = new AList<>();
            Stopwatch sw = new Stopwatch();//开始计时
            for(int j = 1; j <= i; j++){//调用n次addLast
                L.addLast(i);
            }            
            Double time = sw.elapsedTime();//停止计时
            times.addLast(time);
        }
        printTimingTable(Ns, times, opCounts);//打印表格
    }
}

屏幕截图 2024-02-28 182154.png

乘法AList

调整addLast策略

public void addLast(Item x) {
        if (size == items.length) {
            resize((int) (size * 1.01));
        }

        items[size] = x;
        size = size + 1;
    }

调用方法后我们会发现 非常快!
image.png

SLList的getLast方法

我们想知道getLast方法的运行时间如何依赖于数据结构的大小

  1. 创建一个SLList
  2. 添加N个项目到SLList
  3. 启动计时器。
  4. 执行M次getLast操作
  5. 停止计时

实现代码

public static void main(String[] args) {
        timeGetLast();
    }

    public static void timeGetLast() {
        // TODO: YOUR CODE HERE
        //创建三个数组分别储存数据结构的大小 所需时间 调用次数
        AList<Integer> Ns = new AList<>();
        AList<Double> times = new AList<>();
        AList<Integer> opCounts = new AList<>();
        int M = 10000;

        for(int i = 1000; i <= 128000; i *=2){
            //分别创建大小为1000-128000的SLList
            Ns.addLast(i);
            opCounts.addLast(M);
            SLList L = new SLList<>();
            //添加N个项目到L
            for(int j = 1; j <= i; j++){
                L.addLast(j);
            }
            Stopwatch sw = new Stopwatch();//开始计时
            for(int j = 1; j <= M; j++){//执行M次getLast操作
                int k = (int) L.getLast();
            }
            Double time = sw.elapsedTime();//结束计时
            times.addLast(time);
        }
        printTimingTable(Ns, times, opCounts);//打印表格
    }
}

image.png

随机比较测试

创建一个简单的比较测试
其中AListNoResizing类是正确的 BuggyAList类是可能有问题的

public void testThreeAddThreeRemove(){
    AListNoResizing<Integer> correct = new AListNoResizing<>();
    BuggyAList<Integer> buggy = new BuggyAList<>();

    correct.addLast(4);
    correct.addLast(5);
    correct.addLast(6);

    buggy.addLast(4);
    buggy.addLast(5);
    buggy.addLast(6);

    assertEquals(correct.size(), buggy.size());
    assertEquals(correct.removeLast(), buggy.removeLast());
    assertEquals(correct.removeLast(), buggy.removeLast());
    assertEquals(correct.removeLast(), buggy.removeLast());
}

条件断点

调试时右键单击断点 可以设置条件 image.png

点击resume按钮 可以每次都执行到断点的位置
image.png

复杂随机测试

在随机测试里添加getLastremoveLast操作
tips

  • 使StdRandom.uniform选择0到3之间的数字 分别对应addLast size removeLast getLast方法
  • size为0时应跳过getLastremovelast方法
  • size为1000时应跳过addLast方法 因为L无法调节数组大小 超过size继续添加会越界
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);
            if (operationNumber == 0 && L.size() < 1000) {
                // addLast
                int randVal = StdRandom.uniform(0, 100);
                L.addLast(randVal);
                B.addLast(randVal);
                System.out.println("addLast(" + randVal + ")");
            } else if (operationNumber == 1) {
                // size
                int size = L.size();
                int size1 = B.size();
                System.out.println("size: " + size);
                assertEquals(size, size1);
            } else if (operationNumber == 2) {
                // removeLast
                if (L.size() == 0) {
                    continue;
                }
                int RemoveLast = L.getLast();
                int RemoveLast1= B.getLast();
                System.out.println("removeLast: " + RemoveLast);
                assertEquals(RemoveLast, RemoveLast1);
            } else if (operationNumber == 3) {
                // getLast
                if (L.size() == 0) {
                    continue;
                }
                int Last = L.getLast();
                int Last1 = B.getLast();
                System.out.println("getLast: " + Last);
                assertEquals(Last, Last1);
            }
        }
    }
}

执行测试 可能成功也可能失败 增大N的值 可发现几乎每次都是失败(随机测试可能无法检测到错误

执行断点

工作遇到异常时停止代码并可视化异常的情况

  1. 单击运行--查看断点
    image.png
  2. 勾选任何异常--条件--输入具体异常(从JUnit给出的异常问题中复制粘贴 image.png
  3. 进行调试 找出异常原因并修改代码

用断点找出错误并修复

运行测试后 发现如下的错误信息

java.lang.ArrayIndexOutOfBoundsException: Index 1000 out of bounds for length 1000

执行断点操作
image.png
B经过removeLast操作后size应变为14 但resize中却变成了3 发现是removeLast方法中调用的resize方法出现了问题 新建的数组太小 无法将原来的数组复制过去 原来是缩小数组长度时 把items.length/4写成了size/4

public Item removeLast() {
    //为提高空间利用率 当size小于数组长度的四分之一时 将数组长度缩小为原来的四分之一
    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;
}