红魔咖啡馆

头发越掉越多,头发越掉越少

0%

Project1A-Linked List Deque 61B

这个项目要求我们用链表实现双端队列(Deque)

Javadoc comment

1
2
3
4
5
/** ...
 * @param index index to get
 * @return element at {@code index} in the deque
 */
T get(int index);

如以上注释,该形式的注释为java文档注释

  • @param表示方法的变量
  • @return表示方法的返回值类型
  • @code 用于格式化文本为代码风格

步骤

创建文件

我们希望创建的类可以接受多种数据类型,故需要实现泛型,在声明类时:

public class LinkedListDeque61B<T>

另外需要指定LinkedListDeque61B是Deque61B的子类,并且实现所有接口中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.List;

public class LinkedListDeque61B<T> implements Deque61B<T>{
    @Override
    public void addFirst(T x) {
        
    }

    @Override
    public void addLast(T x) {

    }

    @Override
    public List<T> toList() {
        return List.of();
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public T removeFirst() {
        return null;
    }

    @Override
    public T removeLast() {
        return null;
    }

    @Override
    public T get(int index) {
        return null;
    }

    @Override
    public T getRecursive(int index) {
        return null;
    }
}

最后加入构造函数:

1
2
3
public LinkedListDeque61B(){

}

JUnit Tests

打开LinkedListDeque61BTest文件,去除除最后一行的注释,就可以对编写代码进行本地测试

Writing and Verifying the Constructor

实现构造函数,更具体的,构造函数应传入0个参数,并在LinkedListDeque61B类中实现嵌套类Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private class Node{
    public T item;
    public Node next;
    public Node previous;
    public Node(T i, Node p, Node n){
        item = i;
        next = n;
        previous = p;
    }
}

private Node sentinelFront;
private Node sentinelLast;
private int size;
public LinkedListDeque61B(){
    sentinelFront = new Node(null, null, null);
    sentinelLast = new Node(null, null, null);
    size = 0;
}

这里我们在类中嵌套实现了Node类用于定义每个节点,并且对双端队列设置了两个sentinel节点,方便双端操作,同时引入size记录大小

其中,Node类包括两个指针,同时指向前一个节点与后一个节点

构造函数中,对两个sentinel节点进行初始化,并设置size为0

Writing and Verifying addFirst and addLast

addFirst与addLast需要以常数时间实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void addFirst(T x) {
    Node newNode = new Node(x, sentinelFront, sentinelFront.next);
    sentinelFront.next.previous = newNode;
    sentinelFront.next = newNode;
    size++;
}

@Override
public void addLast(T x) {
   Node newNode = new Node(x, sentinelLast.previous, sentinelLast);
   sentinelLast.previous.next = newNode;
   sentinelLast.previous =  newNode;
    size++;
}

我们直接操作sentinel节点的next与previous来添加节点

Writing and Verifying toList

实现toList,当调用时会自动返回以列表形式表示的Deque61B

例如,如果Deque61B已经调用了addLast(5)、addLast(9)、addLast(10),然后对其调用addFirst(3),那么toList()的结果应该是一个以3开头,然后是5、9、10的列表。如果在Java中打印,它将显示为[3, 5, 9, 10]。

toList方法的第一行应该类似于List<T> returnList = new ArrayList<>(),标志着可以使用该结构的位置

1
2
3
4
5
6
7
8
@Override
public List<T> toList() {
    List<T> returnList = new ArrayList<>();
    for (Node p = sentinelFront.next; p!=sentinelLast; p = p.next){
        returnList.add(p.item);
    }
    return returnList;
}

Writing Tests

写测试例的模板:Arrange-Act-Assert

