CS61B Lec3、4、5 Lab2 Pro0

106 阅读10分钟

Lec3

临时测试

创建一个空白方法

public class Sort {
    //空白的方法体
    public static void sort(String[] x) {
    }
}

编写一个测试来检查方法的输出是否有问题 打印出第一个不匹配并终止测试

class TsetSort {
    //测试Sort类的sort方法
    public static void testSort(){
        String[] input = {"i", "have", "an", "egg"};
        String[] expected = {"an", "egg", "have", "i"};
        Sort.sort(input);
        for(int i=0;i<input.length;i++){
            if (!input[i].equals(expected[i])){
                System.out.println("Mismatch in position " + i + ", expected: " + expected[i] + ", but got: " + input[i] + ".");
                break;
            }
        }
    }
    public static void main(String[] args) {
        testSort();
    }
}

得到的输出是 image.png 说明编写的测试没问题

JUnit测试

JUnit是一个用于调试程序的软件包 org.junit库提供了许多有用的方法和功能用于简化测试的编写 在编写程序时 可以把代码分成一个个部分 然后用junit来测试每个单元的正确性
将上面的简单临时测试替换为

import org.junit.Test;
public class TsetSort {
    //测试Sort类的sort方法
    public static void testSort(){
        String[] input = {"i", "have", "an", "egg"};
        String[] expected = {"an", "egg", "have", "i"};
        Sort.sort(input);
        org.junit.Assert.assertArrayEquals(expected, input);
    }
    public static void main(String[] args) {
        testSort();
    }
}

得到的输出是
image.png

选择排序

编写一个选择排序并验证是否正确

选择排序需要三个步骤

  • 找出最小的项目 findSmallest
  • 将其移到前面(交换位置) swap
  • 对剩余的n-1项进行排序(递归)
public class Sort {
    //把方法组合起来 并使用递归对剩余项进行排序
    public static void sort(String[] x){
        sort(x, 0);
    }

    private static void sort(String[] x, int start){
        if (start == x.length) {
            return;
        }
        int smallestIndex = findSmallest(x, start);
        swap(x, start, smallestIndex);
        sort(x, start + 1);
    }

    //找出最小的项目的索引
    public static int findSmallest(String[] x, int start){
        int smallestIndex = start;
        for(int i = start; i < x.length; i++){
            int cmp = x[i].compareTo(x[smallestIndex]);
            if(cmp < 0){
                smallestIndex = i;
            }
        }
        return smallestIndex;
    }

    //把最小的项目移到前面
    public static void swap(String[] x, int a, int b){
        String temp = x[a];
        x[a] = x[b];
        x[b] = temp;
    }
}

验证选择排序也需要三个步骤

  • 验证是否找出了最小的项目 testFindSmallest
  • 验证是否交换位置 testSawp
  • 验证是否成功排序
import org.junit.Test;
public class TsetSort {
    //测试Sort.sort方法
    @org.junit.Test
    public void testSort(){
        String[] input = {"i", "have", "an", "egg"};
        String[] expected = {"an", "egg", "have", "i"};

        Sort.sort(input);
        org.junit.Assert.assertArrayEquals(expected, input);
    }

    //测试Sort.findSmallest方法
    @org.junit.Test
    public void testFindSmallest() {
        String[] input = {"i", "have", "an", "egg"};
        int expected = 2;

        int actual = Sort.findSmallest(input, 0);
        org.junit.Assert.assertEquals(expected, actual);

        String[] input2 = {"there", "are", "many", "pigs"};
        int expected2 = 2;

        int actual2 = Sort.findSmallest(input2, 2);
        org.junit.Assert.assertEquals(expected2, actual2);
    }

    //测试Sort.swap方法
    @org.junit.Test
    public void testSwap(){
        String[] input = {"i", "have", "an", "egg"};
        int a = 0;
        int b = 2;
        String[] expected = {"an", "have", "i", "egg"};

        Sort.swap(input, a, b);
        org.junit.Assert.assertArrayEquals(expected, input);
    }
}

