@ -82,7 +82,7 @@ N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 的增长数量级为 O(N<sup>3</sup>)。增
### 5. 均摊分析
将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的元素 为 N+4+8+16+...+2N=5N-4( N 是向数组写入元素,其余的都是调整数组大小时进行复制需要的访问数组操作 ),均摊后每次操作 访问数组的平均次数为常数。
将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的次数 为 N+4+8+16+...+2N=5N-4( N 是向数组写入元素的操作次数 ,其余的都是调整数组大小时进行复制需要的访问数组次数 ),均摊后访问数组的平均次数为常数。
## ThreeSum
@ -266,7 +266,7 @@ public class StopWatch {
待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。
研究排序算法的成本模型时,计算 的是比较和交换的次数。
研究排序算法的成本模型时,统 计的是比较和交换的次数。
使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。
@ -565,7 +565,7 @@ private int partition(T[] nums, int l, int h) {
快速排序是原地排序,不需要辅助数组,但是递归调用需要辅助栈。
快速排序最好的情况下是每次都正好能 将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 C< sub > N</ sub > =2C< sub > N/2</ sub > +N, 复杂度为 O(NlogN)。
快速排序最好的情况下是每次都正好将数组对半分,这样递归调用次数才是最少的。这种情况下比较次数为 C< sub > N</ sub > =2C< sub > N/2</ sub > +N, 复杂度为 O(NlogN)。
最坏的情况下,第一次从最小的元素切分,第二次从第二小的元素切分,如此这般。因此最坏的情况下需要比较 N< sup > 2</ sup > /2。为了防止数组最开始就是有序的, 在进行快速排序时需要随机打乱数组。
@ -577,13 +577,13 @@ private int partition(T[] nums, int l, int h) {
#### 4.2 三数取中
最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现 取 3 个元素并将大小居中的元素作为切分元素的效果最好 。
最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。一种折中方法是 取 3 个元素, 并将大小居中的元素作为切分元素。
#### 4.3 三向切分
对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。
三向切分快速排序对于只有若干不同主键 的随机数组可以在线性时间内完成排序。
三向切分快速排序对于有大量重复元素 的随机数组可以在线性时间内完成排序。
```java
public class ThreeWayQuickSort < T extends Comparable < T >> extends QuickSort < T > {
@ -643,7 +643,7 @@ public T select(T[] nums, int k) {
### 1. 堆
堆的 某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。
堆中 某个节点的值总是大于等于其 子节点的值,并且堆是一颗完全二叉树。
堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2, 而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。
@ -798,7 +798,7 @@ public class HeapSort<T extends Comparable<T>> extends Sort<T> {
堆排序是一种原地排序,没有利用额外的空间。
现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较。
现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换 。
## 小结
@ -809,13 +809,15 @@ public class HeapSort<T extends Comparable<T>> extends Sort<T> {
| 选择排序 | × | N< sup > 2</ sup > | 1 | |
| 冒泡排序 | √ | N< sup > 2</ sup > | 1 | |
| 插入排序 | √ | N \~ N< sup > 2</ sup > | 1 | 时间复杂度和初始顺序有关 |
| 希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | |
| 希尔排序 | × | N 的若干倍乘于递增序列的长度 | 1 | 改进版插入排序 |
| 快速排序 | × | NlogN | logN | |
| 三向切分快速排序 | × | N \~ NlogN | logN | 适用于有大量重复主键|
| 归并排序 | √ | NlogN | N | |
| 堆排序 | × | NlogN | 1 | | |
| 堆排序 | × | NlogN | 1 | 无法利用局部性原理 |
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN, 这里的 c 比其它线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN, 这里的 c 比其它线性对数级别的排序算法都要小。
使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
### 2. Java 的排序算法实现
@ -831,7 +833,7 @@ Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使
| :---: | :---: |
| UF(int N) | 构造一个大小为 N 的并查集 |
| void union(int p, int q) | 连接 p 和 q 节点 |
| int find(int p) | 查找 p 所在的连通分量 |
| int find(int p) | 查找 p 所在的连通分量编号 |
| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 |
```java
@ -858,7 +860,7 @@ public abstract class UF {
## Quick Find
可以快速进行 find 操作,即 可以快速判断两个节点是否连通。
可以快速进行 find 操作,也就是 可以快速判断两个节点是否连通。
需要保证同一连通分量的所有节点的 id 值相等。
@ -937,7 +939,7 @@ public class QuickUnionUF extends UF {
这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为节点的数目。
< div align = "center" > < img src = "pics/bfbb11e2-d208-4efa-b97b-24cd40467cd8.png" width = "15 0" /> </ div >< br >
< div align = "center" > < img src = "pics/bfbb11e2-d208-4efa-b97b-24cd40467cd8.png" width = "13 0" /> </ div >< br >
## 加权 Quick Union
@ -945,7 +947,7 @@ public class QuickUnionUF extends UF {
理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。
< div align = "center" > < img src = "pics/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png" width = "20 0" /> </ div >< br >
< div align = "center" > < img src = "pics/a4c17d43-fa5e-4935-b74e-147e7f7e782c.png" width = "17 0" /> </ div >< br >
```java
public class WeightedQuickUnionUF extends UF {
@ -1210,8 +1212,6 @@ public class ListStack<Item> implements MyStack<Item> {
## 队列
First-In-First-Out
下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。
这里需要考虑 first 和 last 指针哪个作为链表的开头。因为出队列操作需要让队首元素的下一个元素成为队首,所以需要容易获取下一个元素,而链表的头部节点的 next 指针指向下一个元素,因此可以让 first 指针链表的开头。
@ -2185,10 +2185,10 @@ private void resize(int cap) {
| 算法 | 插入 | 查找 | 是否有序 |
| :---: | :---: | :---: | :---: |
| 二分查找实现的有序 表 | N | log N | yes |
| 链表实现的无序符号 表 | N | N | yes |
| 二分查找实现的有序符号表 | N | logN | yes |
| 二叉查找树 | logN | logN | yes |
| 2-3 查找树 | logN | logN | yes |
| 链表实现的有序表 | N | N | no |
| 拉链法实现的散列表 | N/M | N/M | no |
| 线性探测法实现的散列表 | 1 | 1 | no |
@ -2233,6 +2233,8 @@ public class SparseVector {
< div align = "center" > < img src = "pics/54f1e052-0596-4b5e-833c-e80d75bf3f9b.png" width = "300" /> </ div >< br >
有三个柱子,分别为 from、buffer、to。需要将 from 上的圆盘全部移动到 to 上,并且要保证小圆盘始终在大圆盘上。
这是一个经典的递归问题,分为三步求解:
① 将 n-1 个圆盘从 from -> buffer