算法是一种思维模式
思路+抽象解决问题的方案
总结
数据结构的基本存储方式就是链式和顺序两种
基本操作就是增删改查
遍历方式无非迭代和递归
算法-第四版
一、基础
本书的目的是研究多种重要而实用的算法,即适合用计算机实现的解决问题的方法。
和算法关系最紧密的是数据结构,即便于算法操作的组织数据的方法 。
本章介绍的就是学习算法和数据结构 所需要的基本工具。
基础编程模型
数据抽象并定义抽象数据类型(ADT)以进行模块化编程
三种基础的抽象数据类型:背包、队列和栈
用数组、变长数组和链表实现了背包、队列和栈的 API//它们是全书算法实现的起点和样板
性能是算法研究的一个核心问题。1.4 节描述了分析算法性能的方法
我们的基本做法是科学式的,即先对性能提出假设,建立数学模型,然后用多种实验验证它们,必要时重复这个过程
我们用一个连通性问题作为例子结束本章,它的解法所用到的算法和数据结构可以实现经典的 union-find (?)抽象数据结构
算法
编写一段计算机程序一般都是实现一种已有的方法来解决某个问题。
这种方法大多 和使用的编程语言无关——它适用于各种计 算机以及编程语言。
是这种方法而非计算机程序本身描述了解决问题的步骤。
在计算机科学领域,我们用算法这个词来描述一种有限、确定、有效的并适合用计算机程序来实现的解决问题的方法。算法是计算机科学的基础,是这个领域研究的核心。
要定义一个算法,我们可以用自然语言 描述解决某个问题的过程或是编写一段程序 来实现这个过程。
// 欧几里得算法-自然语言描述
// 计算两个非负整数 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 p;
int r = p % q;
return gcd(q, r);
}
在本书中,我们将用计算机程序来描述算法。这样做的重要原因之一是可以更容易地验证它们是否 如所要求的那样有限、确定和有效
我们关注的大多数算法都需要适当地组织数据,而为了组织数据就产生了数据结构,数据结构也是计算机科学研究的核心对象,它和算法的关系非常密切
学习算法的主要原因是它们能节约非常多的资源,甚至能够让我们完成一些本不可能完成的任务
我们所研究的基础算法在许多应用领域都是解决困难问题的有效方法
在本书中,我们的重点是用最简洁的方式实现优秀的算法
为一项任务选择最合适的算法是困难的,这可能会需要复杂的数学分析。
计算机科学中研究这种问题的分支叫做算法分析。通过分析,我们将要学习的许多算法都有着优秀的理论性能;而另一 些我们则只是根据经验知道它们是可用的。
我们的主要目标是学习典型问题的各种有效算法,但也会注意比较不同算法之间的性能差异。不应该使用资源消耗情况未知的算法,因此我们会时刻关注 算法的期望性能。
Java 程序的基本结构
一段 Java 程序(类)或者是一个静态方法(函数)库,或者定义了一个数据类型。
要创建静态 方法库和定义数据类型,会用到下面七种语法,它们是 Java 语言的基础,也是大多数现代语言所共有的 。
原始数据类型:它们在计算机程序中精确地定义整数、浮点数和布尔值等。它们的定义包括 取值范围和能够对相应的值进行的操作,它们能够被组合为类似于数学公式定义的表达式。
语句:语句通过创建变量并对其赋值、控制运行流程或者引发副作用来进行计算。我们会使
用六种语句:声明、赋值、条件、循环、调用和返回。
数组:数组是多个同种数据类型的值的集合。
静态方法:静态方法可以封装并重用代码,使我们可以用独立的模块开发程序。
字符串:字符串是一连串的字符,Java 内置了对它们的一些操作。
标准输入 / 输出:标准输入输出是程序与外界联系的桥梁。
数据抽象:数据抽象封装和重用代码,使我们可以定义非原始数据类型,进而支持面向对象编程
典型的数组处理代码
找出数组中最大的元素
double max = a[0];
for (int i = 1; i < a.length; i++)
if (a[i] > max) max = a[i];
计算数组元素的平均值
int N = a.length;
double sum = 0.0;
for (int i = 0; i < N; i++)
sum += a[i];
double average = sum / N;
复制数组
int N = a.length;
double[] b = new double[N];
for (int i = 0; i < N; i++)
b[i] = a[i];
颠倒数组元素的顺序
int N = a.length;
for (int i = 0; i < N/2; i++)
{
double temp = a[i];
a[i] = a[N-1-i];
a[N-i-1] = temp;
}
矩阵相乘(方阵) a[][] * b[][] = c[][]
int N = a.length;
double[][] c = new double[N][N];
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) { //计算行i和列j的点乘
for (int k = 0; k < N; k++)
c[i][j] += a[i][k]*b[k][j];
}
计算一个整数的绝对值
public static int abs(int x)
{
if (x < 0) return -x;
else return x;
}
计算一个浮点数的绝对值
public static double abs(double x)
{
if (x < 0.0) return -x;
else return x;
}
判定一个数是否是素数 //一个大于1的自然数,且除了1和它本身外,不能被其他自然数整除的数叫素数
public boolean isPrime(int n){
if (n < 2)return false;
for (int i = 2; i*i < n; i++) {
if (n%i==0)return false;
}
return true;
}
计算平方根(牛顿迭代法)
public static double sqrt(double c) {
if (c < 0) return Double.NaN;
double err = 1e-15;
double t = c;
while (Math.abs(t - c / t) > err * t)
t = (c / t + t) / 2.0;
return t;
}
计算直角三角形的斜边
public static double hypotenuse(double a, double b)
{ return Math.sqrt(a*a + b*b); }
计算调和级数
public static double H(int N) {
double sum = 0.0;
for (int i = 1; i <= N; i++)
sum += 1.0 / i;
return sum;
}
递归
方法可以调用自己(如果你对递归概念感到奇怪,请完成练习 1.1.16 到练习 1.1.22)
类型转换
String值和数字之间相互转换的API
parseInt(String s)
toString(int i)
parseDouble(String s)
toString(double x)
将字符串 s 转换为整数 将整数 i 转换为字符串
将字符串 s 转换为浮点数 将浮点数 x 转换为字符串
二分查找
public int search(int[] nums, int target) {
int low = 0;
int high = nums.length - 1;
while (low < high) {
int mid = (low + high) >> 1;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
high = mid - 1;
} else if (nums[mid] < target) {
low = mid + 1;
}
}
return -low;
}
问 什么是 Java 的字节码?
答 它是程序的一种低级表示,可以运行于 Java 的虚拟机。将程序抽象为字节码可以保证 Java 程序员的代码能够运行在各种设备之上
问 Java 允许整型溢出并返回错误值的做法是错误的。难道 Java 不应该自动检查溢出吗?
答 这个问题在程序员中一直是有争议的。简单的回答是它们之所以被称为原始数据类型就是因为缺乏此类检查。避免此类问题并不需要很高深的知识。我们会使用 int 类型表示较小的数(小于 10 个十进制位)而使用 long 表示 10 亿以上的数。
问 Math.abs(-2147483648)的返回值是什么?
答 -2147483648。这个奇怪的结果(但的确是真的)就是整数溢出的典型例子
问 如何才能将一个 double 变量初始化为无穷大?
答 可以使用 Java 的内置常数:Double.POSITIVE_INFINITY 和 Double.NEGATIVE_INFINITY。
问 能够将 double 类型的值和 int 类型的值相互比较吗?
答 不通过类型转换是不行的,但请记住 Java 一般会自动进行所需的类型转换。例如,如果 x 的类型是 int 且值为 3,那么表达式 (x<3.1) 的值为 true——Java 会在比较前将 x 转换为 double 类型(因为 3.1是一个 double 类型的字面量)。
问 如果使用一个变量前没有将它初始化,会发生什么?
答 如果代码中存在任何可能导致使用未经初始化的变量的执行路径,Java 都会抛出一个编译异常
问 Java 表达式 1/0 和 1.0/0.0 的值是什么?
答 第一个表达式会产生一个运行时除以零异常(它会终止程序,因为这个值是未定义的);第二个表 达式的值是 Infinity(无穷大)。
问 能够使用 < 和 > 比较 String 变量吗?
答 不行,只有原始数据类型定义了这些运算符。请见 1.1.2.3 节
问 负数的除法和余数的结果是什么?
答 表达式a/b的商会向0取整;a % b的余数的定义是(a/b)*b + a % b恒等于a。例如-14/3和14/-3的商都是-4,但-14 % 3是-2,而14 % -3是 2。
问 为什么使用(a && b)而非(a & b)?
答 运算符 &、| 和 ^ 分别表示整数的位逻辑操作与、或和异或。因此,10|6 的值为 14,10^6 的值为 12。在本书中我们很少(偶尔)会用到这些运算符。&& 和 || 运算符仅在独立的布尔表达式中有效,原因是短路求值法则:表达式从左向右求值,一旦整个表达式的值已知则停止求值。
问 嵌套 if 语句中的二义性有问题吗?
答 是的。在 Java 中,以下语句:
if if else 等价于:
if { if else }
即使你想表达的是:
if { if } else
避免这种“无主的”else 陷阱的最好办法是显式地写明所有大括号
问 一个 for 循环和它的 while 形式有什么区别?
答 for循环头部的代码和for循环的主体代码在同一个代码段之中。在一个典型的for循环中,递
增变量一般在循环结束之后都是不可用的;但在和它等价的 while 循环中,递增变量在循环结束
之后仍然是可用的。这个区别常常是使用 while 而非 for 循环的主要原因
问 有些 Java 程序员用 int a[] 而不是 int[] a 来声明一个数组。这两者有什么不同?
答 在 Java 中,两者等价且都是合法的。前一种是 C 语言中数组的声明方式。后者是 Java 提倡的方式,因为变量的类型 int[] 能更清楚地说明这是一个整型的数组。
问 为什么数组的起始索引是 0 而不是 1 ?
答 这个习惯来源于机器语言,那时要计算一个数组元素的地址需要将数组的起始地址加上该元素的索引。将起始索引设为 1 要么会浪费数组的第一个元素的空间,要么会花费额外的时间来将索引减 1。
问 如果a[]是一个数组,为什么StdOut.println(a)打印出的是一个十六进制的整数,比如@f62373,而不是数组中的元素呢?
答 问得好。该方法打印出的是这个数组的地址,不幸的是你一般都不需要它。
问 我的程序能够重新读取标准输入中的值吗?
答 不行,你只有一次机会,就好像你不能撤销 println() 的结果一样
问 在 Java 中,一个静态方法能够将另一个静态方法作为参数吗?
答 不行,但问得好,因为有很多语言都能够这么做 //?
判断字符串是否是一条回文
public static boolean isPalindrome(String s) {
int N = s.length();
for (int i = 0; i < N / 2; i++)
if (s.charAt(i) != s.charAt(N - 1 - i))
return false;
return true;
}
从一个命令行参数中提取文件名和扩展名
String s = args[0];
int dot = s.indexOf(".");
String base = s.substring(0, dot);
String extension = s.substring(dot + 1, s.length());
检查一个字符串数组中的元素是否已按照字母表顺序排列//降序
public boolean isSorted(String[] a) {
for (int i = 1; i < a.length; i++) {
if (a[i - 1].compareTo(a[i]) > 0)
return false;
}
return true;
}
问 为什么要使用数据抽象?
答 它能够帮助我们编写可靠而正确的代码
问 为什么要区别原始数据类型和引用类型?为什么不只用引用类型?
答 因为性能。Java 提供了 Integer、Double 等和原始数据类型对应的引用类型,以供希望忽略这些类型的区别的程序员使用。原始数据类型更接近计算机硬件所支持的数据类型,因此使用它们的程序比使用引用类型的程序运行得更快
问 数据类型必须是抽象的吗?
答 不。Java 也支持 public 和 protected 来帮助用例直接访问实例变量。如正文所述,允许用例代码 直接访问数据所带来的好处比不上对数据的特定表示方式的依赖所带来的坏处,因此我们代码中所有的实例变量都是私有的(private),有时也会使用私有实例方法在公有方法之间共享代码
问 指针是什么?
答 问得好。或许上面那个异常应该叫做 NullReferenceException。和 Java 的引用一样,可以把指 针看做机器地址。在许多编程语言中,指针是一种原始数据类型,程序员可以用各种方法操作它。但众所周知,指针的编程非常容易出错,因此需要精心设计指针类的操作以帮助程序员避免错误
在 Java 中,创建引 用的方法只有一种(new),且改变引用的方法也只有一种(赋值语句)。也就是说,程序员能对引 用进行的操作只有创建和复制。
在编程语言的行话里,Java 的引用被称为安全指针,因为 Java 能够 保证每个引用都会指向某种类型的对象(而且它能找出无用的对象并将其回收)。习惯于编写直接 操作指针的程序员认为 Java 完全没有指针,但人们仍在为是否真的需要不安全的指针而争论
问 我在哪里能够找到 Java 如何实现引用和进行垃圾收集的细节?
答 Java 系统的实现各有不同。例如,实现引用的一种自然方式是使用指针(机器地址);而另一种使 用的则可能是句柄(指针的指针)。前者访问数据的速度更快,而后者则能够更好地实现垃圾回收
问 实现继承有什么问题?
答 子类继承阻碍模块化编程的原因有两点。
第一,父类的任何改动都会影响它的所有子类。子类的开发不可能和父类无关。事实上,子类是完全依赖于父类的。这种问题被称为脆弱的基类问题。
第二, 子类代码可以访问所有实例变量,因此它们可能会扭曲父类代码的意图
什么是空(null)?
它是一个不指向任何对象的字面量。引用null调用一个方法是没有意义的,并且会产生 NullPointerException。如果你得到了这条错误信息,请检查并确认构造函数是否正确地初始化了类的所有实例变量
背包、队列和栈
背包是一种不支持从中删除元素的集合数据类型
先进先出队列(或简称队列)是一种基于先进先出(FIFO)策略的集合类型
下压栈(或简称栈)是一种基于后进先出(LIFO)策略的集合类型
本节的第一个目标是说明我们对集合中的对象的表示方式将直接影响各种操作的效率。对于集合来说,我们将会设计适于表示一组对象的数据结构并高效地实现所需的方法。
本节的第二个目标是介绍泛型和迭代。它们都是简单的 Java 概念,但能极大地简化用例代码。 它们是高级的编程语言机制,虽然对于算法的理解并不是必需的,但有了它们我们能够写出更加清晰、简洁和优美的用例(以及算法的实现)代码。
本节的第三个目标是介绍并说明链式数据结构的重要性,特别是经典数据结构链表,有了它我 们才能高效地实现背包、队列和栈。理解链表是学习各种算法和数据结构中最关键的第一步
泛型
集合类的抽象数据类型的一个关键特性是我们应该可以用它们存储任意类型的数据。一种特别的 Java 机制能够做到这一点,它被称为泛型,也叫做参数化类型
Evaluate
将操作数压入操作数栈;
将运算符压入运算符栈;
忽略左括号;
在遇到右括号时,弹出一个运算符,弹出所需数量的操作数,并将运算符和操作数的运算结果压入操作数栈。
对增长数量级的常见假设的总结 //1.4.4
第2章 排 序
排序就是将一组对象按照某种逻辑顺序重新排列的过程
即使你只是使用标准库中的排序函数,学习排序算法仍然有三大实际意义:
‰ 对排序算法的分析将有助于你全面理解本书中比较算法性能的方法;
‰ 类似的技术也能有效解决其他类型的问题;
‰ 排序算法常常是我们解决其他问题的第一步。
更重要的是这些算法都很经典、优雅和高效
初级排序//选择、插入
问 为什么有这么多排序算法? 答 原因之一是许多排序算法的性能都和输入模型有很大的关系,因此不同的算法适用于不同应用场景中的不同输入
归并排序
即将两个有序的数组归并成一个更大 的有序数组
快速排序
它可能是应用最广泛的排序算法了。快速排序流行的原因是它实现简单、 适用于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。快速排序引人注目的特点包 括它是原地排序(只需要一个很小的辅助栈),且将长度为 N 的数组排序所需的时间和 NlgN 成正比
它的主要缺点是非常脆弱,在实现 时要非常小心才能避免低劣的性能。已经有无数例子显示许多种错误都能致使它在实际中的性能只 有平方级别
性能特点
快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁性 也是快速排序的一个优点,很难想象排序算法中还能有比这更短小的内循环了。例如,归并 排序和希尔排序一般都比快速排序慢,其原因就是它们还在内循环中移动数据
快速排序另一个速度优势在于它的比较次数很少
快速排序的最好情况是每次都正好能将数组对半分。在这种情况下快速排序所用的比较次数正 好满足分治递归的CN=2CN/2+N公式。2CN/2 表示将两个子数组排序的成本,N表示用切分元素和所 有数组元素进行比较的成本
尽管快速排序有很多优点,它的基本实现仍有一个潜在的缺点:在切分不平衡时这个程序可能会 极为低效。例如,如果第一次从最小的元素切分,第二次从第二小的元素切分,如此这般,每次调用 只会移除一个元素。这会导致一个大子数组需要切分很多次。我们要在快速排序前将数组随机排序的 主要原因就是要避免这种情况。它能够使产生糟糕的切分的可能性降到极低,我们就无需为此担心了
快速排序最多需要约 N 2/2 次比较,但随机打乱数组能够预防这种情况
三向切分的快速排序
优先队列
第 3 章 查 找
二叉查找树、红黑树和散列表
符号表
二叉查找树
一种能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表 实现
定义。一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个 Comparable 的键(以 及相关联的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点 的键。
平衡查找树
红黑二叉查找树
一棵大小为 N 的红黑树的高度不会超过 2lgN
散列表
第4章 图
定义。图是由一组顶点和一组能够将两个顶点相连的边组成的。
无向图
二分图//电影和演员的关系
第5章字符串
字符。String是由一系列字符组成的。字符的类型是char
不可变性。String 对象是不可变的
正则表达式
第6章背景
B- 树
B- 树的成本模型。我们使用页的访问次数(无论读写)作为外部查找算法的成本模型
B- 树
它是对 3.3 节所述的 2-3 树数据结构的扩展。关键的不同在于:我们不会将数据保存在树中,而是会构造一棵由键的副本组成的树,每个副本都关联着一条链接。这种方式能够更加方便地将索引和符号表本身分开,就像一本实体书中的索引一样
线性规划。给定一个由 M 个线性不等式组成的集合和含有 N 个决策变量的线性等式,以及一个由该 N 个决策变量组成的线性目标函数,找出能够使目标函数的值最大化的一组变量值,或者证 明不存在这样的赋值方案