算法(第四版) 1.2 数据抽象
1.2 数据抽象
我们研究同一个问题的不同算法的主要原因在于他们的性能特点不同。抽象数据类型正适合于对算法的研究,因为它确保我们可以随时将算法性能的知识应用于实践:可以在不修改任何用例代码的情况下用一种算法替换为另一种算法并改进所有用例的性能。
1.2.1 使用抽象数据类型
所有对象都有三大重要特性:状态、标识和行为。对象的状态即数据类型的值。对象的标识能够将一个对象区别于另一个对象。可以认为对象的标识就是它在内存中的地址。对象的行为就是数据类型的操作。
创建对象
每当用例调用了new(),系统都会:
- 为新的对象分配内存空间
- 调用构造函数初始化对象中的值
- 返回该对象的一个引用
静态方法的主要作用是实现函数,非静态(实例)方法的主要作用是实现数据类型的操作。
使用对象
Counter类的用例,模拟T次掷硬币
public class Counter {
private final String name;
private int count;
public Counter(String id) {
name = id;
}
public void increment() {
count++;
}
public int tally() {
return count;
}
public String toString() {
return count + " " + name;
}
public static void main(String[] args) {
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
heads.increment();
heads.increment();
tails.increment();
StdOut.println(heads + " " + tails);
StdOut.println(heads.tally() + " " + tails.tally());
}
}
/**
* Counter类用例,模拟T次掷硬币
* @author Dell
*
*/
public class Flips {
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
for (int t = 0; t < T; t++) {
if (StdRandom.bernoulli(0.5)) {
heads.increment();
}else {
tails.increment();
}
}
StdOut.println(heads);
StdOut.println(tails);
int d = heads.tally() - tails.tally();
StdOut.println("delta: " + Math.abs(d));
}
}
输出结果:
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Flips 10
5 heads
5 tails
delta: 0
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Flips 10
4 heads
6 tails
delta: 2
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Flips 1000000
499102 heads
500898 tails
delta: 1796
赋值语句
Counter c1;
c1 = new Counter("ones");
Counter c2 = c1;
c2.increment();
使用引用类型的赋值语句将会创建该引用的一个副本。赋值语句不会创建一个新的对象,而只是创建另一个指向某个已存在的对象的引用。这种情况被称为别名。
将对象作为参数
将对象作为返回值
这种能力非常重要,因为Java中方法只能有一个返回值——有了对象我们的代码实际上就能返回多个值。
/**
* 一个接受对象作为参数并将对象作为返回值的静态方法的例子
* @author Dell
*
*/
public class FlipsMax {
public static Counter max(Counter x, Counter y) {
if (x.tally() > y.tally()) {
return x;
}else {
return y;
}
}
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
for (int t = 0; t < T; t++) {
if (StdRandom.bernoulli(0.5)) {
heads.increment();
}else {
tails.increment();
}
}
if (heads.tally() == tails.tally()) {
StdOut.println("Tie");
}else {
StdOut.println(max(heads, tails) + " wins");
}
}
}
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.FlipsMax 1000000
500846 tails wins
数组也是对象
当我们将数组传递给一个方法或是将一个数组变量放在赋值语句的右侧时,我们都是在创建数组引用的副本,而非数组的副本。
对象的数组
数组元素可以是任意类型。
下面例子模拟制骰子。
public class Rolls {
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
int SIDES = 6;
Counter[] rolls = new Counter[SIDES + 1];
for (int i = 1; i <= SIDES; i++) {
rolls[i] = new Counter(i + "'s");
}
for (int t = 0; t < T; t++) {
// Returns a random integer uniformly in [a, b).
int result = StdRandom.uniform(1, SIDES + 1);
rolls[result].increment();
}
for (int i = 1; i <= SIDES; i++) {
StdOut.println(rolls[i]);
}
}
}
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Rolls 1000000
166765 1's
166795 2's
166690 3's
166691 4's
166578 5's
166481 6's
运用数据抽象的思想编写代码(定义和使用数据类型,但数据类型的值封装在对象中)的方式称为面向对象编程。
数据类型指的是一组值和一组对值的操作的集合。
/**
* Cat是一个In和Out的用例,它使用了多个输入流来将多个输入文件归并到同一个输出文件中。
* @author Dell
*
*/
public class Cat {
public static void main(String[] args) {
// 将所有输入文件复制到输出流(最后一个参数)中
Out out = new Out(args[args.length -1]);
for (int i = 0; i < args.length - 1; i++) {
// 将第i个输入文件复制到输入流中
In in = new In(args[i]);
String s = in.readAll();
out.println(s);
in.close();
}
out.close();
}
}
D:\Soft\eclipse_rcp_workplace\algorithms4\src>more in1.txt
This is
D:\Soft\eclipse_rcp_workplace\algorithms4\src>more in2.txt
a tiny
test.
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Cat in1.txt in2.txt out.txt
D:\Soft\eclipse_rcp_workplace\algorithms4\src>more out.txt
This is
a tiny
test.
1.2.2 抽象数据类型举例
集合类抽象数据类型的主要用途是简化对同一类型的一组数据的操作。
1.2.3 抽象数据类型的实现
实例变量
实例变量和静态方法或是某个代码段中的局部变量最关键的区别是:每一时刻每个局部变量只会有一个值。但每个实例变量则对应着无数个值(数据类型的每个实例对象都会有一个)。
构造函数
实例方法
在一个实例方法中对变量的引用指的是调用该方法的对象的值。
作用域
API、用例与实现
典型的用例:
/**
* Counter类用例,模拟T次掷硬币
* @author Dell
*
*/
public class Flips {
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
for (int t = 0; t < T; t++) {
if (StdRandom.bernoulli(0.5)) {
heads.increment();
}else {
tails.increment();
}
}
StdOut.println(heads);
StdOut.println(tails);
int d = heads.tally() - tails.tally();
StdOut.println("delta: " + Math.abs(d));
}
}
数据类型的实现
public class Counter {
private final String name;
private int count;
public Counter(String id) {
name = id;
}
public void increment() {
count++;
}
public int tally() {
return count;
}
public String toString() {
return count + " " + name;
}
public static void main(String[] args) {
Counter heads = new Counter("heads");
Counter tails = new Counter("tails");
heads.increment();
heads.increment();
tails.increment();
StdOut.println(heads + " " + tails);
StdOut.println(heads.tally() + " " + tails.tally());
}
}
使用方法
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Flips 1000000
499102 heads
500898 tails
delta: 1796
1.2.4 更多抽象数据类型的实现
日期
在实现中使用数据抽象的一个关键优势是我们可以将一种实现替换为另一种实现而无需改变用例的任何代码。
数据类型的实现
/**
* 数据类型的实现
* @author Dell
*
*/
public class Date {
private final int month;
private final int day;
private final int year;
public Date(int m, int d, int y) {
month = m;
day = d;
year = y;
}
public int month() {
return month;
}
public int day() {
return day;
}
public int year() {
return year;
}
@Override
public String toString() {
return month() + "/" + day() + "/" + year();
}
}
数据类型的另一种实现
/**
* 数据类型的另一种实现
* @author Dell
*
*/
public class Date2 {
private final int value;
public Date2(int m, int d, int y) {
value = y*512 + m*32 + d;
}
public int month() {
return (value / 32) % 16;
}
public int day() {
return value % 32;
}
public int year() {
return value / 512;
}
@Override
public String toString() {
return month() + "/" + day() + "/" + year();
}
}
测试用例
public class TestDate {
public static void main(String[] args) {
int m = Integer.parseInt(args[0]);
int d = Integer.parseInt(args[1]);
int y = Integer.parseInt(args[2]);
Date date = new Date(m, d, y);
Date2 date2 = new Date2(m, d, y);
StdOut.println("date: " + date);
StdOut.println("date2: " + date2);
}
}
使用方法
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.TestDate 12 31 1999
date: 12/31/1999
date2: 12/31/1999
维护多个实现
如Date 和 Date2
累加器
累加器定义了一种能够为用例计算一组数据的实时平均值的抽象数据类型。
它的实现很简单:它维护一个int类型的实例变量来记录已经处理过的数据值的数量,以及一个double类型的实例变量来记录所有数据值之和,将和除以数据数量即可得到平均值。
请注意该实现并没有保存数据的值——它可以用于处理大规模的数据。
数据类型的实现
/**
* 累加器数据类型的实现
* @author Dell
*
*/
public class Accumulator {
private double total;
private int N;
public void addDataValue(double val) {
N++;
total += val;
}
public double mean() {
return total/N;
}
@Override
public String toString() {
return "Mean (" + N + " values): " + String.format("%7.5f", mean());
}
}
典型的用例
public class TestAccumulator {
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
Accumulator a = new Accumulator();
for (int t = 0; t < T; t++) {
a.addDataValue(StdRandom.random());
}
StdOut.println(a);
}
}
使用方法
D:\Soft\eclipse_rcp_workplace\algorithms4\src>javac chapter01/*.java
注: 某些输入文件使用或覆盖了已过时的 API。
注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.TestAccumulator 1000
Mean (1000 values): 0.49042
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.TestAccumulator 1000000
Mean (1000000 values): 0.50010
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.TestAccumulator 1000000
Mean (1000000 values): 0.49938
可视化累加器
数据类型的实现
public class VisualAccumulator {
private double total;
private int N;
public VisualAccumulator(int trials, double max) {
StdDraw.setXscale(0, trials);
StdDraw.setYscale(0, max);
StdDraw.setPenRadius(.005);
}
public void addDataValue(double val) {
N++;
total += val;
StdDraw.setPenColor(StdDraw.DARK_GRAY);
StdDraw.point(N, val);
StdDraw.setPenColor(StdDraw.RED);
StdDraw.point(N, total / N);
}
public double mean() {
return total / N;
}
@Override
public String toString() {
return "Mean (" + N + " values): " + String.format("%7.5f", mean());
}
}
典型的用例
public class TestVisualAccumulator {
public static void main(String[] args) {
int T = Integer.parseInt(args[0]);
VisualAccumulator a = new VisualAccumulator(T, 1.0);
for (int t = 0; t < T; t++) {
a.addDataValue(StdRandom.random());
}
StdOut.println(a);
}
}
可视化累加器图像
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.TestVisualAccumulator 2000
Mean (2000 values): 0.49969
1.2.5 抽象数据的设计
封装
设计API
算法与抽象数据类型
数据抽象使我们能够:
- 准确定义算法能为用例提供什么;
- 隔离算法的实现与用例的代码;
- 实现多层抽象,用已知算法实现其他算法。
将二分查找重写为一段面向对象的程序(用于在整数集合中进行查找的一种抽象数据类型)
API | public class StaticSETofInts | |
---|---|---|
StaticSETofInts(int[] a) | 根据a[]中所有值创建一个集合 | |
boolean contains(int key) | key是否存在于集合中 |
数据类型的实现
public class StaticSETofInts {
private int[] a;
public StaticSETofInts(int[] keys) {
a = new int[keys.length];
for (int i = 0; i < keys.length; i++) {
a[i] = keys[i]; // 保护性复制
}
Arrays.sort(a);
}
public boolean contains(int key) {
return rank(key) != -1;
}
private int rank(int key) {
// 二分查找
int lo = 0;
int hi = a.length - 1;
while (lo <= hi) {
// 键要么存在于a[lo...hi]之中,要么不存在
int mid = lo + (hi - lo) / 2;
if (key < a[mid]) {
hi = mid - 1;
}else if (key > a[mid]) {
lo = mid + 1;
}else {
return mid;
}
}
return -1;
}
}
典型的用例
public class Whitelist {
public static void main(String[] args) {
int[] w = In.readInts(args[0]);
StaticSETofInts set = new StaticSETofInts(w);
while (!StdIn.isEmpty()) {
// 读取键,如果不在白名单中则打印它
int key = StdIn.readInt();
if (!set.contains(key)) {
StdOut.println(key);
}
}
}
}
用法
D:\Soft\eclipse_rcp_workplace\algorithms4\src>java chapter01.Whitelist largeW.txt < largeT.txt
...
97167270
18307996
81294490
32208304
2853029
29798919
9505145
32449528
38862597
69830567
接口继承
这种继承机制,也叫子类型,即public class Date implements Dateable
实现继承
这种继承机制,也叫子类,即extends
字符串表示的习惯
封装类型
等价性
两个对象相等意味着什么?
-
自反性,
x.equals(x)为true
; -
对称性,
当且仅当y.equals(x)为true时,x.equals(y)返回true
; -
传递性,
如果x.equals(y)和y.equals(z)均为true,x.equals(z)也将为true
。另外,它必须接受一个Object为参数并满足以下性质:
-
一致性,
当两个对象均未被修改时,反复调用x.equals(y)总是返回相同的值
; -
非空性,
x.equals(null)总是返回false
。
在数据类型的定义中重写toString()和equals()方法
/**
* 在数据类型的定义中重写toString()和equals()方法
* @author Dell
*
*/
public class Date {
private final int month;
private final int day;
private final int year;
public Date(int m, int d, int y) {
month = m;
day = d;
year = y;
}
public int month() {
return month;
}
public int day() {
return day;
}
public int year() {
return year;
}
@Override
public String toString() {
return month() + "/" + day() + "/" + year();
}
@Override
public boolean equals(Object x) {
if (this == x) {
return true;
}
if (x == null) {
return false;
}
if (this.getClass() != x.getClass()) {
return false;
}
Date that = (Date) x;
if (this.day != that.day) {
return false;
}
if (this.month != that.month) {
return false;
}
if (this.year != that.year) {
return false;
}
return true;
}
}
如果两个对象的类不同,返回false。要得到一个对象的类,可以使用getClass()
方法。请注意我们会使用==来判断Class类型的对象是否相等,因为同一种类型的所有对象的getClass()
方法一定能够返回相同的值。
你可以使用上面的实现作为实现任意数据类型的
equals()
方法的模板。必要实现一次equals()
方法,下一次就不会那么困难了。
内存管理
不可变性
契约式设计
- 异常(Exception),一般用于不受我们控制的不可预见的错误;
- 断言(Assertion),验证我们在代码中做出的一些假设。
异常与错误
断言
默认设置没有启动断言,可以再命令行下使用-enableassertions
标志(简写为-ea
)启用断言。