学习笔记
- Introduction
- Defining and Using Classes
- Primitive Types, Reference Types, and Linked Data Structures
- SLLists, Nested Classes, Sentinel Nodes
- DLLists and Arrays
- Testing
- Array and Lists
- Interface and lmplementation lnheritance
- Extends Casting Higher Order Functions
- Subtype Polymorphism Comparators Comparable
- lterators, Object,Methods
- Asymptotic
- Disjoint Sets
伯克利大学《算法与数据结构》,做个笔记喵。
参考资料: CS61B精翻
Introduction
在编程中,不仅仅要高效地写出代码,还要能快速地编写代码。
这是一门用Java教授的编程课程,Java很在乎你所写的代码都在一个类中,
所以我们用来定义一个类的魔法词是public class,
我们先来写一个简单的HelloWorld:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
我们再来写一个Larger:
public class Larger {
public static int larger(int x, int y){
if(x>y){
return x;
}else{
return y;
}
}
public static void main(String[] args) {
System.out.println(larger(1, 2));
}
}
我们将主方法定义为public static void main(String[] args)
,我们看到的所有代码都必须是类的一部分,在Java中,我们不是用空格缩进,我们使用大括号{ }来表示事物的起始和结束,用分号结束了每一行,这些都是我们在练习使用Java的过程中会逐渐掌握的。
- 在Java中,使用变量之前,必须声明它的存在,并且说明它的类型,一旦你声明了它,它就永远不能改变
- 程序会在运行之前检查所有类型是否正确,只有在程序经过检查且所有类型匹配的情况下,程序才能真正运行
什么是对象呢?对象将信息和相关行为捆绑在一起,这个想法是,这个对象可以追踪一些信息,你也可以通过调用它的方法与对象进行交互。
那我们如何构造对象呢?我们写一个类,这就像我们创建全新对象的蓝图一样。
现在我们写一些面向对象的代码
public class Car {
String model;
int wheels;
public Car(String m){
this.model = m;
this.wheels = 4;
}
public void drive(){
if(this.wheels<4){
System.out.println(this.model + "no go vroom");
}else{
System.out.println(this.model + "go vroom");
}
}
public int number(){
return this.wheels;
}
public void driveintoditch(int wheelslost){
this.wheels = this.wheels -wheelslost;
}
public static void main(String[] args) {
Car c1;
Car c2;
c1 = new Car("Mercedes-Benz");
c2 = new Car("BMW");
c1.drive();
c1.driveintoditch(2);
c1.drive();
System.out.println(c2.number());
}
}
如果你正在创建一个新的对象,Java需要知道你正在创建一个新的对象,所以你给它这个小小的new的关键字
面向对象编程:模块性:我可以构建程序的一部分而不担心其他部分,然后我可以构建程序的另一部分,而不必担心其余部分,最后,他们都将以这种美妙的方式拼合在一起,这将使我们能够构建非常庞大,复杂的东西。
不变量: 代码运行时始终为真的东西
Defining and Using Classes
这节课讲了类的定义与使用
通过一个狗定义的方法来介绍
public class Dog {
int weightInPounds;
public Dog(int w){
weightInPounds = w;
}
public void makeNoise(){
if(weightInPounds < 10){
System.out.println("yipyipyip");
}else if (weightInPounds < 30) {
System.out.println("bark");
}else{
System.out.println("arooooooo");
}
}
}
我们在Dog类中创建了一个实例变量(可以说,我们创建的每只狗都有自己的weightInPounds),接着,我们写了一个构造函数(就像创建新狗的蓝图一样)
public class DogLauncher {
public static void main(String[] args) {
Dog smallDog;
new Dog(20);
smallDog = new Dog(5);
Dog hugeDog = new Dog(150);
smallDog.makeNoise();
hugeDog.makeNoise();
}
}
首先,我们必须声明有一个类型为小狗的变量,它的类型是狗;当我们说新狗时,是指从头创建了一个全新的对象,它从未分配给一个变量,但第二行的问题在于,实际上没有将狗保存在任何地方,它从未分配给一个变量,因此,由于这只狗已经被创建但没有引用它,它将被垃圾收集器销毁。相比之下,看看下一行,创建了一个尺寸为5的全新狗,但也将它分配给了smallDog变量。
Dog hugeDog = new Dog(150);
看到这一行,这是在声明变量,创建新对象,然后再将新对象赋给刚刚声明的变量。
静态与非静态:
- 当你的方法是静态的时候 这个方法是通用的 适用于所有狗
- 相比之下 如果我有一个非静态方法 那么非静态方法就是针对一个狗的
- 在同一个类中可以同时拥有静态方法和非静态方法
下面我们来看一些对比
静态:
public static void makeNoise(){
System.out.println("Bark");
}
非静态:
public void makeNoise(){
if(weightInPounds < 10){
System.out.println("yipyipyip");
}else if (weightInPounds < 30) {
System.out.println("bark");
}else{
System.out.println("arooooooo");
}
}
比较狗的大小也有静态或者非静态:
public static Dog maxDog(Dog d1,Dog d2){
if(d1.weightInPounds > d2.weightInPounds){
return d1;
}
return d2;
}
public Dog maxDog(Dog d2){
if(weightInPounds > d2.weightInPounds){
return this;
}
return d2;
}
这两种方法都是正确的,使用哪个取决于你喜欢有一个公正的观察者 还是喜欢特定的狗做把戏并将自己与其他狗比较。在这里我们看到有实例变量 但你也可以有静态变量。
本节课还介绍了一个有用的工具:调试器,我们可以通过设置断点来对程序进行调试。
- 单步调试就是执行现在的语句 如果这个语句调用了函数 就会进入函数里面
- 逐过程就是不会进入函数里面
- 单步跳出就是执行到函数返回 或者 到断点
- 当你想要的东西不在代码里这里也有一个小框 你可以输入代码行 它会告诉你这些代码行现在的评估结果
Primitive Types, Reference Types, and Linked Data Structures
这节课从海象的谜题来引入,请看如下代码:
public class Walrus {
public int weight;
public double tuskSize;
public Walrus(int w, double ts){
weight =w;
tuskSize =ts;
}
public String toString(){
return String.format("weight:%d, tusk size:%f",this.weight,this.tuskSize);
}
}
public class PollQuestions {
public static void main(String[] args){
Walrus a=new Walrus(1000,8.3);
Walrus b;
b = a;
b.weight =5;
System.out.println(a);
System.out.println(b);
int x= 5;
int y;
y = x;
x= 2;
System.out.println("x is:"+ x);
System.out.println("y is:"+ y);
}
}
在这段代码中,b的改变会影响到a,但y的改变不会影响到x,这是为什么呢?
在Java中,每个变量都有关联的类型,八种基本类型是:bvte、short、int、long、float、double、boolean、char。
首先,先了解声明变量时会发生什么?
就拿int x;
来说,可以理解为,当你声明一个变量时 我们就制造这个盒子,也就是内存存放的地方,然后在这个盒子里 我们会保存
与该变量相对应的一串一串的1和0,而Java会保留某种查找表,表明如果你引用了x 就去这里你在肉存中留出一些空间 然后说 这就是x的位置 ;y = x;
等号所做的一切就是 它获取x中的所有位 对应于你命名的内存区域的1和0,然后我们只是将所有这些东西复制到y中 。
任何不是这八种类型之一的东西都被称为 引用类型,就如我们创建的Walrus。
当我想要创建一个新的海象时Java编译器或解释器需要在内存中查找并说 你想要一个全新的海象,我们要用到关键字new,当我使用构造函数创建了海象 Java进入内存然后创建了这界漂亮的海象。
这个新的海象构造函数做的第一件事,它分配内存并说 这是海象的家,然后,你可以想象new返回了一个数字,这个数字就是我美妙的海象所在的编号位,也就是地址。那么,接下来我们需要一个地方来存放这个地址。
Walrus somewalrus;
somewalrus = new Walrus(1000,8.3);
这个空间实际上存放的是海象的地址,而不是海象,就像C语言中的指针。
同样在1我们研究数组时,也要注意,数组也是引用类型。
int[] a = new int[]{0,1,2};
/*数组是不会适合在a中的!这只会适合一个引用 指向我数组在肉存中真正存在的地方的地址,
所以我声明有个a当我使用 new 关键字时 它实际止创建了数组并指向位置*/
接下来是课上的另一个内容:列表。
public class IntList {
int first;
public IntList rest;
public IntList(int f,IntList r){
first = f;
rest = r;
}
/* Return the size of this IntList. */
public int size(){
if(rest == null){
return 1;
}
return 1 + this.rest.size();
}
/* Returns the i'th item in the list.*/
public int get(int i){
if(i == 0){
return first;
}
return rest.get(i-1);
}
/* Returns the size of the list using no recursion! */
public int iterativesize(){
int totalSize = 0;
IntList p= this;
while(p!= null){
totalSize += 1;
p = p.rest;
}
return totalSize;
}
public static void main(String[] args){
IntList L =new IntList(15,null);
L = new IntList(10,L);
L = new IntList(5,L);
System.out.println(L.get(0));
System.out.println(L.size());
}
}
SLLists, Nested Classes, Sentinel Nodes
这节课的目标是:我们要向这个数据结构中添加衣服,这样用户就不必费太多心思去考虑裸露的、递归的数据结构以及它是如何工作的。
使用IntList的人 他们必须对递归有相当深的理解,因为递归就在那里,他们必须指定剩余列表的空值。相比之下,如果你使用的是SLList 你不必指定空值,因为我们已经将其隐藏在构造函数内部,如下:
public class IntNode {
int item;
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);
}
/* Adds item xto the front of the list. */
public void addFirst(int x){
first =new IntNode(x,first);
}
/* Gets the first item in the list.*/
public int getFirst(){
return first.item;
}
public static void main(String[] args) {
SLList L = new SLList(5);
L.addFirst(10);
System.out.println(L.getFirst());
}
}
这节课讲了61B的一个重要主题: 其核心思想是 当我使用一个没有S 的对象时 我不一定要理解整个对象的工作原理!在计算机科学中最强大的理念之一是:它让你能够从使用你的类或对象的人那里隐藏实现细节 这样你就能构建非常庞大、复杂的程序。
1.与其让用户每次使用数据结构时都要处理递归 我们要隐藏递归在这个不带S的数据结构后面,关于这个底层结构的好处就是 你不需要自己处理递归链接。
2.在这段代码中, 以某种方式,通过狁许用户直接访问我的列表,我们给了他们一个机会来破坏我们创建的这个美丽的列表结构,如L.first.next.next =L.first.next;
,但是我不希望他们进入我的列表,于是如果我把public关键字替换为private,如private IntNode first;
,那么如果我有另一个类在使用 SLList,他们将不被允许访问first。(private称其私有并不意味着它像一个安全措施,而是提醒人们如果不想做坏事和搞砸它,你可能不应该碰它)
3.我们可以把IntNode想象成SLList类的一个特性,它并不是一个独立的类,我可以只需复制或剪切它 然后将其粘贴到 S-list 类中创建一个嵌套类,这样可以使得代码更有条理。
问题回到给我们的数据结构“加衣服”
public class SLList{
private IntNode first;
public int size(){
return size(first);
}//这个外部方法对于用户来说 它不理解递归
//但这个私有助手方法 我将通过给它一个参数 p 来使它理解递归
private int size(IntNode p){
if(p.next == null){
return 1;
}
return 1 + size(p.next);
}
}
这里的想法是为用户提供的公共方法,用户不知道什么是IntNode
如果是这样的话,有点小问题,我们写的方法有点慢,我必须检查每个IntNode的大小 扫描整个列表并计算它们的数量。
为了提升速度,我们可以添加一个特殊的大小变量,我们在列表增长和缩小时一直追踪他。
private int size;
public SLList(int x){
first = new IntNode(x,r: null);
size = 1;
}
public void addFirst(int x){
first =new IntNode(x,first);
size += 1;
}
public void addLast(int x){
IntNode p= first;
/* Scan p until it reaches the end of the list. */
while(p.next != null){
p= p.next;
}
p.next = new IntNode(x,r:null);
size += 1;
}
public int size(){
return size;
}
有一个小问题 那就是有点取舍 为了获得快速的尺寸 我不得不使用一点额外的内存,
与上节课的方法相比之下,在这种 SLList 方法中 面向对象的方法中,有一个非常自然的地方可以放置大小。
那如果我想要一个空列表呢?
public SLList(){
first = null;
size = 0;
}
但这有一个非常微妙的错误。如果列表是空的,那么在addLast终究会出现错误。
我们可以在addLast中加入if(first == null){first = new IntNode(x, null);return;}
但是,拥有特殊情况会变得丑陋,虽然有用,但不是很好。
对此,我们的解决方案是,每当我创建一个列表时,我实际上都会创建一个特殊的虚拟节点,它总是存在的。如果有第一个项目 它不会存放在哨兵那里因为哨兵指向我们忠实的伙伴’它的项自并不重要。改进后完整的代码如下:
public class SLList {
private IntNode sentinel;
int size;
public SLList(int x){
sentinel = new IntNode(0, null);
sentinel.next = new IntNode(x, null);
size = 1;
}
public SLList(){
sentinel = new IntNode(0, null);
size = 0;
}
/* Adds item xto the front of the list. */
public void addFirst(int x){
sentinel.next =new IntNode(x,sentinel.next);
size += 1;
}
/* Gets the first item in the list.*/
public int getFirst(){
return sentinel.next.item;
}
/* Add xto the end of the list. */
public void addLast(int x){
size += 1;
IntNode p= sentinel;
/*Scan p until it reaches the end of the list. */
while (p.next != null){
p = p.next;
}
p.next= new IntNode(x,null);
}
/* Returns the size of the list. */
public int size(){
return size;
}
/*Returns the size of the list,starting at IntNode p. */
/*private int size(IntNode p){
if(p.next == null){
return 1;
}
return 1+size(p.next);
}*/
public static void main(String[] args) {
SLList L = new SLList(5);
L.addFirst(10);
System.out.println(L.getFirst());
}
}
DLLists and Arrays
按照我们之前的代码,添加最后一项会有点慢,那么,我们要如何改进呢?
有人可能会认为,可以添加一个指向最后一个的指针,但这会给我们增加一个删除最后一个的问题,因为我得找到新的最后一个元素,显然这种做法是不行的。
所以关键的顿悟时刻是如果每个元素不仅指向后面的人,而且还指向前面的人,那么突然间我就不必从头开始扫描到最后了。如果我这样做,我得到的将是一个被称作双向链表的结构。
如果链表是空的,课上为我们提出了两种方案:
1.使用两个”哨兵“,在他们直接安排项目;
2.我们构建一个循环的链表,这样的话,如果是空链表,”哨兵"的下一个项目就是“哨兵”本身,”哨兵"的上一个项目是“哨兵”本身(你可以想象成世界是圆的)。这个方案很巧妙,让我们可以用一个“哨兵”解决问题。具体如下:
public class DLList {
public class Node {
int data;
Node next;
Node prev;
Node(int data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
private Node sentinel;
int size;
public DLList(){
sentinel = new Node(0); // 哨兵节点的数据可以是任意值
sentinel.next = sentinel; // 哨兵节点指向自己
sentinel.prev = sentinel;
size = 0; // 哨兵节点指向自己
}
public void addFirst(int data){
Node node = new Node(data);
Node head = sentinel.next;
sentinel.next = node;
node.prev = sentinel;
head.prev = node;
node.next = head;
size += 1;
}
public void addLast(int data){
Node node = new Node(data);
Node last = sentinel.prev;
sentinel.prev = node;
node.next = sentinel;
last.next = node;
node.prev = last;
size += 1;
}
public void delete(Node node) {
if (node == null || node == sentinel)
return;
node.prev.next = node.next; // 前驱节点的next指向后继节点
node.next.prev = node.prev; // 后继节点的prev指向前驱节点
size--;
}
// 遍历链表(从头到尾)
public void printForward() {
Node current = sentinel.next;
while (current != sentinel) {
System.out.print(current.data + " ");
current = current.next;
}
System.out.println();
}
// 遍历链表(从尾到头)
public void printBackward() {
Node current = sentinel.prev;
while (current != sentinel) {
System.out.print(current.data + " ");
current = current.prev;
}
System.out.println();
}
// 获取链表的大小
public int size() {
return size;
}
public static void main(String[] args) {
DLList dll = new DLList();
dll.addFirst(1);
dll.addFirst(5);
dll.printForward();
}
}
那如果我们要进去,而不是使用整数呢?
我们要使用一种叫做泛型的东西 也就是我们将推迟类型选择到以后。
我要添加一个占位符来表示,这可以是任何东西,如public class SLList <Apple>{ }
,当你后面在创建对象的时候,要说明你使用的类型,如public static void main(String[] args){SLList<String> L=new SList<String>();
,那么,如何确保 Sentinel 节点中的虚拟东西与提供的类型匹配呢?sentinel = new IntNode( null, null);
可以选择使用null。
接下来讲的是数组
- 当你声明有一个数组时,Java就会为一堆变量分配一大块内存,依次排列在一起;
- Java中的主要问题是数组的长度是因定的,一旦我说 这个数组的长度是5,Java已经费了很大的劲挖了一个装下5个整数的地方;
- 还要注意的一件事是,数组暗地里有一个实例变量
x=new int[]{1,2,3,4,5};
int xL = x.length;
y = x;
如果我说y等于x,我拿这个箭头,我把同样的箭头复制到y,现在x和y都有了相同的节点 它是去这个地方访问数组
x = new int[3];
y = new int[]{1,2,3,4,5};
int[] z={9,10,11,12,13};
课上还讲到arraycopy
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
src:源数组
srcPos:源数组中复制的起始位置
dest:目标数组
destPos:目标数组中复制的起始位置
length:要复制的元素个数
int[] srcArray = {1, 2, 3, 4, 5};
int[] destArray = new int[5];
System.arraycopy(srcArray, 0, destArray, 0, srcArray.length);
//最终得到的destArray数组中的元素为{1, 2, 3, 4, 5}。
二位数组这里就不介绍了,感兴趣的可以自己了解一下。
Testing
今天的想法是给你力量 让你能够自己判断你的代码是否有效。即使我们不能完全确走我们的代码是100%正确的,但我们可以编写一些测试来为我们提供非常强有力的证据表明是正确的。
幸运的是,我们可以使用其他人编写的库,其他人写了单元测试库,他们做了所有繁重的工作 包括制定复杂的错误消息、检查和遍历数组,而且有很多库可供选择。
我们要使用的一个库叫Truth
我用的IDEA,在这里面导入依赖,在通过import org.junit.Test;
就可以使用@Test
来测试了。
然后这个Truth不知道怎么搞,IDEA开自动导包后,就能用assertEquals( ,)了。
Array and Lists
我们为什么要构建另一个其于数组的列表呢?为什么我们不能一直使用这个列表呢?
虽然这个列表对于我们上次构建的那些操作非常快,比如addLast、getLast、removeLast,但是 让我们考虑尝试从列表中获取任意项,当你的列表变得非常非常大时,这个获取操作可能会变慢,如果你有一个数组无论数组有多大, 如果我要求数组中的一个项目,它都会很快。
我们想使用数组(大小是固定的)来构建一个列表(一个无限可扩展的数据结构)
希望我的 AList有相同的公共方法,用户与之交互的那些东西,我应该能够走到我的 AList前 说,添加这个项目 或者给我最后一个项目 或者移除最后一个项目,但不同的是底层实现
public class AList {
private int[] items;
private int size;
//size =8
//[5,2,7,6,0,2,4,2,0,0...]
// 0 1 2 3 4 5 6 7 8 9
//The next item we want to add,will go in position...size!
//The last item in the list is at position...size- 1.
//Create an empty list
public AList(){
items = new int[100];
size = 0;
}
public void addLast(int x){
if(size == items.length){
resize(size*2);
}
items[size] = x;
size += 1;
}/*数组太小可以创建一个新的更大的数组,
为了提升运行速度,我们用*2而不是+某个数*/
public void resize(int x){
int[] a = new int[x];
System.arraycopy(items, 0, a, 0, size);
items = a;
}//把两段代码分开可以在测试时更方便
public int getLast(){
return items[size-1];
}
public int removeLast(){
int x = getLast();
size -= 1;
return x;
}//这里的想法是,让用户看不见原来的最后一项就可以了,没必要更改
public int size(){
return size;
}
}
但仍然存在一个问题,那就是我的底层支持数组,我调整了它的大小使其变得越来越大,直到有了数十亿个项目的空间。但我想要移除了一部分,这会使它是一个巨大的数组,但有大量未使用的内存盒子,我浪费了这些空间。我们可以用让数组变大类似的方法,创建一个新的比较小的数组,再来复制。
那如果我们不想用整数类型了呢?public class AList<Glorp>{}
可以使用泛型。
但是Java 有一个非常奇怪的语法怪癖,不允许你实例化泛型类型的数组,如Glorp] a = new Glorp[x];
这样是错误的;正确的操作应该是Glorp] a =(Glorp[])new object[x];
虽然这样可能会引起编译器的抱怨,但这是不可避免的。
当你的类型不是整数,假设是图片的话,在一个通用的AList里,如果过有人想删除索引号为2的图片,我实际上确实想添加这一行额外的代码,其中我将该项目的内存盒子设置为null,这样的话,不再有对这个图片的引用了,垃圾收集器才能对其进行销毁。
Interface and lmplementation lnheritance
当我们调用函数时,可能会遇到我们想传入的是AList,而函数传入类型是SLList,类型不匹配而出现问题。但一个可能的想法是,也许我会复制我的函数,然后 不是接收一个 SLList,而是完全相同的方法,但我会让它接收一个 AList,如:
public static string longest(AList<string> list){
...
}
public static string longest(SLList<string> list){
...
}
主体的方法将是完全复制粘贴的,有时候这被称为overloading
但是,我不喜欢这样,实际,它不仅是两倍长,而且在某种意义上,它是相同的代码重复了两次,
如果你想传入别的list,你还得复制,如果你必须在其中一个方法中修复一些东西,你也必须记得回去在所有复制粘贴的版本中修夏它,这会使事情变得很麻烦,我们得找到更好的办法!
在英语中,有一个花哨的词,叫做上位词,那我们能把这个想法用在Java上吗?
我们不难看出,AList和SLList都是某种类型的列表,如果我们使用我们的高级语法词汇,那我们可以说列表是一个超名称。
我可以做的事是首先定义一个新类型,我们要做的是定义一个引用类型然后指定我们的具体列表上次的SLList,还有AList,我们需要指定这些是具体的列表。
我要查看我的列表,并说,你知道吗?而且,我并不会真正编写方法体,并告诉程序如何addFirst,
我实际上会加上一个分号并说,这就是List61B所有需要有addFirst方法的语句(以同样的方法添加其他语句),我在这里做的是指定了一个类型,告诉我List61B必须做的事情的蓝图,如果我给你一个List61B对象,你可以肯定它有所有这些方法
public interface List61B<Item>{
public void addFirst<Item x>;
public void addLast<Item x>;
...
}
告诉了你一个对象必须能做什么的清单,这是第一步,接下来是第二步,现在我们有了通用列表
我们需要指定AList和SLList,它们是特定类型的列表
public class AList<Item> implements List61B<Item>{
public void addLast<Item x>{}
}
我们拿这个空白的方法,然后用这个方法替换它这个方法将包含有关如何向AList添加最后一个的实际信息,我们可以说在子类中的函数中的参数,这个addLast方法,覆盖了List61B的addLast方法。
现在我们可以接受任何你想要的东西,甚至至还有尚未发明的列表,这相当酷!
public static string longest(List61B<string> list){
...
}
相比之下 之前看到的情况是,你有相同的名称,但它们有不同的参数,一个接受AList ,另一个接受 SLList ,那被称为重载,我们来看以下的情况:
public interface Animals{
public void makeNoise();
}
public class Dog implements Animals{
public void makeNoise(Dog x){}
public void makeNoise(){}
}
现在情况是这样,这两伱都在那里晃来晃去,你使用哪个取決于你调用函数时传入了哪些参数。
我要逐个检查,每当我有一个方法覆盖了父类的方法时,我就会添加一个额外的标记:
@0verride
public void insert(Blorp item,int position){
...
}
就像是对自己的一个提醒,这个插入方法可不是普通的插入方法,它特别是一个被重写的插入方法因为我的父接口有一个空白的插入方法我用这个新的、改进过的 SLList 特定的方法来替代它,我要浏览一下,加上这个覆盖标签,只是为了提醒自己或者代码阅读者,嘿,这个insert方法,可不是从哪里随便拿来的。
另一个 @Override 的好处是,它可以捕捉到拼写的错误,如果你在一个实际上没有覆盖的方法上加上这个标记,它会发出警告。
每当你实现一个接口,你必须拿这些空白的方法并用实际的实现来覆盖它们,否则Java会在编译程序时失败。
public interface List61B<Item>{
...
public void proo();
}
如果我添加这个额外的空白的方法,那所有的List61B都必须能够proo,否则不能的将不再编译。
还有另一种继承,叫做 实现继承:
这一次,你不仅仅继承了空白签名,你实际上还会继承一些实现,现在子类不仅仅是得到了必须去覆盖的自空白签名,它还得到了一些实际的代码。
public interface List61B<Item>{
default public void print(){
for(int i = 0; i < size();i += 1){
System.out.print(get(i) + " ");
}
System.out.println();//表示换行
}
关键字default表示,这是一个默认实现,提供给所有子类
对以上代码,我们发现,对SLList来说太慢了,那怎么改进呢?
我们可以在SLList中重写
public class SLList{
@Override
public void print(){
for(Node p = sentinel.next;p != null;p = p.next){
System.out.print(p.item + " ");
}
System.out.println();
}
...
}
public static void main(String[] args) {
List61B<String> someList = new SLList<String>;
...
someList.print();
}
这里的print会用哪个呢?会用到SLList里重写的那个。
这里的想法是:你在Java中创建的每个变量实际上都只有一种类型,而是两种。
每个变量都会有一个编译时类型(人们称之为静态类型),那就是你在声明变量时指定的类型。
相比之下下所有变量都有第二种类型:被称为它们的运行时类型(人们称之为动态类型),这是你在实际分配变量时指定的类型,目前是空的,指定为实际对象
Extends Casting Higher Order Functions
让我们尝试构建一个旋转的SLList
一个旋转的SLList就是一个SLList,但它多了一个额外的特性,叫做向右旋转。
我们用 extends 告诉Java,RotatingSLList就是一个SLList
public class RotatingSLList<Item> extends SLList<Item> {
public static void main(String[l args){
(
public void rotateRight(){
Item x=removeLast();
addFirst(x);
}
RotatingSLList<Integer>rsl = new RotatingSLList<>();
rsl.addLast(10);
rsl.addLast(11);
rsl.addLast(12);
rsl.addLast(13);
/* Should be:[13,10,11,12] */
rsl.rotateRight();
rsl.print();
}
我们学到了如果你想要扩展另一个类,就使用关键字extends。
我们使用 implemments,这个词是因为列表是一个接口,有所有那些空方法签名,如果你有一个接口,然后子类是一个类的情况,你必须使用 immplemments 这个词。
相比之下,当我们说旋转的SLList是List的子类时,它就是一个列表,SLList已经是一个类,我们使用了extends这个词,继承了SLList的所有实现细节。
接下来来看另一个例子VengefulSLList
每当你删除一个项目,VengefulSLList会记住被删除的项目,然后你可以要求VengefulSLList打印所有丢失的项目,它会告诉你删除了什么。
父类中的removeLast():
public Item removeLast(){
Node back = getLastNode();
if(back == sentinel){
return null;
}
size = size -1;
Node p = sentinel;
while (p.next != back){
p = p.next;
}
p.next = null;
return back.item;
}
public class VengefulSLList<Item> extends SLList<Item> {
SLList<Item> lostItems;
public VengefulsLList(){
lostItems = new SLList<>();
}
public void printLostItems(){
lostItems.print();
}
@0verride
public Item removeLast(){
Item x= super.removeLast();
lostItems.addLast(x);
}
public static void main(String[] args){
VengefulSLList<Integer>vs1 = new VengefulSLList<>();
vs1.addLast(1);
vs1.addLast(5);
vs1.addLast(10);
vs1.addLast(13);
vs1.removeLast();
vs1.removeLast();
System.out.print("The fallen are:");
vs1.printLostItems();
}
}
当我说super.removeLast时我进入父类,做所有这些事情。这里,我们用到了 关键字:super,如果我们直接复制粘贴父类中的removeLast,会访问不到其中private的东西。
每次调用VengefulSLList构造函数时,实际上也会调用超类的SLList构造函数,如果你真的想让你代码的读者知道,我在调用超类的构造函数,那么你可以添加一行额外的代码super();
public VengefulsLList(){
super();
lostItems = new SLList<>();
}
public VengefulsLList(Item x){
super(x);
lostItems = new SLList<>();
}
对super的调用,Java会隐式调用默认构造函数,不明确地说super,Java会认为你想要没有参数的构造函数,所以在下面这段代码在一定要加上super。
你写的每个类都隐式地是 Object 的子类,即使你没有说 extends Object 这几个词,因为每个人都是 Object 的后代或子类,这意味着每个人都继承了 Object 定义的方法:
String toString()
boolean equals(Object obj)
int hashCode()
Class<?> getClass()
protected Object clone()
protected void finalize()
void notify()
void notifyAll()
void wait()
void wait(long timeout)
void wait(longtimeout,int nanos)
Stack栈,栈的背后思想是,你只能从栈中做两件事情,你可以将东西推到栈的顶部push(),我也可以从栈顶弹出东西pop()。
如果你在使用实现继承,你必须非常小心地使用is-a关系而不是has-a关系
封装: 如果你有一个对象,而且实现是完全隐藏的,你只能通过接口或者一组公共的方法来与这个对象交互,那么我们就说这个模块或者这个对象是被封装的。
Java 的一个很棒的地方在于它使封装更加严密,因为如果你在Java 中使用封装,Java 有那个 private 关键字,而 private 关键字强制这个隔离是绝对的。
这如何与实现继承配合呢?
Lecture 9
在课里36:36左右讲了一个Dog的例子,例子告诉我们,实现继承可能会破坏封装
最后,再展示一个谜题:
public static void main(Stringl] args){
VengefulSLList<Integer>vsl=new VengefulsLList<Integer>(9);
SLList<Integer>sl = vsl;
sl.addLast(50);
sl.removeLast();
s1.printLostItems();
VengefulsLList<Integer>vsl2 = sl;
}
因为并非所有的 SLlist 都可以打印丢失的项目,编译器实际上会在s1.printLostItems();
停止,并说,不,我不会编译这个程序,再看VengefulsLList<Integer>vsl2 = sl;
,因为SLList不一定是VengefulsLList,所以,同样这里会编译失败。这里的问题,基本上是编译器基于静态类型进行检查。
SLList<Integer>sl = new VengefulsLList<Integer>();
VengefulsLList<Integer> vsl = new SLList<Integer>();
第一行是正确的,第二行是错误的。编译器非常小心!!!我们在调用函数时要非常注意!一个例子是函数返回类型是Dog,是不能赋给PoodleDog的,为了解决这个问题,我们要添加一个额外的小语法片段,叫做转换。
Poodle largerPoodle =(Poodle)maxDog(frank, frankJr);
这个转换的作用是改变编译时的类型,再看如下:
Poodle frank = new Poodle("Frank",5);
Malamute frankSr = new Malamute("Frank Sr",100);
Poodle largerPoodle =(Poodle)maxDog(frank, frankSr);
我试图将其转换并赋值给Poodle变量,但代码在运行时崩溃了,因此,在进行转换时,你必须非常小心。
Subtype Polymorphism Comparators Comparable
让我们创建一个表示函数的对象,并将该对象传递。
那么我要做的是首先说明,有很多函数我可以传递,也许我会创建一个接口来表示我可以做的所有可能的函数。
public interface IntUnaryFunction {
public int apply(int x);
}
public class TenX implements IntUnaryFunction{
@Override
public int apply(int x){
return 10 * x;
}
}
public class HoFDemo {
public static int doTwice(f,int x){
}
}
看第三段,如果按如上会出问题,它应该接受一个函数,那函数的类型是什么呢?
那就是IntUnaryFunction
,这就是我们要的内存盒类型。
public class HoFDemo {
public static int doTwice(IntUnaryFunction f,int x){
return f.apply(f.apply(x));
}
public static void main(String[] args) {
int result = doTwice(new TenX(), 2);
System.out.println(result);
}
}
记住 f 是一个对象应该用 f.apply。
new TenX()
我不能只放类的名字,我必须说,这是一个TenX对象。
子类型多态性
多态这个词的意思是为不同类型的实体提供一个统一的的接口。
在子类型多态性的方法中,我们让对象为我们做所有的思考,这个对象会选择适合自己的方法,并为我们所用。
接下来我们尝试构建一些东西来体会子类型多态的力量。
我们首先声明这里是表示可以进行比较的所有对象的接口。
public interface OurComparable {
/*return -l if I am less than o.
return 0 if I am equal to o.
return 1 if I am greater thon o.*/
public int compareTo(Object o);
}
public class Dog implements OurComparable{
public String name;
private int size;
public Dog(String n,int s){
name = n;
size = s;
}
public void bark(){
System.out.println(name + "says:bark");
}
@Override
public int compareTo(Object o){
Dog otherDog = (Dog) o;
if(this.size < otherDog.size){
return -1;
}else if(this.size == otherDog.size){
return 0;
} else {
return 1;
}
}
}
觉得if语句太长,也可以更改为this.size - otherDog.size;
public class Maximizer {
public static OurComparable max(OurComparable[] items){
int maxDex = 0;
for(int i = 0; i < items.length; i++){
int cmp = items[i].compareTo(items[maxDex]);
if(cmp > 0){
maxDex = i;
}
}
return items[maxDex];
}
public static void main(String[] args){
Dog d1 = new Dog("Elyse",3);
Dog d2 = new Dog("Sture",9);
Dog d3 = new Dog("Benjamin",15);
Dog[] dogs = new Dog[]{d1, d2, d3};
Dog maxDog = (Dog) Maximizer.max(dogs);
maxDog.bark();
}
}
经过那么多工作,我们创建了所有可比较事物的新界面。
但,如果想要OurComparable变得通用,我们得自己编写很多东西。
事实证明,我们不必使用我们编写的小OurComparable,Java有自己的接口叫Comparable,并且它内置在Java中,有一个好处是Comparable 接受一个泛型类型,这让我们少做一些类型转换。
public class Dog implements Comparable<Dog>{
public String name;
private int size;
public Dog(String n,int s){
name = n;
size = s;
}
public void bark(){
System.out.println(name + "says:bark");
}
@Override
public int compareTo(Dog otherDog){
return this.size - otherDog.size;
}
}
这意味着我们可以使用不同的库函数,事实证明,像max这样的函数也已经为我们编写好了:
Dog largest = Collections.max(Arrays.asList(dogs));
另一个好处是,因为它接受了这种小占位符类型,,我这里不用说对象,我可以直接说Dog。
自然顺序: 如果你在世界上所有的狗身上反复调用compareTo方法,并旦你可以使用compareTo方法将们按某种顺序排列,那么得到的顺序就被称为自然顺序。
我要去实现一个新的比较器,通过大小比较事物。
import java.util.Comparator;
public class NameComparator implements Comparator<Dog>{
@Override
public int compare(Dog o1,Dog o2){
return o1.name.compareTo(o2.name);
}
}
为什么要这样做?因为原来的狗,已经有了自己的自然顺序比较方法,它不能学习更多的比较方法,我们不得不建立一个新的狗比较机。(比较器是一个对象)
lterators, Object,Methods
这节课的主要内容都集中在构建一个ArraySet。
So,what is a set?
集合,是一组无序的对象,不能像列表那样说,给我第五个元素。集合不允许重复。你可以向集合添加东西,也可以询问集合是否包含某项。
public class ArraySet<T> {
private T[] items;
private int size;
public ArraySet(){
items =(T[]) new Object[100];
size = 0;
}
public void add(T x){
if(!contains(x)){
items[size]=x;
size += 1;
}
}
public boolean contains(T x){
for(int i = 0;i < size;i += 1){
if(items[i].equals(x)){
return true;
}
}
return false;
}
public static void main(String[] args) {
ArraySet<Integer> S = new ArraySet<>();
S.add(5);
S.add(12);
System.out.println(S.contains(12));
System.out.println(S.contains(10));
}
}
接下来,我们将向数组添加三个额外的功能。
Set<Integer>javaset = new Hashset<>();
javaset.add(5);
javaset.add(23);
javaset.add(42);
for(int i:javaset){
System.out.prinln(i);
}
增强for循环与如下等同:
我要以某种方式调用一个叫做迭代器的方法,然后我要询问seer,只要你能看到更多的东西,请给我下一样东西然后打印出来,简化来说,我们需要的是一个叫做迭代器的对象,它会喊5,然后它会跨到下一个项目。
Iterator<Integer> seer= javaset.iterator();
while (seer.hasNext()){
int x= seer.next();
System.out.println(x);
}
那我们如何让迭代器在ArraySet工作呢?我需要额外添加什么代码?
我将不得不在ArraySet内部添加一个迭代器方法,以便为我生成新的向导,然后向导本身必须具有hasNext和next方法。
我们先创建一个迭代器接口:
public interface Iterator<T> {
boolean hasNext();
T next();
}
再在ArraySet中添加如下代码:
private class ArraySetIterator<T> implements Iterator<T>{
private int a;
private ArraySetIterator(){
a = 0;
}
@Override
public boolean hasNext() {
return a < size;
}
@Override
public T next() {
T returnitem = (T) items[a];
a += 1;
return returnitem;
}
}
public Iterator<T> iterator(){
return new ArraySetIterator<>();
}
现在,我们可以使用如下代码了:
Iterator<Integer> seer = S.iterator();
while(seer.hasNext()){
int x=seer.next();
System.out.println(x);
}
那如何使用更漂亮的增强for循环呢?
我们需要告诉Java我们有一个迭代器方法(尽管我们能看到,但Java不知道)。
为了告诉Java ArraySet有一个选代器方法,换句话说它是可以遍历的,我实际上必须在这里使用一个新的接口,叫做Iterablepublic class ArraySet<T> implements Iterable<T>{}
,我向Java保证,这个类它是可选代的,我保证它有一个迭代器方法。然后我们美丽的for循环就能使用啦。
for(int i : S){
System.out.println(i);
}
public interface Iterable<T>{
Iterator<T> iterator();
}//java内置的
你可以把可迭代想象成世界上所有具有迭代器的类.。
接下来讲的是对象方法。我们要写两个对象方法。
toString方法: 它获取你当前的对象,然后以字符串的形式呈现给你。
- toString()方法在Object类里定义的,其返回值类型为String类型,返回一个包含类名和哈希码的字符串
- 在进行String类与其他类型的连接操作时,自动调用toString()方法,如下:
Date now = new Date();
System.out.println("now ="+ now);//相当于下一行代码
system.out.println("now ="+ now.toString());
- 实际应用中,可以根据需要在用户自定义类型中重写toString()方法,如Strng类重写了toSting()方法,返回字符串的值。
@Override
public String toString(){
String x="(";
for(T i :this){
x += i.toString() + " ";
}
/*this代表我正在迭代自己*/
x += ")";
return x;
}
在Java中,如果你试图将两个字符串加在一起,你实际上正在创建一个全新的字符串,这可能会有点慢,那如何改进?
@Override
public String toString(){
StringBuilder x = new StringBuilder();
x.append("(");
for(T i:this){
x.append(i.toString());
x.append(" ");
}
x.append(")");
return x.toString();
}
equals方法:
equals 和 == 不相同,==等于检查两个内存盒子的地址是不是相同的,我可以使用equals方法,希望我得到true告诉我,尽管它们不是同一个对象,它们以某种方式代表相同类型的集合。
但在Object.java中,equals只是执行 == 比较,为了达成我的目的,我要重写equals。
这里要用到一个新的关键字 instanceof if(o instanceof ArraySet otherArraySet){}
otherArraySet可加可不加,意思是如果检查的结果是true,会为你生成一个otherArraySet,当然,加了可能会因为java版本或编译器的问题编译不通过,以下代码用传统形式:
@Override
public boolean equals(Object o){
if(o instanceof ArraySet){ //check if o is an arraayset
ArraySet<T> otherArraySet = (ArraySet<T>) o;
if(this.size != otherArraySet.size){
return false;
}
for(T i:this){
if(!otherArraySet.contains(i)){
return false;
}
}
return true;
}
return false;
}
Asymptotic
Asymptotic Analysis
这节课将从一些如何衡量程序运行的时间开始。
一个例子:检查从小到大排列好的数组有没有重复数字:
方案一:一 一检查
public static boolean dupl(int[]A){
for(int i=0;i<A.length;i += 1){
for(int j=i+1;j<A.length; j += 1){
if(A[i]== A[j]){
return true;
}
}
}
return false;
}
方案二:检查相邻的项
public static boolean dup2(int[]A){
for(inti=0;i<A.length-1;i += 1){
if(A[i]== A[i + 1]){
return true;
}
}
}
return false;
}
我们的目标是找到一种数学方法来证明重复1比重复2哪个更好。
import com.google.common.base.Stopwatch;
public class YourClassName {
public static void main(String[] args) {
int testsize = 1000;
double lasttestruntime = 0.0;
while (lasttestruntime < 10) {
int[] arr = new int[testsize];
for (int i = 0; i < testsize; i++) {
arr[i] = i;
}
Stopwatch s = Stopwatch.createStarted(); // 启动计时
boolean b = dup1(arr); // 假设 dup1 是你定义的方法
double newtime = s.elapsed().toMillis(); // 获取经过的时间(毫秒)
System.out.println("Test of size " + testsize + " completed in " + newtime + " ms");
lasttestruntime += newtime; // 更新运行时间
}
}
}
如果你仅仅是随便玩玩,最终你会发现dup1如果你把它在这个大小上花费的时间除以之前测试花费的时间,你实际上得到的数字非常接近4,而dup2的非常接近2。
让我们来找一下能不能找到这背后的数学原因。
如果我们有很多东西,比如数十亿个粒子,数十亿个值,那就是我们开始更担心速度变慢的时候,一般来说,我们寻找更好扩展的算法,更像线条的东西,而不是扩展较差的东西,看起来像抛物线的东西。
现在,我们要做一些简化来尝试看看构成正在发生的核心支撑的是什么。
我们的第一步将只忽略我们的最佳情况,我们只看最坏情况(的运行时间)。
对于dup1,最坏情况发生在我们的项没有重复。
课里讲了很多来说明dup1是 N2 型的,其实就是个很简单的数学问题喵(讲了一些判断增长阶的,我称之为抓大头)
如果我们说一个函数是 Θ(n)我们实际上是在说运行该函数的时间复杂度是 Θ(n)
我们来看个例子:
public static void printParty(int N){
for(int i=1;i<=N;i=i*2){
for(int j=0;j<i;j+= 1){
System.out.println("hello");
int ZUG =1+1;
}
}
}
这个复杂度是什么呢?答案是:N 为什么?
1+2+4+8+…+2k =2*(2k)-1
if N = 2k 1+2+4+…+N=2N-1
我们没有简单的方法来判断一个函数是Θ(N)还是Θ(N log N),除非通过直接的数学分析。以下四种情况较为常见。
这里有一个较为好用的方法:积分 (即使有这个快捷方式,它在范围上也往往有些有限)
∫
0
N
f
(
x
)
d
x
\int_0^Nf(x)\,dx
∫0Nf(x)dx
public void addLast(int x){
if(size == items.length){
resize(size*2);
}
items[size] = x;
size += 1;
}
Amortized Analysis
让我们回到addLast,来看一下摊销分析。
如果你向数组列表添加n个项目,需要Θ(n)的时间,如果我们将这两者相除,那意味着添加一件事,平均需要一个单位的时间,这被称为摊销运行时间(摊销其实和平均差不多一个意思)
这种摊销运行时间有时实际上比最坏情况下的运行时间更有用,因为任何单个操作可能需要更长的时间,在addLast中,每次调整大小时,你都需要花费很长的时间来复制和粘贴这些值,这就是为什么我们最终使用这个乘法因子的原因,因为这种渐进的运行时间要好得多。
如果我们在大量操作中使用这个,我们将会有更好的平均性能保证。
Recursive Analysis
public static int f3(int x){
if(x <= 1)
return 1;
return f3(x-1) + f3(x-1);
}
来看一下f3调用多少次:C(3)=1+2+4,C(N)=1+2+4+…+2N-1
答案是 2N-1
但如果把其中一个x-1换成x-2,分析就会变得困难:
public static int fib(int x){
if(x <= 1)
return 1;
return fib(x-1) + fib(x-2);
}
我们不能像之前那样简单的分析。
为了进行这种分析,你需要注意到这代表了斐波那契数,你只需要以一种奇怪的方式计数,其中你分别计算叶节点的数量和内部节点的数量。
如果你计算叶节点的数量,每个叶节点都对应将总和增加。(我们每有一个叶子,就返回1) 所以叶子节点的总数是第n个斐波那契数
F
n
F_n
Fn(1 2 3 5 8…)
为了将一切都组合起来,每一步都将两个数字相加到一个 其他节点的数量为 (
F
n
F_n
Fn-1 )
综上,C(N)=2
F
N
F_N
FN-1=Θ(
F
N
F_N
FN)
如果你做数学运算,你会发现
F
N
F_N
FN∈Θ(φN)
φ为黄金分割比,约为1.618
我们可以说这个的总运行时间大约是Θ(φN),(这些计算的部分了解一下就好)
这里想说的是:就是这么微小的改变,我把这个1改成了2 结果完全改变了运行时间
事实证明,我们可以进行精确计算,显示它是1.618的n次方,实际上,你可能甚至无法得到这个Θ边界,只要有了o和w的边界,你可能就能说这是指数增长。我们之所以能够计算出这个特定的渐近性,唯一的原因是因为我们知道斐波那契数列的性质。
(这部分在Lecture16,听的有点晕( ╯□╰ ))
Binary Search
接下来的例子将讨论二分查找的算法
static int binarySearch(String[] sorted,String x,int lo, int hi){
if(lo >hi) return -1;
int m=(lo+hi)/2;
int cmp =x.compareTo(sorted[m]);
if(cmp<0)return binarySearch(sorted,x,lo,m-1);
else if(cmp >0)return binarySearch(sorted,x,m+ 1,hi);
}
这个想法是你有一列排序好的项目,你想要找到一个特定的项目,你拿到一个已排序的列表 把它分成两半,看看你的项在哪一边。
二分查找的运行时间(最坏情况)是多少?
l
o
g
2
log_2
log2N+1=Θ(logN)
(你不需要比二分搜索更快,因为对数时间就是非常非常快的)
如果我们对一个数字取整,那仍然是原函数的Θ阶
在排序算法中 我们的目标是根据某种可比较的方式对列表中的项目进行排序,我们假设我们可以在Θ(1)的时间内比较事物,所以进行比较的时间是常数,有了这个前提,这里大致是我们如何进行归并排序的想法:
static List<Comparable>sort(List<comparable>x){
List<comparable> firsthalf= sort(x.sublist(0,x.size()/2));
List<Comparable> secondhalf= sort(x.sublist(x.size()/2,x.size()));
return merge(firsthalf,secondhalf);
}
这个合并行为的时间复杂度为Θ(N);
只要我们在寻找渐近运行时间,我们可以简化为只关心实际运行的操作:
static void sortRuntime(int n) {
sortRuntime(n / 2); // 递归调用
sortRuntime(n / 2); // 递归调用
// n units of work
}
每下降一层,你会使得项目数量翻倍,但每个项目都会减半。因此,总的来说,在每个层面上的总运行时间都等于 n。
如果我们有总共k层,那么我们的总运行时间将是n乘以k,因为每一层都做相同量的工作,不难看出k就是log n,所以我们的运行时间为n log n
(重要的是要注意,这个特定的属性只适用于这个特定的2的n次方对数和倍增数)
我们回到dup1,dup2,如果没有排序,dup2不能用,dup1的时间复杂度Θ(n2),我们可以通过排序再用dup2来加速,得到的时间为n log n + n
Disjoint Sets
我今天要做的两个简化,实际上不会影响功能,也不会使数据结构变得更糟:
1.我会强制所有items都是整数;
2.我会声明有多少个items。
public interface DisjointSets {
/* Connects two items p and q.*/
void connect(int p,int q);
/* Checks to see if two items are connected. */
boolean isConnect(int p,int q);
}
我们的实现选择将决定实现这个的难易程度,也决定了速度。
你开始注意到当事情变得缓慢和不是一个好主意的时候,我们可以在这里停下来,寻找更好、更巧妙的实现。
如课程中的这个例子,一遍遍迭代会很麻烦,但我们可以分别把左中右看成一部分,这会让事情变得简单。{0,1,2,4} {3,5} {6}我们发现,这是个好主意!但我们仍须实现它。
如果要构建一个集合列表,那也会变得很麻烦,那要是如上图在同一部分的数字的位置上放上相同的数字,那就能使事情变得简单,我只需要检查数组的一两个事物,然后就完成了。但在连接时,如连接0和5,就得把第一组所有项都改成5,这又会花费很多时间。我们称这种想法为QuickFind
public class QuickFindDS implements DisjointSets{
public int[] id;
public QuickFindDS(int n){
id = new int[n];
for(int i = 0;i < n;i += 1){
id[i] = 1;
}
}
@Override
public void connect(int p, int q){
int pid = id[p];
int qid = id[q];
for(int i = 0;i < id.length;i++){
if(id[i] == pid){
id[i] = qid;
}
}
}
@Override
public boolean isConnect(int p, int q) {
return id[p] == id[q];
}
}
一个截然不同的想法是:不是说每个人都是item的一部分,与其说你属于item编号,不如给每个item分配一个父项
我们给每一部分的boss一个 -1,用数字表明是第几项的下属
public class QuickUnionDs implements DisjointSets{
private int[] parent;
public QuickUnionDs(int N){
parent = new int[N];
for(int i=0;i<N;i++){
parent[i] = -1;
}
}
public int find(int x){
int r = x;
while (parent[r] >= 0) {
r = parent[r];
}
return r;
}
@Override
public void connect(int p, int q) {
int i = find(p);
int j = find(q);
parent[i] = j;
}
@Override
public boolean isConnect(int p, int q) {
return find(p) == find(q);
}
}
这种想法有可能会导致0–1–2–3–4这种连的很长的情况(速度变慢),如何避免?
现在我要添加一种叫做加权快速并查集的方法。这最终将给我们带来最先进的数据结构。
(当我们将两棵树连接在工起时 进行这个连接操作时,应始终选择较小的树 并将其放在较大的树的下面)
一个巧妙的追踪方法是用负数表示你是boss,-(数字),数字表示你的items有多少个。如果你使用加权快速联合规则,最坏情况下的树高是
l
o
g
2
log_2
log2 N。
当然,比较“树高”可以得到差不多的结果,但编码起来会比较困难.
下一个将我们带入真正快速不相交集实现的巧妙想法是:当你爬树时,你应该真正地跟踪你沿途看到的所有东西,在我沿途遇到的任何项目,我都会将其与根节点关联起来,这是一种数据结构,你使用它的次数越多,你就越爬高树,调用 isConnected 和 callConnected,你爬得越多,使用这种数据结构的速度就越快。