第一章 · 前言
【1】算法本质上就是解决问题的一套方法描述,说直白一点就是我们常说的解题思路。
【2】比如有这么一个问题:我们要求两个非负整数(18和12)的最大公约数(Greatest Common Divisor,简称GCD),你就得像下面这样大概描述一下怎么求解,注意,这里只能说是大概,因为没有考虑一些特殊情况:
- 最简单粗暴的,先看哪个小,然后看看最小的是不是他们的最大公约数,如果不是逐个向下找,也就是说先看12是不是他们的最大公约数,如果不是,那么11,10,9…一直找到6,所以结果是6。
- 如果数字稍微大一点的话,像上面那种做法会死人的,按照我们以前的做题方法,是从2,3这些数字先除,这些除数最终相乘就是结果,比如先让他们被2除,得到9和6,然后让他们再被3除,得到3和2,到此他们不能被除了1以后的其他数除了,这是我们把之前的除数2和3相乘,得到结果6。
- 假设两个非负整数p和q,若q为0,则最大公约数是p,否则将p除以q得到余数r,p和q的最大公约数就是q和r的最大公约数。
以上是文字语言的描述,我们下面用编程语言来描述。第二种算法总体而言会比第一种效率要慢一些,如果感兴趣可以自己实现,我们来看看第一种和第三种。第一种运行时间11743毫秒,第三种运行时间10798毫秒。也就是说,对于追求效率来讲,算法之间是存在优劣的。
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
long start1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++){
gcd1(10240000,52000);
}
long end1 = System.currentTimeMillis();
System.out.println(end1 - start1);
long start2 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++){
gcd3(10240000,52000);
}
long end2 = System.currentTimeMillis();
System.out.println((end2 - start2));
}
private static int gcd1(int num1, int num2){
int min = num1;
int max = num2;
if (num1 > num2){
min = num2;
max = num1;
}
if (min == 0){
return max;
}
int res = min;
for (int i = min; i > 0; i--){
if (num1 % i == 0 && num2 % i == 0){
res = i;
break;
}
}
return res;
}
private static int gcd3(int num1, int num2){
if (num2 == 0){
return num1;
}
int r = num1 % num2;
return gcd1(num2, r);
}
}
【3】所以,学习算法的目的就是在解决问题的时候能找到那种效率最高的解题思路。但是,好的算法并不是谁都能设计出来的,大部分我们都是使用了现成的前人设计出来的算法。比如上面的求最大公约数的第三种算法,可能很多人都没听过,甚至即便现在看到了,也在怀疑这种算法到底正不正确。其实,这种算法是可以用数学证明的,详见欧几里得算法:从证明等式gcd(m, n) = gcd(n, m mod n)对每一对正整数m, n都成立说开去:
令 d = gcd(m, n),则 d|m 且 d|n。
设 m = kn + r(0≤ r < n),则 d|(kn+r)。
又有 d|n,因此 d|kn,所以有 d|r。
即我们由 d|m 且 d|n 这个前提可以得出 d|r。
换就话说,我们也可以说成由 d|m 且 d|n 这个前提推出了 d|n 且 d|r。
使用类似的推理过程同样可以得到:由 d|n 且 d|r 可以推出 d|m 且 d|n。
注意到这里的 r 即 m mod n。
因此我们可以说 gcd(m, n) = gcd(n, m mod n)是双向成立的,命题得证。
第一章 · 1.1 基础编程模型
【1】这一节如果有编程基础的可以略过,主要是在讲Java的数据类型和简单编程知识。
【2】这一节的最后,使用了一个二分查找(Binary Search)的例子来说明如何用Java写一个完整的算法程序。这里的二分查找的核心思想是:首先被查找的那个数组或者列表一定要是排序过的,而且降序升序也有讲究,我们这里是升序排列,也就是从小到大;然后不管你要找什么数字,我的范围是从0到数组最后一个n,我先取中间0 + (n-0)/2
的那个来和你比较,如果恰好相等,说明就找到了,如果中间的这个数字比你要找的数字大,那么你要找的数字肯定在中间向左边的位置,那我们就在0到0 + (n-0)/2
中找;反之就在中间向右边的位置,我们就在0 + (n-0)/2
到n中找;一直重复上面的步骤,直到找到相等的,实在没有相等的就返回找不到的结果。如你所见,如果是降序的数组或者列表,那么上面的左边还是右边正好是相反。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入几个数字,以英文逗号分隔:");
String numStr = sc.nextLine();
List<String> numStrList = Arrays.asList(numStr.split(","));
List<Integer> numsIntList = new ArrayList<>();
for (int i = 0; i < numStrList.size(); i++){
numsIntList.add(Integer.valueOf(numStrList.get(i)));
}
System.out.println("您输入的几个数字是:" + numsIntList.toString());
Collections.sort(numsIntList);
System.out.println("排序后的几个数字是:" + numsIntList.toString());
System.out.println("请输入要查找的数组:");
int num = sc.nextInt();
int index = binarySearch(num, numsIntList);
if(index == -1){
System.out.println("您要查找的数字不在数组中");
}else{
System.out.println("您要查找的数字位置在排序后数组的:" + String.valueOf(index));
}
}
private static int binarySearch(int num, List<Integer> numList){
int low = 0;
int high = numList.size() - 1;
while (low <= high){
int middle = low + (high - low)/2;
if(num > numList.get(middle)){
low = middle + 1;
}else if(num < numList.get(middle)){
high = middle - 1;
}else {
return middle;
}
}
return -1;
}
}
运行后:
请输入几个数字,以英文逗号分隔:
45,23,24,5,67,654,34
您输入的几个数字是:[45, 23, 24, 5, 67, 654, 34]
排序后的几个数字是:[5, 23, 24, 34, 45, 67, 654]
请输入要查找的数组:
67
您要查找的数字位置在排序后数组的:5
第一章 · 1.2 数据抽象
【1】这一小节,有面向对象基础的可以略过,这里可以简单地认为作者在介绍类、对象以及封装和接口等等的编程思想。
第一章 · 1.3 背包、队列和栈
【1】