概述
编写一段计算机程序一般都是实现一种已有的方法来解决问题。
在计算机领域中,我们用算法这个词来描述一种有限、确定、有效的并适合用计算机程序来实现的解决问题的方法。
第一章
欧几里德算法:
自然语言描述:计算俩个非负整数p和q的最大公约数:若q是0,则最大公约数为p。否则,将p除以q得到余数r,p和q的最大公约数即为q和r的最大公约数。
Java语言描述:
public static int gcd(int p,int q){
if(q==0) return q;
int r=p%q;
return gcd(q,r);
}
我们关注的大多数算法都需要适当地组织数据,而为了组织数据就产生了数据结构,它与算法的关系非常密切。
二分查找算法:
//二分查找算法
public class BinarySearch {
public static int rank(int key, int[] a) {
//数组必须是有序的
int first = 0;
int last = a.length - 1;
while (first <= last) {
//被查找的键要么不存在,要么必定存在于a[first...last]中
int mind = (first + last) / 2;
if (key < a[mind])
last = mind - 1;
else if (key > a[mind])
first = mind + 1;
else
return mind;
}
return -1;
}
第一章课后答疑
Java允许整形溢出并返回错误值的做法是错误的。难道Java不应该自动检查溢出吗?
答:这个问题在程序员中一直是有争议的。简单的回答是它们之所以称为原始数据类型就是因为缺乏此类检查。避免此类问题并不需要很高深的知识。我们会用int类型表示较小的数(小于10个十进制位)而使用long表示10亿以上的数。
Math.abs(-2147483648)的返回值是什么?
答:-2147483648。原因:整数溢出
如何才能将一个double变量初始化为无穷大?
答:可以使用java的内置常数:Double.POSITIVE_INFINITY 和 Double.NEGATIVE_INFINITY
Java表达式1/0和1.0/0.0的值是什么?
答:第一个表达式会产生一个运行时除零异常(它会终止这个程序,因为这个值是未定义的);第二个表达式的值是Ifinity(无穷大)。
负数的除法和余数的结果是什么?
答:表达式a/b的商会向0取整;a%b的余数的定义是(a/b)*b+a%b恒等于a。例如-14/3和14/-3的商都是-4,但-14%3是-2,而14%-3是2
嵌套if语句的二义性有问题吗?
答:是的。在Java中,以下语句:
if<exp1> if<exp2><stmnA>else<stnmB>
等价于:
if<exp1>{ if<exp2><stmnA>else<stnmB>}
有些java程序员用int a[]而不是int []a来声明一个数组,有什么区别?
答:在Java中,俩者等价且都是合法的。前一种是C语言中数组的声明方式。后者是Java提倡的方式,因为变量的类型int[]能更清楚地说明这是一个整形数组。
为什么数组的起始索引是0而不是1?
答:这个习惯来源于机器语言,那时要计算数组元素的地址需要将数组的起始地址加上该元素的索引。将起始索引设为1的话会浪费数组的第一个元素的空间,要么会花费额外的时间来将索引减1。
我们的程序能够重新读取标准输入的值吗?
答:不行,你只要一次机会,就好像你不能撤销println()
的结果一样。
在Java中,一个静态方法能够将另一个静态方法作为参数吗?
答:不行,但有很多语言可以这么做。
第二章
选择排序:
public class Selection {
public static void sort(Comparable[] a){
//将a[]按升序排序
int length=a.length;
int temp;
for(int i=0;i<a.length;i++){
int min=i;
for(int j=i+1;j<a.length;j++){
if(a[j]<a[i]){
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
}
}
}
选择排序时间复杂度为O(n²),空间复杂度为O(1),时间复杂度不稳定
插入排序:
public class Insertion {
public static void sort(int[] a){
//将a[]按升序排序
int length=a.length;
int temp;
for(int i=1;i<length;i++){
for(int j=i;j>0;j--){
if(a[j]<a[j-1]){
temp=a[j-1];
a[j-1]=a[j];
a[j]=temp;
}
}
}
}
}
选择排序时间复杂度为O(n²),空间复杂度为O(1),时间复杂度稳定
希尔排序:
基于插入排序基础的快速排序算法
public class Shell {
public static void sort(Comparable[] a){
//将a[]按升序排序
int N=a.length;
int h=1;
int temp;
if(n<N/3) h=h*3+1; //1,4,13,40,121,364.....
while(h>0){
for (int i = h; i < N; i++) {
for (int j = i; j >= h; j -= h) {
if (a[j] < a[j - h]) {
temp = a[j - h];
a[j - h] = a[j];
a[j] = temp;
}
}
}
h=h/3;
}
}
}
希尔排序的时间复杂度取决于所用的序列h的大小,差不多为O(n^1.3),空间复杂度为O(1)
归并排序:
在了解原地归并的思想之前,先回忆一下一般的归并算法,先是将有序子序列分别放入临时数组,然后设置两个指针依次从两个子序列的开始寻找最小元素放入归并数组中;那么原地归并的思想亦是如此,就是归并时要保证指针之前的数字始终是两个子序列中最小的那些元素。
文字叙述多了无用,见示例图解,一看就明白。
假设我们现在有两个有序子序列如图a,进行原地合并的图解示例如图b开始
如图b,首先第一个子序列的值与第二个子序列的第一个值20比较,如果序列一的值小于20,则指针i向后移,直到找到比20大的值,即指针i移动到30;**经过b,我们知道指针i之前的值一定是两个子序列中最小的块。**
如图c,先用一个临时指针记录j的位置,然后用第二个子序列的值与序列一i所指的值30比较,如果序列二的值小于30,则j后移,直到找到比30大的值,即j移动到55的下标;
如图d,经过图c的过程,我们知道数组块 [index, j) 中的值一定是全部都小于指针i所指的值30,即数组块 [index, j) 中的值全部小于数组块 [i, index) 中的值,为了满足原地归并的原则:始终保证指针i之前的元素为两个序列中最小的那些元素,即i之前为已经归并好的元素。我们交换这两块数组的内存块,交换后i移动相应的步数,这个“步数”实际就是该步归并好的数值个数,即数组块[index, j)的个数。从而得到图e如下:
重复上述的过程,如图f,相当于图b的过程,直到最后,这就是原地归并的一种实现思想
原地归并排序:
void merge(T a[],int begin,int mid,int end){
int i = begin;
int j = mid + 1;
while( i < j && j <= end){
while(i < j && a[i] <= a[j]){
i++;
}
int old_j = j;
while(j <= end && a[j] < a[i]){
j++;
}
exchange(a,i,old_j-1,j-1);
i += (j - old_j);
}
}
自顶向下的归并排序:
采用分治法进行自顶向下的程序设计方式,分治法的核心思想就是分解、求解、合并。
(1)先将长度为N的无序序列分割平均分割为两段
(2)然后分别对前半段进行归并排序、后半段进行归并排序
(3)最后再将排序好的前半段和后半段归并
过程(2)中进行递归求解,最终下图详细的分解了自顶向下的合并算法的实现过程:
代码:
平均时间复杂度:O(nlog2n)
空间复杂度:O(n) (用于存储有序子序列合并后有序序列)
稳定性:稳定
自底向上的归并排序:
自底向上的排序是归并排序的一种实现方式,将一个无序的N长数组切个成N个有序子序列,然后再两两合并,然后再将合并后的N/2(或者N/2 + 1)个子序列继续进行两两合并,以此类推得到一个完整的有序数组。下图详细的分解了自底向上的合并算法的实现过程:
快速排序:
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。