  • Arrange:组织测试样例,如初始化数据结构或往里面填充元素
  • Act:执行想要执行的操作
  • Assert:评估Act中的结果

在单个测试方法中通常有多个act与assert步骤

真值评估的模板:

格式:assertThat(actual).isEqualTo(expected);

添加信息:

1
2
3
assertWithMessage("actual is not expected")
    .that(actual)
    .isEqualTo(expected);

判断列表包含内容:

1
2
3
assertThat(actualList)
    .containsExactly(0, 1, 2, 3)
    .inOrder();

对于引用类型:

1
2
3
assertThat(actualList)
    .containsExactlyElementsIn(expected)  // `expected` is a List
    .inOrder();

注意:assert一个布尔值时,记得在后面使用.isTrue() 或者 .isFalse()判断正误

其余方法

isEmpty()与size()

要求常数时间实现

1
2
3
4
5
6
7
8
9
@Override
public boolean isEmpty() {
    return size==0;
}

@Override
public int size() {
    return size;
}

get()与getRecursive()

给予下标寻找下标对应元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public T get(int index) {
    if (index<0||index>=size){
        return null;
    }
    Node p = sentinelFront.next;
    for (int i = 0; i<index; i++){
        p = p.next;
    }
    return p.item;
}

private T getRecursiveHelper(Node p, int index){
    if (index == 0) return p.item;
    return getRecursiveHelper(p.next, index-1);
}
@Override
public T getRecursive(int index) {
    if (index<0||index>=size){
        return null;
    }
    return getRecursiveHelper(sentinelFront.next, index);
}

写了一个辅助递归函数,可以接受当前节点与下标,进行节点的推进

removeFirst()与removeLast()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public T removeFirst() {
    if (isEmpty()){
        return null;
    }
   Node temp = sentinelFront.next;
    sentinelFront.next = temp.next;
    temp.next.previous = sentinelFront;
    size--;
    return temp.item;
}

@Override
public T removeLast() {
    if (isEmpty()){
        return null;
    }
    Node temp = sentinelLast.previous;
    sentinelLast.previous = temp.previous;
    temp.previous.next = sentinelLast;
    size--;
    return temp.item;
}

Project1B - ArrayDeque 61B

这个项目中,我们需要用数组实现循环双端队列

初始化

创建类文件,并修改类声明以让该类是Deque61B接口的一个实现,且支持泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package deque;

import java.util.List;

public class ArrayDeque61B<T> implements Deque61B<T> {
    @Override
    public void addFirst(T x) {
        
    }

    @Override
    public void addLast(T x) {

    }

    @Override
    public List<T> toList() {
        return List.of();
    }

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public T removeFirst() {
        return null;
    }

    @Override
    public T removeLast() {
        return null;
    }

    @Override
    public T get(int index) {
        return null;
    }

    @Override
    public T getRecursive(int index) {
        return null;
    }
}

构造函数

使用到的成员有

  • size:确定数组目前容量
  • capacity:确定数组最大容量
  • items:使用java内置数组存储数据
  • nextFirst:指向队首(第一个空位置)
  • nextLast:指向队尾(最后一个空位置)
1
2
3
4
5
6
7
public ArrayDeque61B(){
    size = 0;
    capacity = 8;
    items = (T[])new Object[capacity];
    nextFirst = 0;
    nextLast = 0;
}

addFirst() & addLast()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void addFirst(T x) {
    if (size==capacity){
        resize(size*2);
    }
    items[nextFirst] = x;
    size++;
    nextFirst = floorMod(nextFirst-1, capacity);
}

@Override
public void addLast(T x) {
    if (size==capacity){
        resize(size*2);
    }
    items[nextLast] = x;
    size++;
    nextLast = floorMod(nextLast+1, capacity);
}

这里在添加时要考虑循环,若到队头了要转到队尾加

另外,当队列满了需要手动扩容,一般可以扩到原来都两倍,防止过多调用

get()

按下标获得元素,若无效返回null

1
2
3
4
5
6
7
8
@Override
public T get(int index) {
    if (index<0||index>=size){
        return null;
    }
    int realIndex = floorMod(index+nextFirst+1, capacity);
    return items[realIndex];
}

这里注意,get获得的是相对下标,我们需要转换成真实下标(即元素不一定是从0开始的,需要添加nextFirst偏移量)

getRecursive()没有什么必要,可以直接抛出一个异常

1
2
3
4
@Override
public T getRecursive(int index) {
    throw new UnsupportedOperationException("No need to implement getRecursive for proj 1b");
}

toList()

1
2
3
4
5
6
7
8
9
@Override
public List<T> toList() {
    List<T> show = new ArrayList<>(size);
    int cnt = 0;
    for (int i = 0; i<size;i++){
        show.add(get(i));
    }
    return show;
}

结合get方法会更加方便

removeFirst() & removeLast()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public T removeFirst() {
    if (isEmpty()){
        return null;
    }
    nextFirst = floorMod(nextFirst+1,capacity);
    size--;
    return items[nextFirst];
}

@Override
public T removeLast() {
    if (isEmpty()){
        return null;
    }
    nextLast = floorMod(nextLast-1,capacity);
    size--;
    return items[nextLast];
}

不要忘记判空以及减小size

resize()

作为数组大小不够时的方案,一次扩容两倍使用

1
2
3
4
5
6
7
8
9
10
11
12
13
private void resize(int newCapacity){
    T[] a = (T[]) new Object[newCapacity];
    int cur = floorMod(nextFirst+1,capacity);
    for(int i = 0; i<size;i++){
        a[i] = items[cur];
        cur = floorMod(cur+1, capacity);
    }
    items = a;
    capacity = newCapacity;
    nextFirst = newCapacity -1;
    nextLast = size;

}

这里要将原数组复制到一个新容量的数组中使用

采用的方法是将原数组元素按从0开始的下标复制到新数组,再用新数组覆盖

同时调整nextFirst与nextLast

iterator()

实现迭代器,让我们的双端队列可以迭代

即在内部嵌套一个Iterator类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public Iterator<T> iterator() {
    return new ArrayDequeIterator();
}

public class ArrayDequeIterator implements Iterator<T>{
    private int pos;
    ArrayDequeIterator(){
        pos = 0;
    }
    @Override
    public boolean hasNext() {
        return pos < size;
    }