另外 需要做三件事情

  • 在每个测试方法前加上@org.junit.Test
  • 把每个测试方法改成非静态
  • 删掉TestSort中的main方法
  • 另外 可以导入一些语句缩写 比如 import org.junit.Test; 后只需要@Testimport static org.junit.Assert.* ; 后只需要assertEquals(expected2, actual2); image.png

TDD

驱动测试开发 在编写代码本身之前编写代码测试 步骤如下

  • 标识新功能
  • 为该功能编写单元测试
  • 运行测试 应该是失败的
  • 编写那些通过测试的代码
  • 重构使代码更快更简洁

Lec4

引用变量表示法

数据有八种基本类型:byte short int long float double bool char 其余都是引用类型
Java与c语言不同 我们无法得知数据的内存地址
当我们声明引用类型时 Java会分配一个64位框来储存内存的地址
我们为引用变量创造一个简化的框表示法

  • 如果一个地址全为0 用null表示
    image.png
  • 非零地址将由指向实例化的箭头表示
    image.png

值传递

参数传递总是复制所有位 称为按值传递

  • 基本类型作为参数传递时,传递的是变量的值,在函数内部修改并不影响原变量
  • 引用类型作为参数传递时,是把对象在内存中的地址拷贝了一份传给了参数,不改变地址的情况下,在函数内部修改会影响原变量

图示如下

Walrus a = new Walrus(1000, 8.3);
Walrus b;
b = a;

image.png

列表

比数组更有扩展性
构建一个列表类 其中包括int数值和另一个int列表的指针和地址

public class IntList {
    public int first;
    public IntList rest;
    
    public IntList(int f, IntList r){
        first = f;
        rest = r;
    }
}

想列出数字5.15.20
从前往后

IntList L = new IntList(5, null);
L.rest = new IntList(10, null);
L.rest.rest = new IntList(15,  null);

image.png
或者从后往前

IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);

image.png 编写一个函数 返回列表的长度
使用递归

public int size(){
    if(rest == null){
        return 1;
    }
    return 1 + this.rest.size();
}

不使用递归

public int size(){
    IntList p = this;
    int total = 0;
    while(p != null){
        total++;
        p = p.rest;
    }
    return total;
}

编写一个函数 返回列表的第i个值

public int get(int i){
    if(i == 0){
        return first;
    }
    return rest.get(i - 1);
}

Lec5

SLList 另一种使用列表的方式

现代程序员在使用列表时 往往隐藏递归的结构 而创造一个列表类 丢掉其中的辅助方法 再创造一个用户名类来使用列表并在其中编写辅助方法

public class IntNode {
    public int item;
    public IntNode next;
    
    public IntNode(int i, IntNode n){
        item = i;
        next = n;
    }
}
public class SLList {
    public IntNode first;
    
    //构造函数
    public SLList(int x){
        first = new IntNode(x, null);
    }
    
    //空列表的构造函数
    public SLList() {
        first = null;
        size = 0;
    }
}

与原列表相比 使用起来更简洁

IntList L1 = new IntList(5, null);
SLList L2  = new SLList(5);

辅助方法

添加首项

public void addFirst(int x){
    first = new IntNode(x, first);
}

读取首项

public int getFirst(){
    return first.item;
}

添加尾项

public void addLast(int x){
    //空列表的情况
    if (first == null) {
        first = new IntNode(x, null);
        return;
    }
    
    IntNode p = first;
    //移动p直到列表的末尾
    while(p.next != null){
        p = p.next;
    }
    p.next = new IntNode(x,null);
}

返回列表的长度

private static int size(IntNode p) {
    if (p.next == null) {
        return 1;
    }

    return 1 + size(p.next);
}

与原列表相比 更加易于使用

//SLList
SLList L = new SLList(15);
L.addFirst(10);
L.addFirst(5);
int x = L.getFirst();
//IntList
IntList L = new IntList(15, null);
L = new IntList(10, L);
L = new IntList(5, L);
int x = L.first;

image.png 本质上 该类充当列表用户和递归结构的中介
另外 为了防止访问错误 要把SLList类中的IntNode改成private

