讲解了在随后的章节中用来实现、分析和比较算法的基本原则和方法。
颠倒数组元素的顺序
int N = a.length;
for(int i = 0; i < N/2; i++){
double temp = a[i];
a[i] = a[N-1-i];
a[N-i-1] = temp;
}
矩阵相乘(方阵,行列相等)
int N = a.length;
double[][] c = new double[N][N];
for(int i = 0;i<N;i++){
for(int j = 0;j<N;j++){
for(int k = 0;k<N;k++){
c[i][j] += a[i][k] * b[k][j]; //计算行i 和 列j 的点乘
}
}
}
判定一个数是否为素数
public static boolean isPrime(int N){
if(N < 2)
return false;
for(int i = 2;i*i<=N;i++){ // 重点
if(N%i==0)
return false;
}
return true;
}
本书自己定义了一些静态函数还有一些自己编写的库
P18 StdRandom 类(库),用于各种随机数的方法 P19 具体实现方法。
P22 StdOut 类,用于各种 输出一个数方法。
P24 StdIn 类,用于各种 输入一个数的方法。 P51 继承StdIn 和 StdOut 类的标准输入/输出流
P25 In 类,用于读入数组的方法,Out 类,用于输出数组的方法。P51 对两个类API的补充
P26,P27 StdDraw 类,用于画图(标准绘图库) P51 Draw类,继承StdDraw类
P39 Counter类(书里自己声明的计数器类) API (后面例子里会用到) P52实现
P46 Point2D类 平面上的点API
P47 Interval1D 类 直线上间隔的API,Interval2D 类 平面上间隔的API。
P48 商业应用程序API示例 Date类和Transaction类。 P56 Date类实现 P65 Date类 equals方法实现
P49 String类API(部分)
P57 Accumulator类 用于累加数据。P58 实现 P59 可视版本
P74 背包Bag 与 队列Queue API 分别在P95,P98 实现
P75 栈Stack API P94 实现
P110 Stopwatch计时器类 API,并且有实现方法。
数据抽象
数据类型指的是 一组值和一组对这些值的操作的集合。抽象数据类型(ADT)是一种能够对使用者隐藏数据表示的数据类型。主要不同之处在于它将数据和函数的实现关联,并将数据的表示方式隐藏起来。在使用抽象数据类型时,我们的注意力集中在数据本身并将实现对该数据的各种操作。
静态方法
静态方法的主要作用是实现函数,非静态(实例)方法的主要作用是实现数据类型的操作。静态方法调用的开头是类名(一般大写),而非静态方法调用的开头总是对象名(一般为小写)。
String不可变性
将一个String传递给一个方法时,不用担心该方法会改变字符串,因为String对象是不可变的(方法总是会返回一个新的String),但Java数组是可变的,因此方法可以自由改变数组的内容。可变的字符串:StringBuilder,可变的数组:vector。
孤儿
对于某个没有任何指针指向的对象,成为孤儿(注意是对象不是指针)。
Date a = new Date();
Date b = new Date();
a = b; // 此时a指向的对象已经没有任何指针指向它了,此时就是孤儿
算术表达式求值(双栈法)
引申有前缀表达式和后缀表达式,写在知乎上了。
1. 将操作数压入操作数栈。
2. 将运算符压入运算符栈。
3. 忽略左括号。
4. 在遇到右括号时,弹出一个运算符,弹出所需数量的操作数,并将运算符和操作数的运算结果压入操作数栈。
在处理完最后一个右括号之后,操作数栈上只会有一个值,它就是表达式的值。
public class Evaluate {
public static void main(String[] args) {
Stack<String> ops = new Stack<String>(); //符号栈
Stack<Double> vals = new Stack<Double>(); //数字栈
Scanner a = new Scanner(System.in);
String L = "";
while(a.hasNextLine()){ //重要!无线循环输入的写法
L = a.nextLine(); //每次读取一行
if( null == L || L.equals("")){ //输入不正确,退出
break;
}
if(L.equalsIgnoreCase("q")) //主动退出
{
break;
}
String[] sList = L.split(" "); // 按空格分割
for(String s:sList){
if(s.equals("(")){
//这一步绝对不能省略,因为后面有else
}else if(s.equals("+")){
ops.push(s);
}else if(s.equals("-")){
ops.push(s);
}else if(s.equals("*")){
ops.push(s);
}else if(s.equals("/")){
ops.push(s);
}else if(s.equals("sqrt")){
ops.push(s);
}else if(s.equals(")")){ //如果是右括号,弹出运算符和操作数,计算入栈
String op = ops.pop();
double v = vals.pop();
if(op.equals("+")){
v = vals.pop() + v;
}else if(op.equals("-")){
v = vals.pop() - v;
}else if(op.equals("*")){
v = vals.pop() - v;
}else if(op.equals("/")){
v = vals.pop() - v;
}else if(op.equals("sqrt")){
v = Math.sqrt(v);
}
vals.push(v);
}
else vals.push(Double.parseDouble(s));
}
System.out.println(vals.pop()); //输出最终数
}
}
}
1. Java中无限输入的写法:Scanner类的 hasNextLine 函数。
while(a.hasNextLine()){}
2. 读取一行的写法(包括空格):
L = a.nextLine();
3. 注意该算法有不足:输入的式子必须用空格隔开,并且必须要有右括号,比如 1 + 1 这个式子就不能计算了(不过最后可以用empty函数判断符号栈是否为空,这里就不特别写出)
4. P81 算法轨迹图。
泛型栈
普通的栈是定容栈,缺点是只能处理某一种数据类型。如果可以实现处理全部数据类型,那么就用到了泛型。
public class FixedCapacityStack<Item>
Item 是一个类型参数,用于表示用例将会使用的某种具体类型的象征性的占位符。用例只要在创建栈时提供具体的数据类型,它就能用栈处理任意数据类型。实际的类型必须是引用类型,但用例可以依靠自动装箱把原始数据类型转换为相应的封装类型。
创建一个泛型数组
Item[] a = new Item[MaxSize]; //错误的,因为创建泛型数组在Java中是不允许的
Item[] a = (Item[]) new Object[MaxSize]; //正确写法,但Java编译器会警告,忽略即可
创建一个字符串栈数组
Stack<String>[] a = (Stack<String>[]) new Stack[N];
resize方法
如果要更改栈的容量,则需要用到resize方法。
private void resize(int max){
Item[] item = (Item[]) new Object[max]; // 创建一个新数组
for(int i = 0;i < N; i++){
temp[i] = a[i]; //复制数组
}
a = temp; //更改指针指向
}
改进Push与Pop方法
每次Push元素时,都会检查一遍数组大小。
public void push(Item item){
if(N == a.length)
resize(2*a.length); //检查是否栈满,如果栈满则容量乘2
a[N++] = item;
}
每次Pop元素时,如果N太小,降低栈容量。
public Item pop(){
Item item = a[--N];
a[N] = null; //避免对象游离(因为解除指用之后a[N]已经是孤儿了,要赋为null)
if(N>0 ** N == a.length/4)
resize(a.length/2);
return item;
}
这样,栈永远不会溢出,使用率也永远不会低于四分之一。
注:P94 P96 分别给出了 栈和队列的 链表实现,这里建议跟着博客(也就是C语言)的方法来,通过移动尾指针的方法实现,书里给的方法太绕了,书中的思想是先定义一个指针指向旧尾结点,然后用原先的尾结点指针创立一个新结点,其实完全没必要。
P88,89 给出了顺序下压栈的实现算法。
迭代器
两个方法:hasNext返回一个布尔值,next返回一个泛型元素。
public static void main(String[] args) {
Stack<String> a = new Stack<String>(); //一个栈
Iterator<String> i = a.iterator(); //这个栈的迭代器
while(i.hasNext()){
String s = i.next();
System.out.println(s);
}
}
自定义迭代器
自定义迭代器由两部分组成:构造内部类 和 获取方法,构造内部类实现了Iterator接口,获取方法返回一个构造内部类。
private class it implements Iterator<String>{
//构造内部类,定义一个内部类,实现Iterator接口
@Override
public boolean hasNext() {
return i > 0; // hasNext方法可以自主书写
}
@Override
public String next() {
return a[--i]; // next方法也可以自主书写
}
}
private保证方法和实例变量的访问范围限制在包含它的类中。私有嵌套类的一个特点是只有包含它的类能够直接访问它的实例变量。
public Iterator<String> iterator(){ // 获取方法,返回一个构造内部类
return new it();
}
public static void main(String[] args) {
Test a = new Test();
Iterator<String> it = a.iterator(); //调用Test类中的自定义构造器方法
}
注意这种方法只能用于自定义的栈,如果是调用Java的stack类,那么还是要用stack类的迭代器。
Stack<String> a = new Stack<String>();
Iterator<String> b = a.iterator();
最好不要使用内置的栈库,因为它新增了几个一般不属于栈的方法,例如获取第i个元素。它还允许从栈底添加元素。
背包
P98 背包的实现算法
实际上 背包 = 链栈 + 自定义构造器,背包里有栈的push方法(名字叫 add),并且背包类里有自己定义的获取背包迭代器的方法(内部构造类和获取方法)。
算法分析
常见函数:P116 近似函数:P116 增长数量级的分类:P117
2-sum快速算法
找出一个输入文件中所有和为0的整数对的数量。穷举法的时间复杂度是 O(N^2)
public static int count(int[] a){
Arrays.sort(a); // 给a数组从小到大排序
int n = a.length;
int cnt = 0;
for(int i = 0;i<n;i++){
if(BinarySearch(-a[i],a)>i) //如果-a[i]的位置大于i,说明存在并且还未计数
cnt++;
}
return cnt; //for循环结束后,返回cnt
}
使用二分查找法对 -a[i] 进行查找,总共有三种情况:如果二分查找不成功则返回-1,因此不会增加计数器的值。如果 j > i,说明有 a[j] + a[i] = 0,把并且j没有计数,此时计数器加一。如果 j < i,虽然也满足条件,但是j肯定已经统计过了,因此计数器不变。
此时算法复杂度为 O(NlogN),因为二分查找的时间复杂度为O(logN)
3-sum快速算法
与 2-sum 思路相似,要进行双重循环,当且仅当(-a[i] + a[j]) 在数组中,存在和为0的三元组。
public static int count(int[] a){
Arrays.sort(a);
int n = a.length;
int cnt = 0;
for(int i = 0;i<n;i++){
for(int j = i+1;j<n;j++){ //增加一层循环
if(BinarySearch(-a[i]-a[j],a) > j) //当需要的元素位置大于j才是未计数的
cnt++;
}
}
return cnt;
}
需要注意的是需要的元素位置大于 j 才是有效的(因为j前面的元素都遍历过了)。算法复杂度为O(N^2logN)
1.4.6 —— 1.4.8 再看一遍。
内存消耗
对象
Integer:24字节——16字节的对象开销,4字节用于保存int值以及4个填充字节。
Date:16字节的对象开销,3个int实例变量共计12字节,以及4个填充字节。
对象的引用:一般都是一个内存地址,使用8字节。注意对象里的String也是引用。
数组:一个原始数据类型的数组一般需要24字节的头信息(16字节的对象开销,4字节用于保存长度以及4个填充字节)
对象数组:比如一个含有N个Date类对象的数组—— 24字节(数组开销) + 8N字节(所有引用) + 每个对象的32字节共计32N,总共是 (24 + 40N)字节。
二维数组:是一个数组的数组(每个数组都是一个对象)。例如一个 M×N的double类型的二维数组需要使用 24字节(数组的数组的开销) + 8M字节(所有数组的引用) + 24M字节(M个数组的开销) + 8MN字节(M个长度位N的double类型数组里的元素)
P128 int值,double值,对象和数组的数组对内存的需求 关系图。
字符串对象(String):8字节(指向字符数组的引用)+ 12字节(三个int值)。第一个int描述的是字符数组中的偏移量,第二个int值是一个计数器(字符串的长度),第三个int值是一个散列值,在某些情况下可以节省一些计算,现在可以忽略。 + 16字节(表示对象)+ 4字节(填充字节) 共计40字节。 所有字符所需的内存需要另计,因为String的char数组(在常量池)常常是在多个字符串之间共享的。因为String对象是不可变的。
字符串与子字符串:一个长度为N的String一般使用40字节 + (24+2N)字节(字符数组),总共(64 + 2N)字节。但是如果使用 sustring 方法创建一个子字符串时,由于它使用的还是父字符串的数组,因此没有 24+2N。换言之,子字符串所需的内存是一个常数(40),构造一个子字符串所需的时间也是一个常数。
P129 1.4.10 一个String对象和一个子字符串
栈:当程序调用一个方法时,系统会从内存中的一个特定区域为方法分配所需的内存(用于保存局部变量),这个区域叫做栈。当方法返回时,它所占用的内存也被返回给了系统栈。
堆:当通过new创建对象时,系统会从对内存的另一块特定区域为该对象分配所需的内存。所有对象都会一直存在,直到对它的引用消失为止。
动态连通性问题(union=find)
输入一列整数对,其中每个整数都表示某种类型的对象,一对整数p,q可以被理解为"p和q是相连的"。当且仅当两个对象相连时才属于同一个等价类。
当程序从输入中读取了整数对p,q时,如果已知的数据可以说明p和q是相连的,那么程序应该忽略p,q这对整数并继续处理输入中的下一对整数。
用于判断一对新对象是否是相连的,这种问题为“动态连通性问题”。
将对象称为触点,将整数对称称为连接,将等价类称为连通分量或简称分量。假设用到的 0-N-1个整数表示N个触点。
P138 union-find算法的API。
用触点为索引的数组 id[] 作为基本数据结构表示所有的分量。一开始我们有N个分量,每个触点都构成了触点i,因此将 id[i] 的值初始化为 i。
对于每个触点i,我们用find()方法判定它所在的分量所需的信息保存在id[i]中。
UF类的实现
public class UF {
private int[] id; //分量id(以触点作为索引)
private int count; //分量数量
public UF(int N) { //构造器
this.count = N;
id = new int[N];
for(int i =0;i<N;i++){
id[i] = i; //初始化分量id数组
}
}
public int count(){
return count;
}
public boolean connected(int p,int q){
return find(p) == find(q);
}
public int find(int p){...} //下面实现
public void union(int p,int q){...} //下面实现
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt(); //读取触点数量
UF uf = new UF(N); //初始化N个分量
while(scanner.hasNext()){
int p = scanner.nextInt();
int q = scanner.nextInt(); //读取整数对
if(uf.connected(p,q))
continue; //如果已经连接那忽略
uf.union(p,q); //如果没有连接就连接,然后输出整数对
System.out.println(p + " " + q);
}
System.out.println(uf.count); //输出分量数量
}
}
find与union实现
quick-find算法
find思路:当且仅当 id[p]等于id[q]时,p和q是连通的。在同一个连通分量中,id[]的值必须完全相同(因为这样才叫连在一起嘛)。
union思路:如果是在一个连通分量,那就不采取行动。如果不在一个连通分量,那就遍历id数组,将q所在的分量的 id 全部改成 p的id(当然也可以反过来),这样就合二为一了。
public int find(int p){
return id[p]; //返回p对应的id值
}
public void union(int p,int q){
int pID = find(p);
int qID = find(q); //找到p和q对应的id值
if(pID == qID){
return; //如果相等,说明连接,直接返回
}
for(int i=0;i<id.length;i++){ //如果不相等,遍历整个id,把id等于pID的全部改为qID
if(id[i]==pID)
id[i] = qID;
}
count--; //分量减一
}
但这种方法一般无法处理大型问题,因为对于每一对输入union()都需要扫描整个id数组。每次find()调用只需要访问一次,但union操作次数在(N+3)到(2N+1)之间。
quick-union算法
P141详细解释 主要就是利用链接(并查集)
public int find(int p){
while(p!=id[p]){
p = id[p]; //直到找到p的根节点
}
return p; //返回根节点
}
public void union(int p,int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){ //如果根节点相同,那么就是连接的
return ;
}
id[pRoot] = id[qRoot]; //不相同,令p的根节点指向q的根节点(不再指向自己,等于合并)
count--; //分量减一
}
P142 图1.5.4 quick-union算法 概述图
id[] 数组用父链接的形式表示了一片森林。 P143 quick-union算法轨迹(以及森林表示)
加权quick-union算法
public class UF {
private int[] id; //父链接数组(以触点作为索引)
private int[] sz; //各个根节点所对应的分量的大小(具体说就是几个节点)
private int count; //分量数量
public UF(int N) { //构造器
this.count = N;
id = new int[N];
sz = new int[N];
for(int i =0;i<N;i++){
id[i] = i; //初始化分量id数组
}
for(int i =0;i<N;i++){
sz[i] = 1; //初始化分量大小(刚开始都是一个)
}
}
public int count(){
return count;
}
public boolean connected(int p,int q){
return find(p) == find(q);
}
public int find(int p){
while(p!=id[p])
p = id[p]; //顺着链接找到根结点
return p; //返回根结点
}
public void union(int p,int q){
int i = find(p);
int j = find(q);
if(i==j)
return; //链接,返回空
//接下来才是重点,不链接的情况
if(sz[i]<sz[j]){ //如果根结点分量较小(小树)
id[i] = j; //加入到大树中
sz[j] += sz[i]; //大树根结点分量增加小树根结点分量
}else{
id[j] = i;
sz[i] += sz[j];
} //相反情况
count--;
}
}
P146 图1.5.8 加权quick-union 算法的轨迹(森林) P147算法性能
cell-probe模型
是一种计算模型,其中只会记录对随机内存的访问,内存大小足以保存所有输入且假设其他操作均没有成本。