    @Override
    public T next() {
        T returnItems = get(pos);
        pos++;
        return returnItems;
    }
}

注意:接口不允许提供覆盖Object方法的默认方法,故无法在接口中提供默认方法

equals()

重写equals方法,让该方法仅比较元素相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public boolean equals(Object o){
    if (o instanceof ArrayDeque61B<?> uAD){
        if (this.size!=uAD.size){
            return false;
        }
        for (int i = 0; i<size;i++){
            if (!Objects.equals(this.get(i), uAD.get(i))){
                return false;
            }
        }
        return true;
    }
    return false;
}

若想在instanceof中使用泛型,需要使用<?>通配符

toString()

返回自定义的string输出表示双端队列

1
2
3
4
5
6
7
8
9
10
11
@Override
public String toString(){
    StringBuilder stringToReturn = new StringBuilder("[");
    for (T x: this){
        stringToReturn.append(x);
        stringToReturn.append(", ");
    }
    stringToReturn.delete(stringToReturn.length()-2,stringToReturn.length());
    stringToReturn.append("]");
    return stringToReturn.toString();
}

Project0 - 2048

Hard Mode

项目结构

1
2
3
4
5
6
7
8
9
10
proj0_hardmode
├── game2048logic
|   ├── GameLogic.java
|   ├── MatrixUtils.java
├── game2048rendering
    ├── Board.java
    ... (some other files) ...
    ├── Main.java
    ├── Side.java
    ├── Tile.java

分为了两个包:负责逻辑与负责渲染的,主要更改在负责逻辑的包中

Task1:游戏原理

2048的核心规则为Tilting,项目需要主要实现该功能

当两个方格发生融合时,遵循以下规则

  1. 两相同值的方格融合为一个,数值翻倍

  2. 由合并产生的新方块在该次移动中不会再次合并。例如,当[X, 2, 2, 4](X 代表空格)向左移动时,结果应为[4, 4, X, X]而非[8, X, X, X]。这是因为最左侧的 4 已参与过合并,故不应二次合并

  3. 当融合方向上有三个相邻同值方块时,仅该方向最前方的两个方块会合并,第三个方块保不变。例如[X, 2, 2, 2]左移后应为[4, 2, X, X],而非[2, 4, X, X]。

