算法(Algorithms)
第1章 基础
- break语句,立即从循环中跳出;
- continue语句,立即开始下一轮循环;
在Java中创建一个数组需要三步:
- 创建数组的名字和类型;
- 创建数组;
- 初始化数组元素;
#完整模式
double[] a;
a=new double[N];
for( int i=0;i<N;i++)
a[i]=0.0;
#简化写法
double[] a=new double[N];
#声明初始化
int[] a={1,1,2,3,5,8}
简化写法中,数组中每个元素的值为0.0,这是数组创建后的默认值
起别名
a = new int[N];
a[i] = 1234;
int[] b=a;
b[i] = 5678; //a[i]的值也会变为5678
起别名有可能会出现一些难以察觉的问题,所以最好将需要的数组重新复制一份。
二维数组初始化时需要借用嵌套的for循环,其第一维是行数,第二维是列数。
静态方法
-
静态方法是使用static关键字修饰的方法,属于类的,不属于对象;非静态方法是不使用static关键字修饰的普通方法,属于对象,不属于类。
-
静态方法可以直接调用,类名调用和对象调用;非静态方法只能通过对象调用。
-
生命周期不同。
静态方法库是定义在一个Java类中的一组静态方法。类的申明是public class加上类名,以及用花括号包含的静态方法。存放类的文件名和类名相同,扩展名是.java。Java开发的基本模式就是编写一个静态方法库来完成一个任务。
递归
编写递归代码时最重要的就是以下三点:
- 递归总有一个最简单的情况——方法的第一条语句总是一个包含return的条件语句。
- 递归调用总是去尝试解决一个规模更小的子问题,这样递归才能收敛到最简单的情况。在下面的代码中,第四个参数和第三个参数的差值一直在缩小。
- 递归调用的父问题和尝试解决的子问题之间不应该有交集。在下面的代码中,两个子问题各自操作的数组部分是不同的。
public static int rank(int key, int[] a)
{
return rank(key,a,0,a.length-1);
}
public static int rank(int key, int[] a, int lo, int hi){
//如果key的值存在于a[]中,他的索引不会小于lo且不会大于hi
if(lo>hi) return -1;
int mid=lo+(hi-lo)/2;
if (key<a[mid]) return rank(key,a ,lo,mid-1);
else if (key>a[mid]) return rank(key,a,mid+1,hi);
else return mid;
}
模块化编程
好处:
- 程序整体的代码量很大时,每次处理的模块大小仍然适中;
- 可以共享和重用代码而无需重新实现;
- 很容易用改进的实现替换老的实现;
- 可以为解决编程问题建立合适的抽象模型;
- 缩小调试范围;
将对象作为返回值
方法可以将他们的参数对象返回,也可以创建一个对象并返回他的引用。这种能力非常重要,应为Java中的方法只能有一个返回值,有了对象我们的代码实际上就能返回多个值。
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");
}
}
######################
%java FlipsMax 1000000
500635 heads wins
其中的部分方法是自己定义,并未展示,全篇皆是类似,需要可以私信我
背包、队列和栈
背包
背包是一种不支持从中删除元素的集合数据类型——它的目的就是帮助用例收集元素并迭代遍历所有手机到的元素。迭代的顺序不确定且与用例无关。
背包的典型用例:
public class Stats {
public static void main(String[] args) {
Bag<Double> numbers = new Bag<Double>();
while (!StdIn.isEmpty())
numbers.add(StdIn.readDouble());
int N = numbers.size();
double sum = 0.0;
for (double x : numbers)
sum += x;
double mean = sum / N;
sum = 0.0;
for (double x : numbers)
sum += (x - mean) * (x - mean);
double std = Math.sqrt(sum / (N - 1));
StdOut.printf("Mean: %.2f\n", mean);
StdOut.printf("Std dev: %.2f\n", std);
}
}
算术表达式求值
根据以下四种情况从左到右逐个将这些实体送入栈处理:
- 将操作数压入操作数栈;
- 将运算符压入运算符栈;
- 忽略左括号;
- 在遇到右括号时,弹出一个运算符,弹出所需数量的操作数,并将运算符和操作数的运算结果压入操作数栈。
Dijkstra的双栈算法表达式求值算法:
public class Evaluate {
public static void main(String[] args){
Stack<String> ops= new Stack<String>();
Stack<Double> vals=new Stack<Double>();
while(!StdIn.isEmpty()){
//读取字符,如果是运算符则压入栈中
String s =StdIn.readString();
if (s.equals("("));
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));
}
StdOut.println(vals.pop());
}
}
案例研究:union-find算法
问题的输入是一列整数对,其中每个整数都表示一个某种类型的对象,一对整数p,q可以被理解为“p和q是相连的”。我们假设相连是一种等价关系。
我们的目标是编写一个程序来过滤掉序列中所有无意义的整数对。换句话说,当程序从输入中读取了整数对p q时,如果已知的所有整数对都不能说明p和q是相连的,那么则将这一对整数写入到输出中。如果已知的数据可以说明p和q是相连的,那么程序应该忽略p q这对整数并继续处理输入中的下一对整数。(找出对连通性有影响的整数对,将其输出到屏幕上,整数对前后出现的顺序会对结果产生影响)
我们将这个问题通俗的叫做动态连通性问题。
union-find算法框架:
public class UF {
private int[] id; //分量ID(以触点作为索引
private int count; //分量数量
public UF(int N){
//初始化分量id数组
count=N;
id =new int[N];
for(int i=0;i<N;i++){
id[i]=i;
}
}
public int count()
{
return count;
}
public boolean connected(int p,int q)
{
return find(p)==find(q);
}
public int find(int p)
{
return p;
}
public void union(int p,int q) //这两个函数是UF算法的主题,将在后面讨论实现
{
}
public static void main(String[] args)
{
//解决由StdIn得到的动态连通性问题
int N=StdIn.readInt(); //读取触点数量
UF uf=new UF(N);
while(!StdIn.isEmpty())
{
int p=StdIn.readInt();
int q=StdIn.readInt(); //读取整数对
if(uf.connected(p,q)) continue; //如果已经连通则忽略
uf.union(p,q); //归并分量
StdOut.println(p+" "+q); //打印连接
}
StdOut.println(uf.count()+"components");
}
}
实现
我们将讨论三种不同的实现,他们均是根据以触点为索引的**id[]**数组来确定两个触点是否存在于相同的连通分量中。
quck-find算法
一种方法保证当且仅当id[p]等于id[q]时p和q是连通的。换句话说,在同一个连通分量中的所有触点在id[]中的值必须全部相同。这意味着connected(p,q)只需要判断id[p]==id[q],当且仅当p和q在同一连通分量中该语句才会返回true。调用union(p,q),如果是处于同一分量,不需要采取任何行动,否则将p有关的id[]值改为id[p]。
public int find(int p) { return id[p]; } public void union(int p, int q) //这两个函数是UF算法的主题,将在后面讨论实现 { //将p和q归并到相同的分量中 int pID = find(p); int qID = find(q); //如果p和q已经在相同的分量中则不需要采取任何行动 if (pID == qID) return; //将p的分量重命名为q的名称 for (int i = 0; i < id.length; i++) { if (id[i] == pID) id[i] = qID; } count--; }
算法分析
find()操作的速度很快,因为它只需要访问**id[]数组一次。但quick-find算法一般无法处理大型问题,因为对于每一对输入union()都需要扫描整个id[]**数组。如果算法最后只得到了一个连通分量,那么至少需要调用N-1次union(),即至少(N+3)(N-1)~ N 2 N^2 N2次数组访问。需要寻找更好的算法。