public class SLList {
    private IntNode first;
...

有时也会将IntNode作为静态嵌套类放在SLList中 (如果不使用外部类的任何实例成员 请将嵌套类设置为静态

public class SLList {
       public static class IntNode {
            public int item;
            public IntNode next;
            public IntNode(int i, IntNode n) {
                item = i;
                next = n;
            }
       }
       
       private IntNode first;
...
}

缓存

如果通过递归来计算列表大小的话 如果列表太大了 将耗费许多时间 所以可以添加一个属性size 在添加首尾项的时候+1 就可以快速得到列表的大小了 把这种方法称作缓存

public class SLList {
    private IntNode first;
    private int size;

    public SLList(int x){
        first = new IntNode(x, null);
        size = 1;
    }

    public void addFirst(int x){
        first = new IntNode(x, first);
        size++;
    }
    
    public int size(){
        return size;
}

哨兵节点

为了应对空节点的情况并使代码更整洁 创建一个始终存在在最前面的节点 就叫哨兵节点 值无所谓
哨兵节点有以下几个不变量

  • sentinel引用始终指向哨兵节点
  • 前面的项目(如果存在)始终位于sentinel.next.item
  • size变量始终是已添加的项目总数
public class SLList {
    private class IntNode {
        public int item;
        public IntNode next;

        public IntNode(int i, IntNode n){
            item = i;
            next = n;
        }
    }

    private IntNode sentinel;
    private int size;

    public SLList(){
        sentinel = new IntNode(63, null);
        size = 0;
    }

    public SLList(int x){
        sentinel = new IntNode(63, null);
        sentinel.next = new IntNode(x, null);
        size = 1;
    }

    public void addFirst(int x){
        sentinel.next = new IntNode(x, sentinel.next);
        size++;
    }

    public int getFirst(){
        return sentinel.next.item;
    }

    public void addLast(int x){
        IntNode p = sentinel;

        //移动p直到列表的末尾
        while(p.next != null){
            p = p.next;
        }
        p.next = new IntNode(x,null);
        size++;
    }

    public int size(){
        return size;
    }

    public static void main(String[] args) {
        SLList L = new SLList();
        L.addLast(20);
        System.out.println(L.size);
    }
}

Lab2

调试指南调试指南 |CS 61B 2021 年春季 (datastructur.es)

image.png lab2主要讲了一些调试的技巧 junit的使用方法 跟着guide一步步做就可以了

Pro0

4*4棋盘的2048 实现Model类中的四个函数
emptySpaceExists maxTileExists atLeastOneMoveExists tilt
棋盘左下角坐标为(0, 0)

emptySpaceExists (Board b)

  • 功能:如果棋盘上有空位 则返回true
  • 用到Board类中的tile(int col, int row)size()方法 分别返回格子Tile(空格子就返回null)和棋盘的规格
  • 思路:遍历整个棋盘 有null则返回true
public static boolean emptySpaceExists(Board b) {
    // TODO: Fill in this function.
    int size = b.size();
    for(int col = 0; col < size; col++){
        for(int row = 0; row < size; row++){
            if(b.tile(col, row) == null){
                return true;
            }
        }
    }

maxTileExists(Board b)

  • 功能:如果棋盘上有2048数值 则返回true
  • 用到Tile类中的t.value()方法 返回格子中的值 要用MAX_PIECE代替2048
  • 思路:遍历整个棋盘 有t.values() == MAX_PIECE则返回true
public static boolean maxTileExists(Board b) {
    // TODO: Fill in this function.
    int size = b.size();
    for(int col = 0; col < size; col++){
        for(int row = 0; row < size; row++){
            Tile t = b.tile(col, row);
            //只有当t不为空时才检查t.value()
            if(t != null && t.value() == MAX_PIECE) {
                return true;
            }
        }
    }
    return false;
}

atLeastOneMoveExists(Board b)

  • 功能:如果棋盘可以进行有效移动 则返回true
    • 棋盘上至少有一个空位
    • 至少有一对相邻格子具有同样的值
  • 用到int[] x = {0, -1, 0, 1} int[] y = {-1, 0, 1, 0};来移动坐标找到相邻格子 判断格子是否在棋盘内 再判断两个格子的值是否相等
  • 思路:先判断有无空格 再遍历整个棋盘找相邻同值
public static boolean atLeastOneMoveExists(Board b) {
    // TODO: Fill in this function.
    //棋盘上有空位的情况
    if(emptySpaceExists(b)){
        return true;
    }
    //有相邻格子值相同的情况
    int size = b.size();
    int[] x = {0, -1, 0, 1};//左 下 右 上移动坐标
    int[] y = {-1, 0, 1, 0};
    for(int i = 0; i < size; i++){
        for(int j = 0; j < size; j++){
            int value = b.tile(i, j).value();
            for(int n = 0; n < 4; n++){
                int i2 = i + x[n];
                int j2 = j + y[n];
                if(i2 < size && j2 < size && i2 >= 0 && j2 >= 0){
                    int value2 = b.tile(i2, j2).value();
                    if(value == value2){
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

tilt(Side side)

  • 构建2048的核心逻辑
    1. 两个具有相同值的图块合并为一个图块,其中包含双倍的初始数字。
    2. 合并结果的图块将不会在该方向上再次合并。例如[X, 2, 2, 4]向左移动应该得到 [4, 4, X, X] 而不是 [8, X, X, X] 这是因为最左边的4已经是合并的一部分,因此不应再次合并。
    3. 当运动方向上的三个相邻块具有相同的编号时,则运动方向上前两个块合并,而尾随块不合并。 例如[X, 2, 2, 2] 并向左移动图块应该得到 [4, 2, X, X] 而不是 [2, 4, X, X]
  • 用到board.move(int col, int row, tile t); 移动格子t到c列r行
    board.setViewingPerspective(side); 视角转换到side方向(旋转棋盘使side方向为北边)
    board.setViewingPerspective(side);
    // put your code here
    board.setViewingPerspective(Side.NORTH);
    
  • 思路:按列遍历棋盘 每列从上往下检查
    1. 先把非空格子移到前面 空格子移到后面 [x, 2, 2, x] -> [2, 2, x, x]
    2. 再判断能否和相邻格子合并 [2, 2, 2, x] -> [4, x, 2, x] -> [4, 2, x, x]
public boolean tilt(Side side) {
    boolean changed;
    changed = false;

    // TODO: Modify this.board (and perhaps this.score) to account
    // for the tilt to the Side SIDE. If the board changed, set the
    // changed local variable to true.

    board.setViewingPerspective(side);
    int size = board.size();
    //把非空格子移到前面
    for (int col = 0; col < size; col++){
        for (int row = size - 1; row >= 0; row--){
            Tile t = board.tile(col, row);
            if(t != null){
                int next = size - 1;
                while(row < next){
                    if(board.tile(col, next) == null){
                        board.move(col, next, t);
                        changed = true;
                        break;
                    }
                    next--;
                }
            }
        }
    }
    //把格子合并
    for (int col = 0; col < size; col++){
        size = board.size();
        for (int row = size - 2; row >= 0; row--){//从第二行开始 向上检查有没有相同项
            Tile t = board.tile(col, row);
            if(t != null){//本行不为空 则上一行不为空
                // 检查上一行是否相同
                int next = row + 1;
                while(next < size){//不能超出棋盘
                    Tile nextTile = board.tile(col, next);
                    if((nextTile != null)&&(t.value() == nextTile.value())){//上一行与本行相同
                        board.move(col, next, t);
                        score += t.value() * 2;
                        changed = true;
                        size--;//后续棋子移动范围减1
                    } else if (nextTile == null && board.tile(col, next + 1 ).value() == t.value() && next + 1 < size) {
                        //上一行为空但上上行与本行相同且不超出移动范围
                        board.move(col, next + 1, t);
                        score += t.value() * 2;
                        changed = true;
                        size--;//后续棋子移动范围减1
                    } else if (nextTile == null){//上一行为空且上上行不可移动
                        board.move(col, next, t);
                        changed = true;
                    }
                    next++;
                }
            }
        }
    }
    board.setViewingPerspective(Side.NORTH);

    checkGameOver();
    if (changed) {
        setChanged();
    }
    return changed;
}