  4. 推论:若有四个相邻同值方块,则会合并出两个同值方块,如[2, 2, 2, 2]左移后应该为[4, 4, X, X],但合并出的两个同值方块不会合并

Task2:实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void compress(int[][] board, int c){
    int n = board.length;
    int[] comp = new int[n];
    int index = 0;
    for (int[] ints : board) {
        if (ints[c] != 0) {
            comp[index++] = ints[c];
        }
    }
    for (int i = 0; i<n; i++){
        board[i][c] = comp[i];
    }
}
public static void merge(int[][] board, int c){
    int n = board.length;
    for (int i = 0; i<n-1; i++){
        if(board[i][c]!=0 && board[i][c]==board[i+1][c]){
            board[i][c] *= 2;
            board[i+1][c] = 0;
            i++;

        }
    }

}
public static void moveColumn(int[][] board, int c){
   compress(board, c);
   merge(board, c);
   compress(board, c);


}
public static void moveUp(int[][] board){
    for (int i = 0; i<board.length; i++){
        moveColumn(board, i);
    }
}

compress函数用于压缩,即将所有数都移动到顶上,而不进行合并

merge函数用于将相邻的数进行合并

每次移动一列,然后循环处理每列,处理一列时移动后合并,合并后可能会出现中间合并剩下的空位置,需要再进行压缩

Lec16 - Extends, Sets, Maps, and BSTs

Extends

从前,若一个类是接口的下位词,我们使用implements来书写方法的实现

但若想实现一个类是另一个类的下位词,我们需要使用extends

extends可以用于类与类、接口与接口之间

例:实现RotatingSLList

即让最后一个元素右旋到第一个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package Lec16;

public class RotatingSLList<Item> extends SLList<Item> {
    public void rotateRight(){
        Item x = removeLast();
        addFirst(x);
    }
    public static void main(String[] args){
        RotatingSLList<Integer> rsl = new RotatingSLList<>();
        rsl.addLast(10);
        rsl.addLast(11);
        rsl.addLast(12);
        rsl.addLast(13);

        rsl.rotateRight();
        rsl.print();
    }
}

extends继承了SLList中的所有成员:

  • 所有实例与静态变量
  • 所有方法(除了构造函数)
  • 所有嵌套类

但若成员是私有的就无法访问了

例:实现VengefulSLList

记录被删除的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package Lec16;


import java.util.ArrayList;
import java.util.List;

public class VengefulSLList<Item> extends SLList<Item>{
    private SLList<Item> deletedItems ;
    public VengefulSLList(){
        deletedItems = new SLList<Item>();
    }

    @Override
    public Item removeLast(){
        Item removedItem = super.removeLast(); // 使用父类的方法
        deletedItems.addLast(removedItem);
        return removedItem;
    }
    public void printLostItems(){
        deletedItems.print();
    }
    public static void main(String[] args){
        VengefulSLList<Integer> vsl = new VengefulSLList<>();
        vsl.addLast(1);
        vsl.addLast(5);
        vsl.addLast(10);
        vsl.addLast(15);

        vsl.removeLast();
        vsl.removeLast();

        vsl.printLostItems();
    }
}

这里我们实现了对继承方法的重写

其中,我们可以使用super来调用父类中的方法

BST

随机表

链表的搜索效率较慢,我们可以在不同节点间随机额外添加一些链接,这样可以依赖随机性获得更好的效率

二叉搜索树的原理

若我们将每次搜索都折半,就能大幅提高效率,一个链表可以进行如下改造:

BST

这是一棵二叉搜索树

二叉搜索树是一个有根的二叉树,对于书中的每个节点

  • 左子树中的每个节点的值都小于该节点的值
  • 右子树中的每个节点的值都大于该节点的值
BST

寻找特定元素

  • 若待寻找元素等于当前元素,返回
  • 若待寻找元素小于当前元素,前往左子树寻找
  • 若待寻找元素大于当前元素,前往右子树寻找

时间复杂度:\(\Theta(\log n)\)

因此搜索一颗完全BST的速度是很快的

1
2
3
4
5
6
7
8
9
10
static BST find(BST T, key sk){
    if (T==null)
        return null;
    if (sk.equals(T.key))
        return T;
    else if (sk<T.key)
        return find(T.left, sk);
    else
        return find(T.right, sk);
}

添加元素

首先寻找待插入元素

  • 若找到了就不进行操作
  • 若没找到
    • 创建一个新节点
    • 创建合适链接

使用递归书写

1
2
3
4
5
6
7
8
9
static BST insert(BST T, key ik){
    if (T==null) 
        return new BST(ik);
    if (ik<T.key)
        T.left = insert(T.left, ik);
    else if (ik>T.key)
        T.right = insert(T.right, ik);
    return T;
}

删除元素

删除没有孩子的节点:

直接删除,即将其父节点对应的左/右节点置空

删除一个孩子的节点:

要在维持BST性质的前提下删除:可以发现该节点的孩子一定大于(右侧)或小于(左侧)该节点

故我们可以寻找该节点的孩子,将指向它的节点指向他的孩子即可

删除两个孩子的节点:

找到左子树最大的节点或右子树最小的节点,替代待删除节点即可

Lec15 - Disjoint Sets

解决连通性问题

较好的方法是只需要记录每个相连元素属于哪个集合,至于如何连接不必关心

数据结构的选择

sets

过于复杂并且耗时高

arrays

另一个想法是:起始将每个元素设置为不同id,只需要记录每个元素所在集合的id,连通只需要将该元素所在集合id改变为目标id即可;检查是否连通只需要检查id是否一致

时间复杂度:

连通方法复杂度还是高了一些

trees

想法是:给每一个元素分配一个父节点来代替id

