今天跟着左程云算法课019和020,学习了【算法笔试中输入输出的处理】和【递归及master公式】。两节课都是偏重理论,简单总结一下。
一、算法笔试中的输入输出
(1)填函数风格
填函数风格是最简单的,之前几节算法课上使用的力扣测试链接就是填函数风格回答。在后台运行的时候一般是A.B.func(a,b)这样的形式,A和B的类名以及func的函数名不可以改变。这种风格不用管测试数据和输入和输出问题,只需要把功能函数写出来就行。
(2)acm风格
acm相比于填函数风格更加复杂一些,需要自己在main函数中处理输入和输出数据。
那么关于输入输出数据的处理,输入数据一般使用BufferedReader(而不使用Scanner),原因在于Scanner会频繁的进行文件IO,涉及到磁盘与内存和CPU之间的IO,速度会比较慢;用BufferedReader的话是开辟一块内存一次性把文件给读进来,这样涉及文件IO的次数会减少很多,速度会更快一些。输出数据也是同样的道理,使用PrintWriter而不使用System.out。具体的代码如下(求矩阵的最大子集和,解题代码不贴,只贴main中输入输出数据的处理):
public static void main(String[] args) throws IOException {
// 把文件里的内容,load进来,保存在内存里,很高效,很经济,托管的很好
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 一个一个读数字
StreamTokenizer in = new StreamTokenizer(br);
// 提交答案的时候用的,也是一个内存托管区
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) { // 文件没有结束就继续
// n,二维数组的行
int n = (int) in.nval;
in.nextToken();
// m,二维数组的列
int m = (int) in.nval;
// 装数字的矩阵,临时动态生成
int[][] mat = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
in.nextToken();
mat[i][j] = (int) in.nval;
}
}
out.println(maxSumSubmatrix(mat, n, m));
}
out.flush();
br.close();
out.close();
}
(3)不推荐使用临时空间,推荐使用全局静态空间
acm在计算空间的时候,不会管你前面开创的临时数组空间是否已经销毁,只要开辟了就算。假如在一个while循环里开辟临时数组arr[m][n],这个循环跑N次,那么acm计算的空间使用量就是N*m*n;所以在最前头开辟一个静态数组空间,可以反复利用。代码如下:
// 题目给定的行的最大数据量
public static int MAXN = 201;
// 题目给定的列的最大数据量
public static int MAXM = 201;
// 申请这么大的矩阵空间,一定够用了
// 静态的空间,不停复用
public static int[][] mat = new int[MAXN][MAXM];
// 需要的所有辅助空间也提前生成
// 静态的空间,不停复用
public static int[] arr = new int[MAXM];
// 当前测试数据行的数量是n
// 当前测试数据列的数量是m
// 这两个变量可以把代码运行的边界规定下来
public static int n, m;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
n = (int) in.nval;
in.nextToken();
m = (int) in.nval;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
in.nextToken();
mat[i][j] = (int) in.nval;
}
}
out.println(maxSumSubmatrix());
}
out.flush();
br.close();
out.close();
}
// 求子矩阵的最大累加和,后面的课会讲
public static int maxSumSubmatrix() {
int max = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
// 因为之前的过程可能用过辅助数组
// 为了让之前结果不干扰到这次运行,需要自己清空辅助数组需要用到的部分
//arr数组0到m-1的位置填充0
Arrays.fill(arr, 0, m, 0);
for (int j = i; j < n; j++) {
for (int k = 0; k < m; k++) {
arr[k] += mat[j][k];
}
max = Math.max(max, maxSumSubarray());
}
}
return max;
}
(4)按行读取
在(2)中的StreamTokenizer是一个数一个数的读,如果碰到需要一行一行读的数据(比如要计算每一行数的和,但是每一行数据的个数并不一致),StramTokenizer并不能识别/n,那就需要一行一行的读出来然后按空格split。代码如下:
public static void main(String[] args) throws IOException {
BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
PrintWriter out=new PrintWriter(new OutputStreamWriter(System.out));
line=in.readLine();
while(line!=null) {
parts=line.split(" ");
sum=0;
for(String num:parts) {
sum=sum+Integer.valueOf(num);
}
out.println(sum);
line=in.readLine();
}
out.flush();
in.close();
out.close();
}
二、递归和master公式
(1)理解递归
从思想上理解递归,想不明白的时候画递归调用图是非常重要的,有利于分析递归。
从底层上理解递归,递归是通过系统栈来实现的;比如说f(0,3)要分解成f(0,1)和f(2,3),要进入f(0,1),先把f(0,3)压入系统栈中,f(0,1)运行结束返回f(0,3)时,再从系统栈中把f(0,3)出栈恢复重建现场。
任何递归都可以改成非递归,可以自己创造一个栈,模仿系统栈的入栈出栈过程。自己创造栈的话可能会更加优化一些,因为系统栈是把所用的信息一起压入栈中,而自己用内存空间来实现递归的话,可能入栈出栈的是一些关键信息,可以比系统栈更加简单快速一些。
(2)递归改成非递归的必要性
首先工程中是一定要改的,除非数据量再大递归层数也一定不深,比如归并排序,快速排序,线段树和平衡树等。举个例子,归并排序就是二分之后使左边有序,右边有序,然后整合一下左右(整合这个动作是非递归的)。其递归的层数为O(log2 N),也就是说假如有2^64个数据归并排序的话,递归的层数也就是64层。
其次就是算法比赛和面试中,能通过就不用改,除非是时间复杂度不行通不过再改。
(3)master公式
master公式用来确定递归的时间复杂度,看公式可以看出来,master公式只能用于估计子问题等规模的时候的时间复杂度。具体总结如下。