auto commit
This commit is contained in:
70
notes/算法.md
70
notes/算法.md
@ -379,7 +379,7 @@ public class Insertion<T extends Comparable<T>> extends Sort<T> {
|
||||
|
||||
对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。
|
||||
|
||||
希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。
|
||||
希尔排序的出现就是为了解决插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。
|
||||
|
||||
希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。
|
||||
|
||||
@ -571,15 +571,15 @@ private int partition(T[] nums, int l, int h) {
|
||||
|
||||
### 4. 算法改进
|
||||
|
||||
(一)切换到插入排序
|
||||
#### 4.1 切换到插入排序
|
||||
|
||||
因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。
|
||||
|
||||
(二)三数取中
|
||||
#### 4.2 三数取中
|
||||
|
||||
最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。
|
||||
|
||||
(三)三向切分
|
||||
#### 4.3 三向切分
|
||||
|
||||
对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。
|
||||
|
||||
@ -645,7 +645,7 @@ public T select(T[] nums, int k) {
|
||||
|
||||
堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。
|
||||
|
||||
堆可以用数组来表示,因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。
|
||||
堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。
|
||||
|
||||
<div align="center"> <img src="../pics//f3080f83-6239-459b-8e9c-03b6641f7815.png" width="200"/> </div><br>
|
||||
|
||||
@ -739,15 +739,15 @@ public T delMax() {
|
||||
|
||||
### 5. 堆排序
|
||||
|
||||
由于堆可以很容易得到最大的元素并删除它,不断地进行这种操作可以得到一个递减序列。如果把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列。因此很容易使用堆来进行排序。并且堆排序是原地排序,不占用额外空间。
|
||||
把最大元素和当前堆中数组的最后一个元素交换位置,并且不删除它,那么就可以得到一个从尾到头的递减序列,从正向来看就是一个递增序列,这就是堆排序。
|
||||
|
||||
(一)构建堆
|
||||
#### 5.1 构建堆
|
||||
|
||||
无序数组建立堆最直接的方法是从左到右遍历数组,然后进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。
|
||||
无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。
|
||||
|
||||
<div align="center"> <img src="../pics//b84ba6fb-312b-4e69-8c77-fb6eb6fb38d4.png" width="300"/> </div><br>
|
||||
|
||||
(二)交换堆顶元素与最后一个元素
|
||||
#### 5.2 交换堆顶元素与最后一个元素
|
||||
|
||||
交换之后需要进行下沉操作维持堆的有序状态。
|
||||
|
||||
@ -804,7 +804,7 @@ public class HeapSort<T extends Comparable<T>> extends Sort<T> {
|
||||
|
||||
### 1. 排序算法的比较
|
||||
|
||||
| 算法 | 稳定 | 时间复杂度 | 空间复杂度 | 备注 |
|
||||
| 算法 | 稳定性 | 时间复杂度 | 空间复杂度 | 备注 |
|
||||
| :---: | :---: |:---: | :---: | :---: |
|
||||
| 选择排序 | × | N<sup>2</sup> | 1 | |
|
||||
| 冒泡排序 | √ | N<sup>2</sup> | 1 | |
|
||||
@ -815,7 +815,7 @@ public class HeapSort<T extends Comparable<T>> extends Sort<T> {
|
||||
| 归并排序 | √ | NlogN | N | |
|
||||
| 堆排序 | × | NlogN | 1 | | |
|
||||
|
||||
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
|
||||
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其它线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。
|
||||
|
||||
### 2. Java 的排序算法实现
|
||||
|
||||
@ -882,7 +882,6 @@ public class QuickFindUF extends UF {
|
||||
|
||||
@Override
|
||||
public void union(int p, int q) {
|
||||
|
||||
int pID = find(p);
|
||||
int qID = find(q);
|
||||
|
||||
@ -917,7 +916,6 @@ public class QuickUnionUF extends UF {
|
||||
|
||||
@Override
|
||||
public int find(int p) {
|
||||
|
||||
while (p != id[p]) {
|
||||
p = id[p];
|
||||
}
|
||||
@ -927,7 +925,6 @@ public class QuickUnionUF extends UF {
|
||||
|
||||
@Override
|
||||
public void union(int p, int q) {
|
||||
|
||||
int pRoot = find(p);
|
||||
int qRoot = find(q);
|
||||
|
||||
@ -938,7 +935,7 @@ public class QuickUnionUF extends UF {
|
||||
}
|
||||
```
|
||||
|
||||
这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。
|
||||
这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为节点的数目。
|
||||
|
||||
<div align="center"> <img src="../pics//bfbb11e2-d208-4efa-b97b-24cd40467cd8.png" width="150"/> </div><br>
|
||||
|
||||
@ -1588,7 +1585,7 @@ public class BST<Key extends Comparable<Key>, Value> implements OrderedST<Key, V
|
||||
}
|
||||
```
|
||||
|
||||
(为了方便绘图,下文中二叉树的空链接不画出来。)
|
||||
为了方便绘图,下文中二叉树的空链接不画出来。
|
||||
|
||||
### 1. get()
|
||||
|
||||
@ -1748,7 +1745,7 @@ public Node deleteMin(Node x) {
|
||||
|
||||
### 8. delete()
|
||||
|
||||
- 如果待删除的节点只有一个子树,那么只需要让指向待删除节点的链接指向唯一的子树即可;
|
||||
- 如果待删除的节点只有一个子树, 那么只需要让指向待删除节点的链接指向唯一的子树即可;
|
||||
- 否则,让右子树的最小节点替换该节点。
|
||||
|
||||
<div align="center"> <img src="../pics//fa568fac-ac58-48dd-a9bb-23b3065bf2dc.png" width="400"/> </div><br>
|
||||
@ -1806,9 +1803,9 @@ private List<Key> keys(Node x, Key l, Key h) {
|
||||
}
|
||||
```
|
||||
|
||||
### 10. 性能分析
|
||||
### 10. 分析
|
||||
|
||||
复杂度:二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。
|
||||
二叉查找树所有操作在最坏的情况下所需要的时间都和树的高度成正比。
|
||||
|
||||
## 2-3 查找树
|
||||
|
||||
@ -1838,7 +1835,7 @@ private List<Key> keys(Node x, Key l, Key h) {
|
||||
|
||||
## 红黑树
|
||||
|
||||
2-3 查找树需要用到 2- 节点和 3- 节点,红黑树使用红链接来实现 3- 节点。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
|
||||
红黑树是 2-3 查找树,但它不需要分别定义 2- 节点和 3- 节点,而是在普通的二叉查找树之上,为节点添加颜色。指向一个节点的链接颜色如果为红色,那么这个节点和上层节点表示的是一个 3- 节点,而黑色则是普通链接。
|
||||
|
||||
<div align="center"> <img src="../pics//4f48e806-f90b-4c09-a55f-ac0cd641c047.png" width="250"/> </div><br>
|
||||
|
||||
@ -1853,6 +1850,7 @@ private List<Key> keys(Node x, Key l, Key h) {
|
||||
|
||||
```java
|
||||
public class RedBlackBST<Key extends Comparable<Key>, Value> extends BST<Key, Value> {
|
||||
|
||||
private static final boolean RED = true;
|
||||
private static final boolean BLACK = false;
|
||||
|
||||
@ -2008,16 +2006,17 @@ int hash = (((day * R + month) % M) * R + year) % M;
|
||||
|
||||
R 通常取 31。
|
||||
|
||||
Java 中的 hashCode() 实现了 hash 函数,但是默认使用对象的内存地址值。在使用 hashCode() 函数时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。
|
||||
Java 中的 hashCode() 实现了哈希函数,但是默认使用对象的内存地址值。在使用 hashCode() 时,应当结合除留余数法来使用。因为内存地址是 32 位整数,我们只需要 31 位的非负整数,因此应当屏蔽符号位之后再使用除留余数法。
|
||||
|
||||
```java
|
||||
int hash = (x.hashCode() & 0x7fffffff) % M;
|
||||
```
|
||||
|
||||
使用 Java 自带的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode():
|
||||
使用 Java 的 HashMap 等自带的哈希表实现时,只需要去实现 Key 类型的 hashCode() 函数即可。Java 规定 hashCode() 能够将键均匀分布于所有的 32 位整数,Java 中的 String、Integer 等对象的 hashCode() 都能实现这一点。以下展示了自定义类型如何实现 hashCode():
|
||||
|
||||
```java
|
||||
public class Transaction {
|
||||
|
||||
private final String who;
|
||||
private final Date when;
|
||||
private final double amount;
|
||||
@ -2039,17 +2038,17 @@ public class Transaction {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 基于拉链法的散列表
|
||||
### 2. 拉链法
|
||||
|
||||
拉链法使用链表来存储 hash 值相同的键,从而解决冲突。
|
||||
|
||||
查找需要分两步,首先查找 Key 所在的链表,然后在链表中顺序查找。
|
||||
|
||||
对于 N 个键,M 条链表 (N>M),如果 hash 函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
|
||||
对于 N 个键,M 条链表 (N>M),如果哈希函数能够满足均匀性的条件,每条链表的大小趋向于 N/M,因此未命中的查找和插入操作所需要的比较次数为 \~N/M。
|
||||
|
||||
<div align="center"> <img src="../pics//b4252c85-6fb0-4995-9a68-a1a5925fbdb1.png" width="300"/> </div><br>
|
||||
|
||||
### 3. 基于线性探测法的散列表
|
||||
### 3. 线性探测法
|
||||
|
||||
线性探测法使用空位来解决冲突,当冲突发生时,向前探测一个空位来存储冲突的键。
|
||||
|
||||
@ -2059,6 +2058,7 @@ public class Transaction {
|
||||
|
||||
```java
|
||||
public class LinearProbingHashST<Key, Value> implements UnorderedST<Key, Value> {
|
||||
|
||||
private int N = 0;
|
||||
private int M = 16;
|
||||
private Key[] keys;
|
||||
@ -2084,7 +2084,7 @@ public class LinearProbingHashST<Key, Value> implements UnorderedST<Key, Value>
|
||||
}
|
||||
```
|
||||
|
||||
**(一)查找**
|
||||
#### 3.1 查找
|
||||
|
||||
```java
|
||||
public Value get(Key key) {
|
||||
@ -2096,7 +2096,7 @@ public Value get(Key key) {
|
||||
}
|
||||
```
|
||||
|
||||
**(二)插入**
|
||||
#### 3.2 插入
|
||||
|
||||
```java
|
||||
public void put(Key key, Value value) {
|
||||
@ -2118,7 +2118,7 @@ private void putInternal(Key key, Value value) {
|
||||
}
|
||||
```
|
||||
|
||||
**(三)删除**
|
||||
#### 3.3 删除
|
||||
|
||||
删除操作应当将右侧所有相邻的键值对重新插入散列表中。
|
||||
|
||||
@ -2151,7 +2151,7 @@ public void delete(Key key) {
|
||||
}
|
||||
```
|
||||
|
||||
**(四)调整数组大小**
|
||||
#### 3.5 调整数组大小
|
||||
|
||||
线性探测法的成本取决于连续条目的长度,连续条目也叫聚簇。当聚簇很长时,在查找和插入时也需要进行很多次探测。例如下图中 2\~5 位置就是一个聚簇。
|
||||
|
||||
@ -2235,15 +2235,15 @@ public class SparseVector {
|
||||
|
||||
这是一个经典的递归问题,分为三步求解:
|
||||
|
||||
- 将 n-1 个圆盘从 from -> buffer
|
||||
① 将 n-1 个圆盘从 from -> buffer
|
||||
|
||||
<div align="center"> <img src="../pics//8587132a-021d-4f1f-a8ec-5a9daa7157a7.png" width="300"/> </div><br>
|
||||
|
||||
- 将 1 个圆盘从 from -> to
|
||||
② 将 1 个圆盘从 from -> to
|
||||
|
||||
<div align="center"> <img src="../pics//2861e923-4862-4526-881c-15529279d49c.png" width="300"/> </div><br>
|
||||
|
||||
- 将 n-1 个圆盘从 buffer -> to
|
||||
③ 将 n-1 个圆盘从 buffer -> to
|
||||
|
||||
<div align="center"> <img src="../pics//1c4e8185-8153-46b6-bd5a-288b15feeae6.png" width="300"/> </div><br>
|
||||
|
||||
@ -2281,9 +2281,9 @@ from H1 to H3
|
||||
|
||||
## 哈夫曼编码
|
||||
|
||||
哈夫曼编码根据数据出现的频率对数据进行编码,从而压缩原始数据。
|
||||
根据数据出现的频率对数据进行编码,从而压缩原始数据。
|
||||
|
||||
例如对于文本文件,其中各种字符出现的次数如下:
|
||||
例如对于一个文本文件,其中各种字符出现的次数如下:
|
||||
|
||||
- a : 10
|
||||
- b : 20
|
||||
@ -2294,7 +2294,7 @@ from H1 to H3
|
||||
|
||||
首先生成一颗哈夫曼树,每次生成过程中选取频率最少的两个节点,生成一个新节点作为它们的父节点,并且新节点的频率为两个节点的和。选取频率最少的原因是,生成过程使得先选取的节点在树的最底层,那么需要的编码长度更长,频率更少可以使得总编码长度更少。
|
||||
|
||||
生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到根节点,根节点代表的字符的编码就是这个路径编码。
|
||||
生成编码时,从根节点出发,向左遍历则添加二进制位 0,向右则添加二进制位 1,直到遍历到叶子节点,叶子节点代表的字符的编码就是这个路径编码。
|
||||
|
||||
<div align="center"> <img src="../pics//3ff4f00a-2321-48fd-95f4-ce6001332151.png" width="400"/> </div><br>
|
||||
|
||||
|
Reference in New Issue
Block a user