  • 对于连通操作:可以找到要连通的两个集合的根节点,将一个的根节点设置为另一个

  • 对于检查连通操作:从待检查节点向上寻找父节点,一直找到根节点看是否相同即可

操作本质即寻根并操作根节点

时间复杂度:

优化树结构

我们需要最小化树高

  • 将较短的树连接到较长的树下
  • 若高度相同,则需要打破相同

规定:总是将更小的树根连接到更大的树上

注意:使用权重衡量才能最小化平均步数,而使用高度衡量只能最小化最坏步数

时间复杂度:

再次进行优化:

查找后将所有节点都连接到根节点上,这样再次查找不必再遍历

这样,随着操作继续,我们的树会变得越来越矮,直到树高为1,这样所有操作都很快

其中\(\alpha(N)\)为逆阿克曼函数,该函数的增长速度特别缓慢,对于具有意义的值x,\(\alpha(x)\)的值始终不大于4,故可以看作常数时间

Lec13 - Asymptotics II

\(\Theta\)

对于一个表达运行时间的式子\(R(N)\in \Theta(f(N))\)

意味着存在两个整数\(k_1\)\(k_2\)使得\(k_1\cdot f(N)\leq R(N)\leq k_2 \cdot f(N)\)

对于所有\(N\geq N_0\)

常用该表示法描述函数的增长顺序与运行时长的增长率

大O

\(R(N)\in O(f(N))\)

意味着存在整数与\(k_2\)使得\(R(N)\leq k_2 \cdot f(N)\)

对于所有\(N\geq N_0\)

即大O表示仅被上界约束,描述小于等于\(f(N)\)

\(\Omega\)

\(R(N)\in \Omega(f(N))\)

意味着存在两个整数\(k_1\)\(k_2\)使得\(k_1\cdot f(N)\leq R(N)\)

对于所有\(N\geq N_0\)

常用该表示法描述函数的增长顺序与

即大\(\Omega\)表示仅被下界约束,描述大于等于\(f(N)\)

关系证明

证明若\(f(x)\in O(g(x))\)\(f(x)\in \Omega(g(x))\),则\(f(x)\in \Theta(g(x))\)

另一种表示形式

\(g(x)\)除到\(f(x)\)下面,有\(a\leq \frac{f(x)}{g(x)}\leq b\)

  • \(\frac{f(x)}{g(x)}\)有上界,则\(f(x)\in O(g(x))\)

  • \(\frac{f(x)}{g(x)}\)有下界,则\(f(x)\in \Omega(g(x))\)

  • \(\frac{f(x)}{g(x)}\)有界,则\(f(x)\in \Theta(g(x))\)

Lec11 - Asymptotics I

衡量计算花费的方法

使用客户端程序测量执行时间

优点:易测量,意义明显

缺点:可能需要大量计算时间,结果可能因机器、编译器、数据不同而不同

计算可能的操作数

对于输出的大小为N的数组,计算可能的操作数

优点:独立于机器,输入基于给到的模型,显示了算法如何进行拓展

缺点:很难计算

对给定N大小数组,选择一个特定输入来代表输入规模,计算该操作数

特定输入包括:

  • 最差情况
  • 最好情况

渐进表现

大多数情况下,对于很大的数据N,我们只关心其渐进表现

具有较好规模(如线性)的算法会比具有相对差规模(如抛物线型)有着更好的运行时间表现

简化等式

  1. 将表格用多个变量的等式表示

  2. 忽略低阶项

  1. 将所有系数转换为一个常数,即\(CN^2\),将拥有这些常数的函数归到一类,称为\(\Theta(N^2)\)

\(\Theta(N^2)\)是包含了最坏运行时间的函数集合,用于表达运行时间

Lec10 - Iterators, Object Methods

Iterators

使用迭代器可以让自己的数据类型可以被迭代(支持for-each loop)

