1. 知识点梳理
本节主要是讲了一些java的基本语法。其中值得注意的几点有:
运算符的重载P6,数组实现矩阵乘法P12,对数组深拷贝和浅拷贝的介绍(起别名)P12,sqrt方法求平方根使用的牛顿迭代法P13,方法的参数按值传递P14,递归的注意事项P14,格式化输出P22,linux系统的重定向与管道命令P24。
答疑部分值得注意的有:
- 负数的除法和余数的结果是什么?
表达式a/b结果向0取整,a%b的定义是(a/b)*b+a%b恒等于a。例如-14/3和14/-3结果都为-4,而-14%3=-2,14/-3=2 - for循环和while循环有什么区别?
for循环头部的代码和for循环的主体代码在同一个代码段中。通常递增变量一般在循环结束后都是不可用的(循环变量在for循环内定义),但在与之等价的while循环中,递增变量在循环结束后还是可用的(循环变量在while循环外定义)。同时如果while语句中有continue关键词,在满足条件跳转后不会自动执行自增操作。(除非自增操作写在continue语句执行前)
这里牵扯到java的变量作用域问题(这里的变量特指在方法内定义的局部变量)
举一下几个例子:
第一段:
{
int x = 12;
/* only x available */
{
int q = 96;
/* both x & q available */
}
/* only x available */
/* q “out of scope” */
}
第二段:
{
int x = 12;
{
int x = 96; /* illegal */
}
}
注意第二段代码,Java编译器会认为变量已被定义,所以作用域中的变量不能重复定义,但是在C和C++中能将一个变量“隐藏”在一个更大的作用域里,在C和C++中被允许,在Java中是不允许的,因为Java的设计者认为这样做使程序产生了混淆。
再看如下两种情况
第一段代码,name变量离开作用域,变量所分配的内存空间将被JVM回收,所以新的name变量语法不会有错误,而第二种写法name并没有离开{}作用域,所以会语法错误。
大家仔细的观察并结合代码思考,可以得出变量的作用域结论如下:
在同一作用域范围的包裹下成员变量名和局部变量名是可以变量名相同的,在同一个作用域范围的包裹下局部变量和局部变量不可以变量名相同(作用域内不能重复命名),在方法中使用变量的时候如果不指明使用成员变量还是局部变量,那么默认的就是使用局部的那个变量,但是如果局部变量超出了它本身的作用域范围则会失效,被JVM垃圾回收,那么则可以重复命名此变量,并使用最新定义的这个局部变量。
由以上问题引入一个重要的概念:GC垃圾收集器
Java对象不具备与引用一样的存在时间。用new关键字创建一个Java对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:
{
String s = new String("a string");
} /* 作用域的终点 */
那么句柄s,也就是引用会在作用域的终点处消失。然而,s指向的String对象依然占据着内存空间。在上面这段代码里,我们没有办法继续使用这个对象,因为指向它的唯一一个句柄已经超出了作用域的边界。
这样造成的结果是:对于用new创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和C++里特别突出。在C++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它们是否可用。而且最麻烦的是,在C++里,一旦完成工作,必须保证将对象手动清除。
这样便带来了一个有趣的问题。假如 Java 让对象依然故我,怎样才能防止它们大量充斥内存,并最终造成程序的“凝固”呢。在 C++里,这个问题最令程序员头痛。但 Java 以后,情况却发生了改观。 Java 有一个特别的“垃圾收集器”,它会查找用 new 创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根本不必操心内存的回收问题。只需简单地创建对象,一旦不再需要它们,它们就会自动离去。这样做可防止在 C++里很常见的一个编程问题:由于程序员忘记释放内存造成的“内存溢出”。
- 如果a[]是一个数组,为什么System.out.println(a)打印出一个符号加十六进制整数,比如[I@b81eda8?
相当于自动调用了数组类的toString方法,输出的 [ 代表class对象是一个数组,I指int类型(还有别的类型用别的字母)@后面指数组对象的地址值(十六进制表示)。 - java中是否可以用一个静态方法作为另一个静态方法的参数(如果返回值匹配)?
不可以,但有些语言允许。
1.1节习题
因为很多都是简单的语法题目,这里只挑选一些有意义的题目。(部分答案源自网络,只添加了自己认为必要的注释)
1.1.3 编写一个程序,从命令行得到三个整数参数。如果它们都相等则打印 equal,否则打印 not equal。
public static void main(String[] args) {
System.out.println("请输入三个整数");
Integer number1 = Integer.valueOf(args[0]);
Integer number2 = Integer.valueOf(args[1]);
Integer number3 = Integer.valueOf(args[2]);
// 注意这里不能直接使用==(比较的是integer对象是否同一个)也不能用valueOf
if(number1.intValue() == number2.intValue() && number2.intValue() == number3.intValue()) {
System.out.println("equal");
} else {
System.out.println("not equal");
}
}
下面看一下valueOf的源码
public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
public static int parseInt(String s, int radix)
throws NumberFormatException
{
/*
* WARNING: This method may be invoked early during VM initialization
* before IntegerCache is initialized. Care must be taken to not use
* the valueOf method.
*/
// 第一步、判断字符串参数是否为null
if (s == null) {
throw new NumberFormatException("null");
}
// 第二步,判断基数是否小于最小基数 为2
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix +
" less than Character.MIN_RADIX");
}
// 第三步,判断基数是否大于最大基数 为36
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException("radix " + radix +
" greater than Character.MAX_RADIX");
}
int result = 0;
// 标识,是否为负数,默认false
boolean negative = false;
// 字符串转换为char数组后的 下标和数组长度
int i = 0, len = s.length();
int limit = -Integer.MAX_VALUE;
int multmin;
int digit;
// 第四步,判断字符串长度是不大于0
if (len > 0) {
// 取第一个字符
char firstChar = s.charAt(0);
// 字符ASCII是否小于'0' ,可能为 '+' '-' , 如果不是<'0' ,则为'0'之后的字符
// 也就是有效数字 ,跳过第四步判断正负号的处理
if (firstChar < '0') { // Possible leading "+" or "-"
// 如果第一个字符是'-' ,说明是负数,则负数标识改为true ,限制改为最小标识
if (firstChar == '-') {
negative = true;
limit = Integer.MIN_VALUE;
} else if (firstChar != '+')
// 如果第一个字符不是'-' 也不是'+' ,异常
throw NumberFormatException.forInputString(s);
// 第一字符<'0' 且长度为1 则不是数字 异常
if (len == 1) // Cannot have lone "+" or "-"
throw NumberFormatException.forInputString(s);
i++;
}
multmin = limit / radix;
// 遍历字符串转为的字符数组,将每一个字符转为10进制值,并拼接
while (i < len) {
// Accumulating negatively avoids surprises near MAX_VALUE
digit = Character.digit(s.charAt(i++),radix);
if (digit < 0) {
throw NumberFormatException.forInputString(s);
}
if (result < multmin) {
throw NumberFormatException.forInputString(s);
}
result *= radix;
if (result < limit + digit) {
throw NumberFormatException.forInputString(s);
}
result -= digit;
}
} else {
throw NumberFormatException.forInputString(s);
}
// 返回拼接数据
return negative ? result : -result;
}
综上,该方法源码的执行流程:
1、parseInt(String s)--内部调用parseInt(s,10)(默认为10进制)
2、判断字符串参数是否不为null,否则异常
3、判断基数是否在最小基数和最大基数之间,否则异常
4、判断字符串长度是否>0
5、判断第一个字符是否是符号位,是的话判断+-符号,不是的话则第一位不是字符,直接下一步遍历每一个字符
6、循环遍历确定每个字符的十进制值
7、通过*= 和-= 进行计算拼接
8、判断是否为负值 返回结果
对于Character.digit方法,我们放到Character的源码里剖析,这里仅仅知道其返回指定基数中指定字符(Unicode代码点)的数值。 更详细来说:
如果基数不在范围内,即不满足MIN_RADIX≤基数≤MAX_RADIX;或该值不是一个有效的数字则返回-1。当满足以上两个条件后,分为以下情况:
方法isDigit为true的字符和Unicode字符的十进制数值(或分解的单字符)小于指定的基数。在这种情况下的十进制数字值被返回。
该字符是一个大写拉丁字母'A'到'Z'之间。在这种情况下,ch - 'A'+10返回。
字符的小写拉丁字母'a'到'z'之间。在这种情况下,ch - 'a'+10返回。
字符是一个全角大写拉丁字母A('\ uFF21')到Z('\ uFF3A“)之间。在这种情况下,ch - '\ uFF21'+ 10返回。
该字符是一个小写拉丁字母的全角('\ uFF41')到Z('\ uFF5A“)之间。在这种情况下,ch - '\ uFF41'+10返回。
1.1.6 下面这段程序会打印出什么?
int f = 0;
int g = 1;
for (int i = 0; i <= 15; i++)
{
StdOut.println(f);
f = f + g;
g = f - g;
}
打印斐波那契数列。相当于让g,f两个数为指针,每次循环让新的f作为两个指针和,然后让新的g作为旧的f值
如下图(初始情况略微特殊):
1.1.9编写一段代码,将一个正整数 N 用二进制表示并转换为一个 String 类型的值 s。
解答:Java 有一个内置方法 Integer.toBinaryString(N) 专门完成这个任务,但该题的目的就是给出这个方法的其他实现方法。下面就是一个特别简洁的答案:
String s = "";
for (int n = N; n > 0; n /= 2)
s = (n % 2) + s;
本方法由于隐式创建了很多个StringBulider对象以及String对象,显然很耗内存,那么再看一下源码是怎么实现的:
public static String toBinaryString(int i) {
return toUnsignedString0(i, 1);
}
private static String toUnsignedString0(int val, int shift) {
// assert shift > 0 && shift <=5 : "Illegal shift value";
// 重点方法 1
// 这一步SIZE=32减去高位连续0的个数,结果可以简单理解为有效位的个数
int mag = Integer.SIZE - Integer.numberOfLeadingZeros(val);
// 这一步将mag/shift向上取整(2进制变shift进制的所需位数发生变化)与1比较取最大值
// 之所以使用MAX是防止mag+shift-1=0的情况
// 也就是说2^shift代表2,4,8,16,32进制(注意注释中的assert语句),当mag=0,shift=1时
// 即将int型val变量,此时val=0,转化为2进制时,至少需要一个字符来表示,MAX的意义就是
// 在这种特殊情况下保证了字符数组的长度为1
int chars = Math.max(((mag + (shift - 1)) / shift), 1);
char[] buf = new char[chars];
// 重点方法 2
// 将val转为shift进制表示的字符数组,用buf接收,起始位置为0,长度为chars
formatUnsignedInt(val, shift, buf, 0, chars);
// Use special constructor which takes over "buf".
return new String(buf, true);
}
// 重点方法 1 的具体内容
// 以下方法的作用是判断该int数转换为二进制(int数据存储为32位)之后的左侧连续0(包括符号位在内)的个数
// jvm中一个int类型的数据占4个字节,共32位,其实就相当于一个长度为32的数组。
// 那我们要计算首部0的个数,就是从左边第一个位开始累加0的个数,直到遇到一个非零值。
public static int numberOfLeadingZeros(int i) {
if (i == 0)
return 32;
// 下面的代码就是定位从左边开始第一个非零值的位置,在定位过程中顺便累加从左边开始0的个数
// 将i无符号右移16位后,有二种情况;
// 情况1:满足if语句判断条件,i=0,则第一个非零值位于低16位,i至少有16个0,同时将i左移16位
// 把低16位移到原高16位的位置,这样情况1和情况2就能统一后续的判断方式
// 情况2: 不满足if语句判断条件, i!=0,则第一个非零值位于高16位,后续在高16位中继续判断
// 这个思路就是二分查找,首先把32位的数分为高低16位,如果非零值位于高16位,
// 后续再将高16位继续二分为高低8位,一直二分到集合中只有1个元素
//
int n = 1;
if (i >>> 16 == 0) { n += 16; i <<= 16; }
if (i >>> 24 == 0) { n += 8; i <<= 8; }
if (i >>> 28 == 0) { n += 4; i <<= 4; }
if (i >>> 30 == 0) { n += 2; i <<= 2; }
n -= i >>> 31;
return n;
}
// 重点方法 2 的具体内容
// 参数解释:val:待转换的int值;常用shift:1代表二进制,3代表八进制,4代表十六进制
// buf:接收转换结果的char[] ;offset:存储在buf的起始位置;len:长度
static int formatUnsignedInt(int val, int shift, char[] buf, int offset, int len) {
int charPos = len;
int radix = 1 << shift;
int mask = radix - 1;
// 功能是把原来二进制表示的val用2^shift进制表示,具体实现是把val的二进制表示从低位到高位分别用digits数组中的元素表示
// shift为1时,相当于最低位&1,依旧是原最低位,digits索引只能取到0或1,也就是用char值'0','1'分别表示出二进制0,1;
// shift为3时,相当于用掩码取出最低三位,digits索引只能取到0至7,也就是用char值'0'...'7'分别表示出八进制数0~7;shift为4类似
// shift为5及以上,可能会出现元素无意义和数组索引越界问题,所以本方法使用包可见控制,使用反射调用可能会出现问题
// val的低位至高位存放在buf数组中,所以buf数组的索引应该是从尾部递减的,存放好shift位之后
// 让val无符号右移shift位,循环条件的终止情况应该是允许的长度<=0或者val=0
do {
buf[offset + --charPos] = Integer.digits[val & mask];
val >>>= shift;
} while (val != 0 && charPos > 0);
return charPos;
}
final static char[] digits = {
'0' , '1' , '2' , '3' , '4' , '5' ,
'6' , '7' , '8' , '9' , 'a' , 'b' ,
'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
'o' , 'p' , 'q' , 'r' , 's' , 't' ,
'u' , 'v' , 'w' , 'x' , 'y' , 'z'
};
其实注释已经比较详细了,但重点方法1使用了二分查找,比较有趣,以下是更完整的 描述:
本方法中涉及到的全部是移位运算,<<是带符号左移 >>>是无符号右移。如果让我来写,我不看这个源码的方法,我应该是会不断的右移位判断该数是否变为0来判断其有效位数,或者不断左移位与0x80000000(-2^31)做与运算判断结果的值。但是上面的方法效率显然不如源码:O(N)和O(log n )的区别。本方法通过无符号右移与0作比较的方法确定0的个数,而且是一次进行多位的判断,且分区进行判断 16 8 4 2这四个区域,而且不管if块内代码是否执行,最高有效位所在的区域都在缩小。下面只讨论第一个if块之前的代码:首先代码判断了i是否为0,如果i等于0直接返回32;用n来代表左侧0个数先默认n=1,第一个if块比较i无符号右移16位与0大小,如果相等证明n大于等于16,此时对i做左移16位的赋值操作(新的i的0的数目是原来的i的0的数目加16)。之后的if语句分别判断了第一个1出现的位置是否在高8位,高4位,高2位间,如果不在,那么就是0,0的数目n分别加上8,4,2。在最后剩余两位,也只需要讨论第一位的情况,这是因为最开始已经排除了所有位都为0的情况,即必存在至少一个1,如果第一位是1那么第二位就不用管了,而如果第一位是0那么第二位就必然是1。所以让已知的0的个数n(默认为1)减去1(i的第一位为1)或0(i的第一位为0)。
而重点方法2逻辑是比较清晰的,之所以用do...while是为了保证val=0时,把'0'存放进数组。
没想到一个如此简单的功能,源码能把效率提升到这么高,同时对代码复用做到如此地步。看来源码还是要坚持读下去的~~
1.1.13 编写一段代码,打印出一个 M 行 N 列的二维数组的转置(交换行和列)。
int[ ][ ] a={{1,2,3},{4,5,6}};
int[][] temp = new int[a[0].length][a.length];
for (int i = 0; i < a[0].length; i++) {
for (int j = 0; j < a.length; j++) {
temp[i][j] = a[j][i];
System.out.print(temp[i][j] + " ");
if(j == a.length - 1)
System.out.print("\n");
}
}
1.1.14 编写一个静态方法 lg(),接受一个整型参数 N,返回不大于 的最大整数。不要使用 Math 库。
private static int lg(int N) {
int shift = 1;
int n = 0;
while (N != 0){
n++;
N >>>= shift;
}
return n-1;
}
或者更简单的写成如下,但效率没有提升
return Integer.toBinaryString(N).length()-1;
其实就是把二进制表示的有效位数目-1
1.1.15 编写一个静态方法 histogram(),接受一个整型数组 a[] 和一个整数 M 为参数并返回一个大小为 M 的数组,其中第 i 个元素的值为整数 i 在参数数组中出现的次数。如果 a[] 中的值均在 0 到 M-1之间,返回数组中所有元素之和应该和 a.length 相等。
分析:仔细阅读题目,方法名histogram(直方图),这道题目其实就是让你去实现一个直方图。
让原来数组的每个元素值作为新数组的索引,索引是从0到M-1,遍历原数组,每个在0 ~M-1的元素都会让新数组索引为对应0 ~M-1的值加1。结果即为0 ~M-1的值在原数组各自出现的次数。
public static int[] histogram(int[] a,int M)
{
int[] h = new int[M];
int N = a.length;
for (int i = 0; i < N; i++)
if (a[i] < M)
h[a[i]]++;
return h;
}
1.1.18 请看以下递归函数,思考输出的为什么是这样的结果
public static int mystery(int a, int b) {
if (b == 0) return 0;
if (b % 2 == 0) return mystery(a+a, b/2);
return mystery(a+a, b/2) + a;
}
public static int mystery1(int a, int b) {
if (b == 0)
return 1;
if (b % 2 == 0)
return mystery1(a*a, b/2);
return mystery1(a*a, b/2) * a;
}
public static void main(String args[]) {
System.out.println(mystery(2,25)); // 输出50
System.out.println(mystery(3,11)); //输出33
System.out.println(mystery1(2,25)); // 输出33554432
System.out.println(mystery1(3,11)); //输出177147
}
对于可以理解为b的二进制某一位的值(0或1),当为0时,此项为0,当为1时此项为。在代码实现中a的值在每次递归调用时发生变化为最初始a值的倍,把这些递归的结果加起来,b=0时作为递归结束条件,所以相当于也就是的值。
同上面类似,对于可以理解为(类似上题把化为,变为指数时,由连加变连乘),每次递归调用时a的值变为初始值的
倍,而只有当为1时才会让递归的返回值乘以,递归的终止条件是b=0,返回为1。
1.1.20 编写一个递归的静态方法计算 ln(N!) 的值。
/*****************方法一************************/
// 这样计算显然每一步都会产生误差,误差会累积起来
public static double logarithmic1(int N) {
if (N == 0)
return 0;
if (N == 1)
return 0;
return (logarithmic1(N - 1) + Math.log(N));
}
/**********************方法二**************************/
// 这样计算只会在计算n!的对数值这一步有误差,但显然n!溢出的风险很高
public static long factorial(int M) {
if(0 == M || 1 == M)
return 1;
else
return M * factorial(M - 1);
}
public static double logarithmic2(int N) {
return Math.log(factorial(N));
}
1.1.21 编写一段程序,从标准输入按行读取数据,其中每行都包含一个名字和两个整数。然后用printf() 打印一张表格,每行的若干列数据包括名字、两个整数和第一个整数除以第二个整数 的结果,精确到小数点后三位。可以用这种程序将棒球球手的击球命中率或者学生的考试分数 制成表格。
public static void main(String[] args) {
//读取 M个数据
int M = 3;
int index = 0;
String[] strs = new String[M];
while(index < M)
strs[index++] = StdIn.readLine();
for(int i = 0; i < strs.length; ++i) {
// \\s指空白符 +指一个及多个
String[] arr = strs[i].split("\\s+");
double temp = Double.parseDouble(arr[1]) / Double.parseDouble(arr[2]);
StdOut.printf("%-10s %-10s %-10s %-13.3f\n", arr[0], arr[1], arr[2], temp);
}
}
1.1.22 使用1.1.6.4 节中的rank() 递归方法重新实现BinarySearch 并跟踪该方法的调用。每当该方法被调用时,打印出它的参数lo 和hi 并按照递归的深度缩进。提示:为递归方法添加一个参数来保存递归的深度。
public static int rank(int key, int[] arr, int low, int high, int depth) {
printCallInfo(low, high, depth);
if(low > high)
return -1;
// 本程序中已经严格排除了溢出的可能,但其他情况考虑溢出风险可以改动如下,详见位操作篇
// int mid=low&high+(low^high)>>1;
int mid = low + ((high - low) >> 1);
if(key < arr[mid])
return rank(key, arr, low, mid - 1, depth + 1);
else if(key > arr[mid])
return rank(key, arr, mid + 1, high, depth + 1);
else
return mid;
}
/**
* 二分查找 : 递归描述
* @param key
* @param arr
* @return
*/
public static int binarySearch(int key, int[] arr, int depth) {
return rank(key, arr, 0, arr.length - 1, depth);
}
/**
* 打印缩进
* @param indents 缩进数
*/
private static void printIndent(final int indents) {
for(int i = 0; i < indents; ++i)
StdOut.print("----------");
}
/**
* 打印调用信息
* @param low
* @param high
* @param depth
*/
private static void printCallInfo(int low, int high, int depth) {
StdOut.print(depth + "\t");
printIndent(depth);
StdOut.println(low + "\t" + high);
}
public static void main(String[] args) {
int N = 1024;
int[] arr = new int[N];
for(int i = 0; i < N; ++i)
arr[i] = StdRandom.uniform(N * 50);
// 排序
Arrays.sort(arr);
// 从随机数组中随机抽取一个元素作为关键字
// 输出随机数组
StdOut.print("seq = ");
for(int i = 0 ; i < N; ++i)
StdOut.print(arr[i] + "\t");
int key = arr[StdRandom.uniform(N)];
StdOut.println("\nkey = " + key);
StdOut.println("---------------------------------------------------------------------------------------");
binarySearch(key, arr, 0);
}
1.1.23 为BinarySearch 的测试用例添加一个参数:+ 打印出标准输入中不在白名单上的值;-,则打印出标准输入中在白名单上的值。
/**
* 二分查找 : 非递归描述
* @param key 关键字
* @param arr 数组
* @return 若找到则返回true,否则返回false
*/
public static boolean BinaryLookup(int key, int[] arr) {
int low = 0;
int high = arr.length - 1;
while(low <= high) {
int mid = low + ((high - low) >> 1);
if(key < arr[mid])
high = mid - 1;
else if(key > arr[mid])
low = mid + 1;
else
return true;
}
return false;
}
public static void main(String[] args) {
// “ + ” --> 打印出标准输入中不在白名单上的值,
// “ - ” --> 打印出标准输入中在白名单上的值
char symbol = ';
// 从tinyW里读取所有int值,存放在whitelist数组
int[] whitelist = new In(args[0]).readAllInts();
Arrays.sort(whitelist);
// 输入流是tinyT中的内容,对每个tinyT中的int值用二分法在白名单里搜索
while(!StdIn.isEmpty()) {
int key = StdIn.readInt();
boolean found = BinaryLookup(key, whitelist);
if('+' == symbol && !found)
StdOut.println(key);
if('-' == symbol && found)
StdOut.println(key);
}
}
在环境搭建上遇到一些麻烦,参考了这篇博客
https://blog.csdn.net/ys875038467/article/details/78916044
输入参数:java xxx tinyW.txt < tinyT.txt
暂时不了解如何能在控制台输入symbol的值(命令行参数不算。。)
1.1.24 给出使用欧几里德算法计算105 和24 的最大公约数的过程中得到的一系列p 和q 的值。扩展该算法中的代码得到一个程序Euclid,从命令行接受两个参数,计算它们的最大公约数并打印出每次调用递归方法时的两个参数。使用你的程序计算1 111 111 和1 234 567 的最大公约数。
/**
* 打印缩进
* @param indents 缩进数
*/
private static void printIndents(final int indents) {
for(int i = 0; i < indents; ++i)
StdOut.print("----------");
}
/**
* 打印调用信息
* @param p
* @param q
* @param depth
*/
private static void printCallInfo(int p, int q, int depth) {
StdOut.print(depth + "\t");
printIndents(depth);
StdOut.println(p + " " + q);
}
/**
* 使用2300多年前的欧几里得算法求解两数的最大公约数
* @param p 数一
* @param q 数二
* @return 最大公约数
*/
public static int Euclid(int p, int q, int depth) {
printCallInfo(p, q, depth);
if(q == 0)
return p;
int r = p % q;
return Euclid(q, r, depth+1);
}
public static void main(String[] args) {
int p = Integer.parseInt(args[0]);
int q = Integer.parseInt(args[1]);
int gcd = Euclid(p, q, 0);
StdOut.println("\n" + p + " 和 " + q + " 的最大公约数是: " + gcd);
}
1.1.25 使用数学归纳法证明欧几里得算法能够计算任意一对非整数p和q的最大公约数。
1.1.26 将三个数字排序。假设a、b、c 和t 都是同一种原始数字类型的变量。证明以下代码能够将a、 b、c 按照升序排列:
if (a > b) { t = a; a = b; b = t; } // 保证a为a、b两数的较小者
if (a > c) { t = a; a = c; c = t; } // 保证a为a、b、c三数中的最小者
if (b > c) { t = b; b = c; c = t; } // 保证b为比a大的b、c两数的较小者,从而必有c为三数中的最大者
1.1.27 二项分布。用以下代码计算binomial(100, 50,0.25) ,即在单次独立事件发生概率为0.25情况下,实验N次恰好k次发生的概率:
public static double binomial(int N,int k, double p) {
if(N == 0 && k == 0)
return 1.0;
if(N < 0 || k < 0)
return 0.0;
return (1.0 - p) * binomial(N-1, k, p) + p * binomial(N-1, k-1, p);
}
有没有更好的实现方式?
return (1.0 - p) * binomial(N-1, k, p) + p * binomial(N-1, k-1, p);
可以为N次实验发生k次的概率=N-1次实验发生k次的概率*第N次实验不发生的概率 + N-1次实验发生k-1次的概率*第N次实验发生的概率
递归下去求得最后的结果。
上面代码递归求得的值不能被保存,这里考虑使用数组把已经计算过的值保存下来:
private static int binom_N = 100;
private static int binom_k = 50;
private static double[][] binom = new double[binom_N + 1][binom_k + 1];
private static double binomial(int N, int k, double p) {
if(N < 0 || k < 0) {
return 0.0;
} else if(N == 0 && k == 0) {
if(binom[N][k] == -1.0)
binom[N][k] = 1.0;
} else {
if (binom[N][k] == -1.0)
binom[N][k] = (1.0 - p) * binomial(N - 1, k, p) + p * binomial(N - 1, k - 1, p);
}
return binom[N][k];
}
public static void main(String[] args) {
// 数组binom初始化
for(int i = 0; i < binom_N + 1; ++i)
for(int j = 0; j < binom_k + 1; ++j)
binom[i][j] = -1.0;
// 计算概率
double res = binomial(binom_N, binom_k, 0.25);
StdOut.println(res);
}
binom[i][j]代表i次实验,成功j次的概率
打印前十行前五列可得
1.1.28 删除重复元素。修改BinarySearch 类中的测试用例来删去排序之后白名单中的所有重复元素。
/**
* 统计数组arr中重复元素个数
* @param arr
* @return 重复元素个数
*/
public static int countRepeated(int[] arr) {
int cnt = 0;
for (int i = 0; i < (arr.length - 1); ++i)
if (arr[i + 1] == arr[i])
++ cnt;
return cnt;
}
/**
* 去掉重复的元素,并拷贝其余元素
* @param original 原始数组
* @param repeatedCnt 重复的元素个数
* @return 去重后的数组拷贝
*/
public static int[] copy(int[] original, int repeatedCnt) {
int[] res = new int[original.length - repeatedCnt];
int cntIdx = 0;
res[cntIdx ++] = original[0];
// i作为原数组的指针,cuntldx作为新数组的指针,这两个指针在不重复元素间要对齐
for(int i = 1; i < original.length; ++i)
if(original[i] == original[i-1])
continue;
else
res[cntIdx ++] = original[i];
return res;
}
public static void main(String[] args) {
int[] whitelist = new In(args[0]).readAllInts();
// 白名单排序并输出
Arrays.sort(whitelist);
for(int i = 0; i < whitelist.length; ++i)
StdOut.print(whitelist[i] + "\t");
StdOut.println();
// 统计重复元素个数并去重
int cnt = countRepeated(whitelist);
int[] res = copy(whitelist, cnt);
// 输出去重后的数组
for(int i = 0; i < res.length; ++i)
StdOut.print(res[i] + "\t");
}
1.1.29 等值键。为BinarySearch 类添加一个静态方法rank(),它接受一个键和一个整型有序数组(可能存在重复键)作为参数并返回数组中小于该键的元素数量,以及一个类似的方法count() 来返回数组中等于该键的元素的数量。注意:如果i 和j 分别是rank(key,a) 和count(key,a)的返回值,那么a[i~i+j-1] 就是数组中所有和key 相等的元素(只有当key在数组中时成立)
/**
* 二分查找统计
* @param key 待查找关键字
* @param arr 待查找数组
* @return 返回小于key的个数即值等于key的第一个元素的索引值,若找不到则返回-1
*/
private static int countLowers(int key, int[] arr) {
int low = 0;
int high = arr.length - 1;
// 先排除掉key在元素集合外的情况
if(keyarr[high])
return high;
// 二分法搜索直到第一个key值
while(low <= high) {
int mid = low + ((high - low) >> 1);
if(key < arr[mid])
high = mid - 1;
else if(key > arr[mid])
low = mid + 1;
else {
// 当key有多个索引时mid是arr中的某一个索引,向前找到第一个索引
while(mid > 0 && arr[mid] == arr[mid - 1]) // 注意判断条件的先后顺序
-- mid;
return mid;
}
}
// 当key的值在元素集合最小与最大值之间,但没有一个元素等于key时,二分法搜索的low>high跳出while循环
// 此时索引为high的元素是小于key的最大值,索引为low的元素是大于key的最小值,key介于high和low之间
// 索引为X的元素前,有X个元素,故返回low
return low; // 根据算法原理可知low是小于key的个数(low~high)
}
/**
* 统计与key相等的个数
* @param key 待查找关键字
* @param arr 待查找数组
* @return 返回与key相等的个数
*/
private static int countEquals(int key, int[] arr) {
int lowers = countLowers(key, arr);
int idx = lowers;
if(idx == arr.length || key != arr[idx]) // 注意判断条件的先后顺序
return 0;
int cnt = 1;
while((idx < arr.length - 1) && (arr[idx] == arr[idx + 1])) { // 注意判断条件的先后顺序
++ cnt;
++ idx;
}
return cnt;
}
public static void main(String[] args) {
// 从文件读取数据
In in = new In("./data/tinyW.txt");
int[] whiteList = in.readAllInts();
// 排序并打印输出
Arrays.sort(whiteList);
for(int idx = 0; idx < whiteList.length; ++idx)
StdOut.print(whiteList[idx] + "\t");
StdOut.println();
// 从控制台读取关键字
int key = StdIn.readInt();
int lowers = countLowers(key, whiteList);
StdOut.println("小于\t" + key + "\t的个数是:\t" + lowers);
int equals = countEquals(key, whiteList);
StdOut.println("等于\t" + key + "\t的个数是:\t" + equals);
}
习题1.1.30 数组练习。编写一段程序,创建一个 N×N 的布尔数组 a[i][j]。其中当 i+1 和 j+1 互质时(最大公因数为1),a[i][j] 为 true,否则为 false。
private static int gcd(int p, int q) {
if(q == 0)
return p;
int r = p % q;
return gcd(q, r);
}
private static boolean[][] boolArray(int N) {
// 创建NxN的布尔二维数组
boolean[][] boolArr = new boolean[N][N];
for(int i = 1; i <= N; ++i)
for(int j = 1; j <= N; ++j)
if(1 == gcd(i, j))
boolArr[i - 1][j - 1] = true;
else
boolArr[i - 1][j - 1] = false;
return boolArr;
}
public static void main(String[] args) {
int N = 5;
boolean[][] boolArr = boolArray(N);
for(int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j)
StdOut.print(boolArr[i][j] + "\t");
StdOut.println();
}
1.1.31 随机连接。编写一段程序,从命令行接受一个整数N 和double 值p(0 到1 之间)作为参数,在一个圆上画出大小为0.05 且间距相等的N 个点,然后将每对点按照概率p 用灰线连接。
/**
* 画圆
* @param x 圆心x坐标
* @param y 圆心y坐标
* @param r 半径r
*/
private static void drawCircle(double x, double y, double r) {
StdDraw.setXscale(0, 2 * x);
StdDraw.setYscale(0, 2 * y);
StdDraw.setPenRadius(0.003);
StdDraw.setPenColor(StdDraw.BOOK_LIGHT_BLUE);
StdDraw.circle(x, y, r);
}
/**
* 在圆上描点
* @param x0 圆心x坐标
* @param y0 圆心y坐标
* @param r 半径r
* @param N N个点
*/
private static double[][] drawPoints(double x0, double y0, double r, int N) {
// 用一个二维数组存放N个点,第二维分别为x轴与y轴坐标
double[][] points = new double[N][2];
StdDraw.setPenRadius(0.005);
StdDraw.setPenColor(StdDraw.BOOK_RED);
for(int idx = 0; idx < N; ++idx) {
// x=圆心x轴坐标+r*cosθ;y=圆心y轴坐标+r*sinθ(θ是与此点与原点连线同x轴夹角)
double x = x0 + r * Math.cos(2 * Math.PI * idx / N);
double y = y0 + r * Math.sin(2 * Math.PI * idx / N);
StdDraw.point(x, y);
points[idx][0] = x;
points[idx][1] = y;
}
return points;
}
/**
* 以概率p随机连接顶点集points中的点
* @param points 点集
* @param p 概率p
*/
private static void randomLinkPoints(double[][] points, double p) {
StdDraw.setPenRadius(0.002);
StdDraw.setPenColor(StdDraw.LIGHT_GRAY);
int length = points.length;
for(int i = 0; i < length; ++i)
for(int j = 0; j < length; ++j)
// 通过特定算法实现了0~1的连续概率分布,有p的可能性
以下是20个点,0.2概率连接的输出
1.1.32 直方图。假设标准输入流中含有一系列double 值。编写一段程序,从命令行接受一个整数N 和两个double 值l 和r。将(l,r) 分为N 段并使用StdDraw 画出输入流中的值落入每段的数量的直方图。
绘制时需要是用的方法:
public class StdDraw{
static void filledRectangle(double x, double y, duoble rw, double rh)
}
x,y是矩形中心的坐标(按百分比记),例如:
x = 0, y = 0,则该矩形中心的处于画布的左下角
x =0.5, y = 0.5,则该矩形中心处于画布的正中间
rw是长度的一半,rh是高度的一半,也就是说是
绘制以(x,y)为中心,长度为2*rw,高度为rh*2的矩形
/**
* 数据柱状图
* @param N
* @param l
* @param r
* @param arr
*/
private static void dataHistogram(int N, double l, double r, double[] arr) {
int length = arr.length;
int[] statistics = new int[N];
double interval = (r - l) / N;
// 统计数据分布
for(int i = 0; i < length; ++i) {
double remain = arr[i] - l;
int idx = (int)(remain / interval);
++ statistics[idx];
}
// 查找统计结果中最大值,用于绘制直方图时计算柱状图高时
double max = statistics[0];
for(int i = 1; i < statistics.length; ++i) {
if(max < statistics[i])
max = statistics[i];
}
// 绘制直方图
StdDraw.setXscale(l, r);
StdDraw.setPenColor(StdDraw.BOOK_BLUE);
double x0 = l + interval / 2.0;
for(int i = 0; i < statistics.length; ++i) {
double x = x0 + i * interval;
double y = statistics[i] / (max + 1) / 2.0;
double hw = 0.99 * interval / 2.0;
double hh = y;
StdDraw.filledRectangle(x, y, hw, hh);
}
}
public static void main(String[] args) {
In in = new In("./data/largeW.txt");
double[] whiteList = in.readAllDoubles();
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
// 将输入流读取的double数组的最大和最小值找出来
for(int i = 0; i < whiteList.length; ++i) {
if(min > whiteList[i])
min = whiteList[i];
if(max < whiteList[i])
max = whiteList[i];
}
// 从控制台读取应该将数据分割的段数
StdOut.print("将数据分割的段数:");
int N = StdIn.readInt();
dataHistogram(N, min, max + 10.0, whiteList);
}
以下是输入20产生的结果
1.1.33 矩阵库。编写一个Matrix 库并实现以下API: <这道题考虑不周,请读者先略过>
public class Matrix
static double dot(double[ ] x, double[ ] y) 向量点乘
static double[ ][ ] multiple(double[ ][ ] a, double[ ][ ] b) 矩阵和矩阵之积
static double[ ][ ] transpose(double[ ][ ] a) 转置矩阵
static double[ ] multiple(double[ ][ ] a, double[ ] x) 矩阵和向量之积
static double[ ] multiple(double[ ] y, double[ ][ ] a) 向量和矩阵之积
分析:
public class Matrix {
public static double dot(double[] x, double[] y) {
if(x.length != y.length) {
System.out.println("Error!");
return 0;
}
double sum = 0;
for (int i = 0; i < x.length; i++) {
sum += x[i] * y[i];
}
return sum;
}
public static double[][] mult(double[][] a, double [][] b) {
if(a[0].length != b.length) {
System.out.println("Error!");
return null;
}
double[][] c = new double[a.length][b[0].length];
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < b[0].length; j++) {
for (int k = 0; k < b.length; k++) {
c[i][j] += a[i][k] * b[k][j];
}
}
}
return c;
}
public static double[][] transpose(double[][] a) {
double[][] c = new double[a[0].length][a.length];
for (int i = 0; i < a[0].length; i++) {
for (int j = 0; j < a.length; j++) {
c[i][j] = a[j][i];
}
}
return c;
}
public static double[][] mult(double[][] a, double[] x ) {
if(a[0].length != x.length) {
System.out.println("Error!!");
return null;
}
double[][] c = new double[a.length][1];
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[0].length; j++) {
c[i][0] += a[i][j] * x[j];
}
}
return c;
}
public static double[][] mult(double[] y, double[][] a ) {
if(y.length != a.length) {
System.out.println("Error!!!");
return null;
}
double[][] c = new double[1][a[0].length];
for (int j = 0; j < a[0].length; j++) {
for (int k = 0; k < a.length; k++) {
c[0][j] += y[k] * a[k][j];
}
}
return c;
}
public static void print(double[][]a) {
if(a != null) {
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[0].length; j++) {
System.out.print(a[i][j] + " ");
}
System.out.println();
}
}
}
public static void main(String[] args) {
double []a = {1,2};
double []b = {3,4};
System.out.println(dot(a,b));
System.out.println("_____________________________________");
double[][] A = {{1,2,3},{4,5,6}};
double[][] B = {{1,4},{2,5},{8,9}};
print(mult(A,B));
System.out.println("_____________________________________");
double[][] A2 = {{1,2},{3,4},{5,6},{7,8}};
print(transpose(A2));
System.out.println("_____________________________________");
double[][] A3 = {{1,2},{3,4},{5,6},{7,8}};
double[] B3 = {1,2};
print(mult(A3,B3));
System.out.println("_____________________________________");
double[] A4 = {1,2,3,4};
double[][] B4 = {{1,2,3},{4,5,6},{7,8,9},{10,11,12}};
print(mult(A4,B4));
System.out.println("_____________________________________");
}
}
习题:1.1.34(判断题)
过滤。以下哪些任务需要(在数组中,比如)保存标准输入中的所有值?(答案中用A表示)
哪些可以被实现为一个过滤器且仅使用固定数量的变量和固定大小的数组(和N无关)?(答案中用B表示)
在每个问题中,输人都来自于标准输入且含有个0到1的实数。
打印出最大和最小的数 :B
打印出所有数的中位数:A
打印出第k小的数,k小于100:B
打印出所有数的平方和:B
打印出N个数的平均值:B
打印出大于平均值的数的百分比:A
将N个数按照升序打印:A
将N个数按照随机顺序打印:A
实验题
1.1.35 模拟掷骰子。以下代码能够计算每种两个骰子之和的准确概率分布:
int SIDES = 6;
// 虽然结果只有2~12一共11种情况,但为了表述方便,使用一个容量为13的数组存放
double[] dist = new double[2 * SIDES + 1];
for(int i = 1; i <= SIDES; i++)
for(int j = 1; j <= SIDES; j++)
// 相当于用2次循环,找出i+j的每种结果有多少种组成方式
dist[i+j] += 1.0;
for (int k = 2; k <= 2*SIDES; k++)
// 对求出各种情况的概率
dist[k] /= 36.0;
dist[i] 的值就是两个骰子之和为i 的概率。用实验模拟N 次掷骰子,并在计算两个1 到 6 之间的随机整数之和时记录每个值的出现频率以验证它们的概率。N 要多大才能够保证你的经验数据和准确数据的吻合程度达到小数点后三位?
// 色子有六面,数值分别是1、2、3、4、5、6
private static int sides = 6;
/**
* 打印
* @param dist 待输出数组
*/
private static void print(double[] dist) {
for(int i = 2; i <= 2 * sides; ++i)
StdOut.println(dist[i]);
StdOut.println("-------------------------");
}
/**
* 根据统计数据计算概率值
* @param dist 统计数据数组
* @return 概率数组
*/
private static double[] computeProbability(double[] dist, int testTimes) {
for(int i = 2; i <= 2 * sides; ++i)
dist[i] /= (1.0 * testTimes);
return dist;
}
/**
* 两个色子之和的理论概率值
* @return 理论概率值
*/
private static double[] theoreticalValue() {
double[] dist = new double[2 * sides + 1];
// 统计值出现的理论次数
for(int i = 1; i <=sides; ++i)
for(int j = 1; j <= sides; ++j)
dist[i+j] += 1.0;
// 计算理论概率
dist = computeProbability(dist, 36);
return dist;
}
/**
* 用随机数模拟掷色子并统计求出试验概率
* @param N 抛掷次数
* @return 试验概率
*/
private static double[] simulate(int N) {
double[] dist = new double[2 * sides + 1];
// 做N次随机试验模拟抛色子,并统计数据
for(int i = 0; i < N; ++i) {
int a = StdRandom.uniform(1, 6 + 1);
int b = StdRandom.uniform(1, 6 + 1);
dist[a + b] += 1.0;
}
// 计算试验概率值
dist = computeProbability(dist, N);
return dist;
}
/**
* 试验概率值能否与理论概率值至少匹配到小数点后三位数
* @param dist0 理论概率值
* @param dist1 试验概率值
* @return 能匹配到小数点后三位数则返回true,否则返回false
*/
private static boolean isMatch(double[] dist0, double[] dist1) {
for(int i = 2; i <= 2 * sides; ++i)
if(Math.abs(dist0[i] - dist1[i]) >= 0.001)
return false;
return true;
}
/**
* 测试得到符合要求的试验次数N
* @param initTimes 试验初始次数值
* @param dist0 理论概率
* @return 符合要求的试验次数
*/
private static int testGetN(int initTimes, double[] dist0) {
int N = initTimes;
boolean match = false;
while(!match) {
double[] dist1 = simulate(N);
match = isMatch(dist0, dist1);
if(match)
print(dist1);
// 当前N不合要求,则将N增加1000次
N += 1000;
}
return N;
}
public static void main(String[] args) {
double[] dist0 = theoreticalValue();
print(dist0);
int initTimes = 10000;
int N = testGetN(initTimes, dist0);
StdOut.println("至少试验次数的次数: o(" + N + ")");
}
1.1.36 乱序检查。通过实验检查表1.1.10 中的乱序代码是否能够产生预期的效果。编写一个程序ShuffleTest,接受命令行参数M 和N,将大小为M 的数组打乱N 次且在每次打乱之前都将数组重新初始化为a[i] = i。打印一个M×M 的表格,对于所有的列j,行i 表示的是i 在打乱后落到j 的位置的次数。数组中的所有元素的值都应该接近于N/M。
分析:表1.1.10是StdRandom库中的shuffle静态方法的实现(课本第19页)
public static void array_initialization(int[] a) {
for (int i = 0; i < a.length ; i++) {
a[i] = i;
}
}
/**
* @description: 将数组打乱N次
* @param: N 打乱N次
* @param: a 要打乱的数组
* @return: 存储M * M这个表格的数组
*/
public static int[][] upset(int N,int[] a) {
int[][] count = new int[a.length][a.length]; //初始化用于存储M * M这个表格的数组
for (int i = 0; i < N; i++) {
array_initialization(a); //打乱前重新初始化
StdRandom.shuffle(a); //打乱数组
compute(a,count); //计算要打印的表格的值
}
return count;
}
/**
* @description: 计算M * M 表格中,每个元素的值
* @param: a 已经打乱的数组a
* @param: count 存储M * M表格的数组
* @return: Nothing
*/
public static void compute(int[] a,int[][] count) {
int[] b = new int[a.length];
array_initialization(b); //初始化数组b,用来和已经打乱的数组a比较
int c,d;
for (int i = 0; i < a.length; i++) {
c = a[i];
d = b[i];
count[d][c] += 1; //行i表示i在打乱后落到j的次数行i表示i在打乱后落到j的次数
}
}
/**
* @description: 打印一个M * M 的表格,
* @param: count:存储打印信息的数组
* @return: Nothing
*/
public static void print(int[][] count) {
for (int i = 0; i < count.length; i++) {
for (int j = 0; j < count.length; j++) {
System.out.print(count[i][j] + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
int M = Integer.parseInt(args[0]);
int N = Integer.parseInt(args[1]);
int[] a = new int[M];
print(upset(N,a));
}