  • 需要让对应类实现Iterable接口
  • 具有Iterator方法来返回迭代器对象(实现一个嵌套类)
  • 迭代器需要具有hasNext()next()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package Lec10;

import java.util.Iterator;

public class ArraySet<T> implements Iterable<T> {
    private T[] items;
    private int size;

    public ArraySet(){
        items = (T[]) new Object[100];
        size = 0;
    }
    private class ArraySetIterator implements Iterator<T>{

        private int pos;
        ArraySetIterator(){
            pos = 0;
        }
        @Override
        public boolean hasNext() {
            if (pos<size){
                return true;
            }
            return false;
        }

        @Override
        public T next() {
            T itemToReturn = items[pos];
            pos++;
            return itemToReturn;
        }
    }
    public Iterator<T> iterator(){
        return new ArraySetIterator();

    }
    
}

(注:仅有迭代器部分,没有其他具体实现)

Object Method

Java中所有类都是Object的子类

toString()

给予一个object的字符串表示

println方法也调用了toString()方法

该方法类似于python中的repr方法,可以给类一个自定义的字符串输出形式

1
2
3
4
5
6
7
8
9
10
@Override
public String toString(){
    StringBuilder stringToReturn = new StringBuilder("{");
    for(T x: this){
        stringToReturn.append(x);
        stringToReturn.append(", ");
    }
    stringToReturn.append("}");
    return stringToReturn.toString();
}

this

this是当前对象的一个引用,可以使用this来访问自己的实例变量和方法

但java中this是不必须的

若方法中的局部变量和实例变量重名,此时必须要用this加以区分

equals()

equals()方法通过判断两个对象的地址来判断是否相同

当然,可以通过重写方法来改变其他判断相同的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public boolean equals(Object o){
    if (o instanceof ArraySet uSet){
        if (this.size != uSet.size()){
            return false;
        }
        for (T x: this){
            if (!uSet.contains(x)){
                return false;
            }
        }
        return true;
    }
    return false;
}

其中,关键字A instanceof B的用处如下

  • 检查o是否指向类B,若不是返回false
  • 若是,返回true并将A作为B类并命名为B后面的名字
  • o是null也可以运行

Lec9-Subtype Polymorphism, Comparators, Comparables, Generic Functions

Subtype Polymorphism

Java中没有重载运算符,故我们需要使用Comparable接口来实现类似功能

Java源码中对该接口的描述如下:

我们需要实现该接口

e.g. 定义Dog类,通过size属性比较两只狗的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package Lec9;

public class Dog implements Comparable<Dog>{
    public String name;
    public int size;
    public Dog(String n, int s){
        name = n;
        size = s;
    }
    @Override
    public int compareTo(Dog o) {
        if (size < o.size){
            return -1;
        }
        else if (size > o.size){
            return 1;
        }
        return 0;
    }
}

另一种写法

java规定了若更小则返回负数

1
2
3
4
@Override
public int compareTo(Dog o) {
    return size - o.size;
}

这种操作被称为子类型多态

  • 超类指定了可以干什么(如Comparable接口指定了比较功能)
  • 子类通过重写超类的抽象方法,来使java根据调用方法的对象来决定运行时该做什么

Comparator

Java提供了接口Comparator用来比较其他对象

语法:

1
2
3
4
public interface Comparator<T>{
    int compare(T o1, T o2);
    ...
}

Dog类中

1
2
3
4
5
6
7
public static class NameComparator implements Comparator<Dog> {

    @Override
    public int compare(Dog a, Dog b) {
        return a.name.compareTo(b.name);
    }
}

其中Dog.NameComparator()的数据类型为Comparator<Dog>

更现代的方式是使用lambda表达式定义comparator

1
2
Comparator<Dog> dc =(d1, d2) -> d1.name.compareTo(d2.name);
Dog maxNameDog = Collection.max(dogs,dc);

泛型函数

如何编写自己的方法

e.g. 实现一个方法,从数组中随机选择一个元素,并且接受各种类型

在这里,选择随机元素的方法是静态的,正确方式是让方法本身变为泛型,而非让类变为泛型

即声明了一个公共静态函数,用于处理类型T的对象

因为如果让整个类泛型化,你需要实例化那个类来获得泛型行为,但把该方法作为一个静态方法来看,并不需要实例化

1
2
3
4
5
6
7
publicc class RandomPicker{
    public static <T> T pickRandom(T[] x){
        Random random = new Random();
        int randomIndex = random.nextInt(x.length);
        return x[randomIndex];
    }
}