## 排序

### 默认升序

### 冒泡排序(Bubble Sort)

#### 也叫起泡排序

#### 执行流程

- 从头开始比较每一对相邻元素, 如果第1个比第2个大, 就交换他们的位置. 执行完一轮后, 最末尾那个元素就是最大的元素

- 忽略步骤一中找到的最大元素, 重复执行步骤一

```
public static void bubbleSort(Integer[] array) {
for (int end = array.length - 1; end > 0; end--) {
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
}
}
}
}
```

#### 优化一: 如果序列已经完全有序, 可以提前终止

```
/**
* 优化一: 已排序, 可提前终止排序
* @param array
*/
public static void bubbleSort1(Integer[] array) {
for (int end = array.length - 1; end > 0; end--) {
boolean sorted = true;// 默认排序
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
sorted = false;
}
}
if (sorted) break;
}
}

```

#### 优化二: 记录最后一次交换元素的位置, 减少排序次数

```
/**
* 优化二: 尾部局部排序, 记录最后一次交换的位置, 减少排序次数
* @param array
*/
public static void bubbleSort2(Integer[] array) {
for (int end = array.length - 1; end > 0; end--) {
int sortedIndex = 1;// 记录最后一次交换的位置
for (int begin = 1; begin <= end; begin++) {
if (array[begin] < array[begin - 1]) {
int tmp = array[begin];
array[begin] = array[begin - 1];
array[begin - 1] = tmp;
sortedIndex = begin;
}
}
end = sortedIndex;
}
}
```

#### 最坏、平均时间复杂度:O(n2)

#### 最好时间复杂度: O(N)

#### 空间复杂度: O(1)

#### 稳定
---
### 选择排序(Selection Sort)
#### 执行流程
- 从序列中找出最大的那个元素, 然后与最末尾的元素交换. 执行完一轮后, 最末尾的那个元素就是最大的元素
- 忽略步骤一中找到的最大元素, 重复执行步骤一
```
protected void sort() {
for (int end = array.length - 1; end > 0; end--) {
int maxIndex = 0;
for (int begin = 1; begin <= end; begin++) {
if (cmp(maxIndex, begin) < 0) {
maxIndex = begin;
}
}
swap(maxIndex, end);
}
}

```

#### 选择排序的交换次数要远远少于冒泡排序, 平均性能优于冒泡排序
#### 最坏、最好、平均时间复杂度:O(n2)
#### 空间复杂度: O(1)
#### 不稳定
---
### 堆排序(Bubble Sort)
#### 堆排序可以认为是对选择排序的一种优化
#### 执行流程
- 对序列进行原地建堆
- 重复执行以下操作, 直到堆的元素数量为1
- 交换堆顶元素与尾元素
- 堆的元素数量减1
- 对0索引进行siftDown操作
```
private int heapSize;
@Override
protected void sort() {
heapSize = array.length;
// 原地建堆
for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
while (heapSize > 1) {
// 堆顶元素与尾元素交换, 堆大小减1
swap(0, --heapSize);
// 对索引为0位置进行下滤
siftDown(0);
}
}
private void siftDown(int index) {
Integer element = array[index];
int half = heapSize >> 1;
while (index < half) {
int childIndex = (index << 1) + 1;
Integer child = array[childIndex];
int rightIndex = childIndex + 1;
if (rightIndex < heapSize && cmp(rightIndex, childIndex) > 0) {
child = array[childIndex = rightIndex];
}
if (cmpElements(element, child) >= 0) break;
array[index] = child;
index = childIndex;
}
array[index] = element;
}
```#### 最坏、最好、平均时间复杂度:O(nlogn)
#### 空间复杂度: O(1)
#### 不稳定
### 插入排序(Insertion Sort)
#### 插入排序非常类似我们在玩扑克牌时的排序
#### 执行流程
- 在执行过程中, 插入排序会将序列分成头部已排序和尾部待排序的部分
- 从头开始扫描每一个元素, 每当扫描到一个元素, 就将它插入到合适的位置, 使得头部数据依然保持有序
#### 实现一
```
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
int cur = begin;
while (cur > 0 && cmp(array[cur], array[cur - 1]) < 0) {
swap(cur, cur - 1);
cur--;
}
}
}
```
#### 优化一, 减少交换次数
```
/**
* 优化一, 减少交换次数
*/
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
E element = array[begin];
int cur = begin;
while (cur > 0 && cmp(element, array[cur - 1]) < 0) {
array[cur] = array[cur - 1];
cur--;
}
array[cur] = element;
}
}
```
#### 插入排序还可以通过二分搜索进行优化
#### 二分搜索: 在有序数组中查找元素
#### 查找元素索引
```
/**
* 查询元素的索引
*
* @param array
* @param element
* @return
*/
public static int indexOf(Integer[] array, Integer element) {
if (array == null || array.length == 0) return -1;
int begin = 0;
int end = array.length;
while (begin < end) {
int mid = (begin + end) >> 1;
if (element < array[mid]) {
end = mid;
} else if (element > array[mid]) {
begin = mid + 1;
} else {
return mid;
}
}
return -1;
}
```
#### 查询元素应该插入的索引位置
```
/**
* 查询元素element应该被插入的索引位置
* 该索引对应第一个比element大的元素
* [1, 2, 3, 3, 3, 4, 5] 插入3时, 应该返回index = 5
*
* @param array
* @param element
* @return
*/
public static int search(Integer[] array, Integer element) {
if (array == null || array.length == 0) return -1;
int begin = 0;
int end = array.length;
while (begin < end) {
int mid = (begin + end) >> 1;
if (element < array[mid]) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
```
#### 优化二, 使用二分查找
```
/**
* 优化二, 使用二分查找
*/
@Override
protected void sort() {
for (int begin = 1; begin < array.length; begin++) {
E insertElement = array[begin];
int insertIndex = findInsertIndex(begin);
// 从已排序末尾到插入位置, 往后挪动数组元素
for (int j = begin; j > insertIndex; j--) {
array[j] = array[j - 1];
}
array[insertIndex] = insertElement;
}
}
private int findInsertIndex(int index) {
E element = array[index];
// 查询插入位置
int begin = 0;
int end = index;
while (begin < end) {
int mid = (begin + end) >> 1;
if (cmp(element, array[mid]) < 0) {
end = mid;
} else {
begin = mid + 1;
}
}
return begin;
}
```
#### 最好时间复杂度: O(n)
#### 最坏, 平均时间复杂度: O(n2)
#### 空间复杂度: O(1)
#### 稳定
---
### 归并排序(Merge Sort)
####执行流程
- 不断地将当前序列平均分割成2个子序列, 直到不能再分割(序列中只剩一个元素)
- 不断地将2个子序列合并成一个有序序列, 直到最终只剩下一个有序序列
```
E[] leftArray;
@Override
protected void sort() {
leftArray = (E[])new Comparable[array.length >> 1];
sort(0, array.length);
}
private void sort(int begin, int end) {
if (end - begin < 2) return;
int mid = (begin + end) >> 1;
sort(begin, mid);
sort(mid, end);
merge(begin, mid, end);
}
private void merge(int begin, int mid, int end) {
int li = 0, le = mid - begin;
int ri = mid, re = end;
int ai = begin;
for (int i = li; i < le; i++) {
leftArray[i] = array[begin + i];
}
while (li < le) {
if (ri < re && cmp(array[ri], leftArray[li]) < 0) {
array[ai++] = array[ri++];
} else {
array[ai++] = leftArray[li++];
}
}
}
```
#### 最好, 最坏, 平均时间复杂度: O(nlogn)
#### 空间复杂度: O(n)
#### 稳定
---
### 快速排序(Quick Sort)
#### 执行流程
- 从序列中选择一个轴点元素(pivot)
- 利用pivot将序列分割成2个子序列
- 将小于pivot的元素放在pivot前面(左侧)
- 将大于pivot的元素放在pivot后面(右侧)
- 等于pivot的元素放哪边都可以
-
- 对子序列进行1, 2操作, 直到不能再分割(子序列中只剩下一个元素)
```
@Override
protected void sort() {
sort(0, array.length);
}
/**
* 对[begin, end)内的元素进行快速排序
* @param begin
* @param end
*/
private void sort(int begin, int end) {
if (end - begin < 2) return;
// 确定轴点
int mid = pivotIndex(begin, end);
// 对左右两边进行快速排序
sort(begin, mid);
sort(mid + 1, end);
}
/**
* 将轴点元素(首元素)放在合适位置后返回轴点索引
* @param begin
* @param end
* @return
*/
private int pivotIndex(int begin, int end) {
swap(begin, begin + (int)(Math.random() * (end - begin)));
// 备份轴点元素
E pivot = array[begin];
end--;
while (begin < end) {
while (begin < end) {
if (cmp(pivot, array[end]) < 0) {
end--;
} else {
array[begin++] = array[end];
break;
}
}
while (begin < end) {
if (cmp(pivot, array[begin]) > 0) {
begin++;
} else {
array[end--] = array[begin];
break;
}
}
}
array[begin] = pivot;
return begin;
}
```
#### 最好, 平均时间复杂度: O(nlogn)
#### 最坏时间复杂度: O(n2)
#### 空间复杂度: O(logn)
#### 不稳定
---
### 希尔排序(Shell Sort)
#### 希尔排序把序列看作是一个矩阵, 分成m列, 逐列进行排序
- m从某个整数逐渐减为1
- 当m为1时, 整个序列将完全有序
#### 因此, 希尔排序也被称为递减增量排序
#### 矩阵的列数取决于步长序列
- 不同的步长序列, 执行效率也不同
```
@Override
protected void sort() {
// 生成步长序列
List shellStepSequence = shellStepSequence();
// 对每一个步长进行排序
for (Integer step : shellStepSequence) {
sort(step);
}
}
/**
* 对步长为step的数组进行插入排序
* @param step
*/
private void sort(int step) {
for (int col = 0; col < step; col++) {
for (int begin = col + step; begin < array.length; begin += step) {
E element = array[begin];
int cur = begin;
while (cur > col && cmp(element, array[cur - step]) < 0) {
array[cur] = array[cur - step];
cur -= step;
}
array[cur] = element;
}
}
}
/**
* 生成步长序列
* @param count
* @return
*/
private List shellStepSequence() {
List stepSequence = new ArrayList<>();
int step = array.length;
while ((step >>= 1) > 0) {
stepSequence.add(step);
}
return stepSequence;
}
```
#### 最好时间复杂度: O(n)
#### 最坏时间复杂度: O(n4/3) ~ O(n2)
#### 平均时间复杂度取决于步长序列
#### 空间复杂度: O(1)
#### 不稳定
---
### 计数排序(Counting Sort)
#### 之前学的排序都是基于比较的排序
- 平均时间复杂度目前最低是O(nlogn)
- 计数排序, 桶排序, 基数排序都不是基于比较的排序
- 它们是典型的用空间换时间, 在某些时候, 平均时间复杂度可以比O(nlogn)更低
#### 适合对一定范围内的整数进行排序
#### 计数排序的核心思想: 统计每个整数在序列中出现的次数, 进而推导出每个整数在有序序列中的索引
#### 简单实现
```
@Override
protected void sort() {
if (array == null || array.length < 2)
return;
// 找到最大值
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
}
// 构建计数数组
int[] counts = new int[max + 1];
for (int i = 0; i < array.length; i++) {
counts[array[i]]++;
}
// 根据计数数组进行排序
int index = 0;
for (int i = 0; i < counts.length; i++) {
while (counts[i]-- > 0) {
array[index++] = i;
}
}
}
```
#### 上述版本的实现存在以下问题
- 不能排序负整数
- 浪费内存空间
- 不稳定
#### 优化实现
- 计数数组的大小由最大值和最小值共同实现
- 计数数组的索引不等于排序数组的元素
- 计数数组元素存放的是元素的出现次数 + 前面的元素出现次数之和
- 该实现是稳定的
```
/**
* 优化
*
* @param array
*/
protected void sort() {
// 找到最大值, 最小值
int min = array[0];
int max = array[0];
for (int i = 0; i < array.length; i++) {
if (array[i] < min) {
min = array[i];
}
if (array[i] > max) {
max = array[i];
}
}
// 构建计数数组
int[] counts = new int[max - min + 1];
for (int i = 0; i < array.length; i++) {
counts[array[i] - min]++;
}
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
// 排序
Integer[] newArray = new Integer[array.length];
for (int i = array.length - 1; i >= 0; i--) {
newArray[--counts[array[i] - min]] = array[i];
}
for (int i = 0; i < newArray.length; i++) {
array[i] = newArray[i];
}
}
```
#### 最好, 最坏, 平均时间复杂度: O(n + k)
#### 空间复杂度: O(n + k)
#### k是整数的取值范围
#### 稳定
---
### 基数排序(Radix Sort)
#### 基数排序非常适合用于整数排序(尤其是非负整数)
#### 执行流程: 依次对个位数, 十位数, 百位数... 进行排序
#### 如果都是十进制数字, 基数的取值时0-9, 因此对基数排序时可考虑使用计数排序
```
@Override
protected void sort() {
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) max = array[i];
}
for (int divider = 1; divider <= max; divider *= 10) {
countingSort(divider);
}
}
private void countingSort(int divider) {
int[] counts = new int[10];
for (int i = 0; i < array.length; i++) {
counts[array[i] / divider % 10]++;
}
for (int i = 1; i < counts.length; i++) {
counts[i] += counts[i - 1];
}
Integer[] newArray = new Integer[array.length];
for (int i = array.length - 1; i >= 0 ; i--) {
newArray[--counts[array[i] / divider % 10]] = array[i];
}
for (int i = 0; i < newArray.length; i++) {
array[i] = newArray[i];
}
}
```
#### 最好, 最坏, 平均时间复杂度: O(d * (n + k))
#### 空间复杂度: O(n + k)
#### d是最大值的位数, k是进制
#### 稳定
---
### 桶排序(Bucket Sort)
#### 多种实现方式, 仅作为了解
#### 执行流程
- 创建一定数量的桶(数组, 链表都可以)
- 按照一定的规则(不同类型的数组, 规则不同), 将序列中的元素均匀分配到对应的桶
- 分别对每个桶进行单独的排序
- 将所有非空桶的元素合并成有序序列
#### 时间复杂度: O(n + k)
#### 空间复杂度: O(n + m), m是桶数量
#### 稳定
---
## 并查集(Union Find)
### 假设有n个村庄, 有些村庄之间有连接的路, 有些村庄之间并没有连接的路, 要求设计几个数据结构, 能够快速执行2个操作
- 查询2个村庄之间是否有连接的路
- 连接两个村庄
- 此时, 使用数组, 链表, 平衡二叉树, 集合, 查询, 连接的时间复杂度都是: O(n)
### 并查集有2个核心操作
- 查找(Find): 查找元素所在的集合
- 合并(Union): 将两个元素所在的集合合并为一个集合
### 并查集有两种实现思路, QuickFind和QuickUnion, 一般使用后者
### QuickFind
```
@Override
public int find(int v) {
rangeCheck(v);
return parents[v];
}
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
for (int i = 0; i < parents.length; i++) {
if (parents[i] == p1) {
parents[i] = p2;
}
}
}
```
#### Find: O(1)
#### Union: O(n)
### QuickUnion
```
@Override
public int find(int v) {
rangeCheck(v);
while (v != parents[v]) {
v = parents[v];
}
return v;
}
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
parents[p1] = p2;
}
```
#### Find: O(logn)
#### Union: O(logn)
### QuickUnion优化
#### 在union的过程中, 可能会出现树不平衡的情况, 甚至退化成链表
- 优化一: 基于size进行优化, 元素少的树嫁接到元素多的树
```
private int[] sizes;
public QuickUnionSize(int capacity) {
super(capacity);
sizes = new int[capacity];
for (int i = 0; i < sizes.length; i++) {
sizes[i] = 1;
}
}
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
if (sizes[p1] < sizes[p2]) {
parents[p1] = p2;
sizes[p2] += sizes[p1];
} else {
parents[p2] = p1;
sizes[p1] += sizes[p2];
}
}
```
- 有时元素多的树高度低, 元素少的树高度高, 嫁接后也会存在不平衡的问题
- 优化二: 基于rank(高度)进行优化, 矮的树嫁接到高的树
```
private int[] ranks;
public QuickUnionRank(int capacity) {
super(capacity);
ranks = new int[capacity];
for (int i = 0; i < ranks.length; i++) {
ranks[i] = 1;
}
}
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
if (ranks[p1] < ranks[p2]) {
parents[p1] = p2;
} else if (ranks[p1] > ranks[p2]) {
parents[p2] = p1;
} else {
parents[p1] = p2;
ranks[p2]++;
}
}
```
#### 虽然优化了union操作, 但随着union次数变多, 树的高度会越来越高, 导致find操作变慢, 尤其是底层节点
- 路径压缩(Path Compression): 在find时使路径上所有节点都指向根节点, 降低树的高度
@Override
public int find(int v) {
rangeCheck(v);
if (v != parents[v]) {
parents[v] = find(parents[v]);
}
return parents[v];
}
- 路径压缩时操作路径上的所有节点, 实现成本比较高, 还有两种更优的做法, 不但能降低树高, 实现成本也比路径压缩低, 两种效率差不多, 但都比路径压缩好
- 路径分裂(Path Spliting): 使路径上的每个节点都指向其祖父节点
@Override
public int find(int v) {
rangeCheck(v);
while (v != parents[v]) {
int p = parents[v];
parents[v] = parents[parents[v]];
v = p;
}
return v;
}
- 路径减半(Path halving): 使路径上每隔一个节点就指向其祖父节点
@Override
public int find(int v) {
rangeCheck(v);
while (v != parents[v]) {
parents[v] = parents[parents[v]];
v = parents[v];
}
return v;
}
### 总结
- 使用路径压缩, 分裂, 减半 + 基于rank或size优化, 可以确保每个操作的均摊时间复杂度为O(a(n)), a(n) < 5
- 建议搭配
- QuickUnion
- rank优化
- Path Halving或Path Spliting
## 图
#### 图由顶点(vertex)和边(edge)组成, 通常表示为G=(V, E)
#### 顶点的定义
private static class Vertex {
V value;
Set> inEdges = new HashSet<>();
Set> outEdges = new HashSet<>();
public Vertex(V value) {
this.value = value;
}
@Override
public int hashCode() {
return value == null ? 0 : value.hashCode();
}
@Override
public boolean equals(Object obj) {
Vertex vertex = (Vertex)obj;
return Objects.equals(value, vertex.value);
}
}
#### 边的定义
```
private static class Edge {
Vertex from;
Vertex to;
E weight;
public Edge(Vertex from, Vertex to) {
this.from = from;
this.to = to;
}
@Override
public int hashCode() {
int hashCode = from.hashCode();
hashCode = hashCode * 31 + to.hashCode();
return hashCode;
}
@Override
public boolean equals(Object obj) {
Edge edge = (Edge)obj;
return Objects.equals(to, edge.to) && Objects.equals(from, edge.from);
}
}
```
#### 添加顶点
- 使用HashMap来存储图中的顶点
- 先判断Map中是否存在顶点, 没有则创建后放入map
```
@Override
public void addVertex(V v) {
if (vertices.containsKey(v)) return;
vertices.put(v, new Vertex<>(v));
}
```
#### 添加边
- 获取起点和终点, 没有则创建
- 将原来的边直接删除
- 插入新的边, 使用HashSet存放图中的边
```
@Override
public void addEdge(V from, V to, E weight) {
Vertex fromVertex = vertices.get(from);
if (fromVertex == null) {
fromVertex = new Vertex<>(from);
vertices.put(from, fromVertex);
}
Vertex toVertex = vertices.get(to);
if (toVertex == null) {
toVertex = new Vertex<>(to);
vertices.put(to, toVertex);
}
Edge edge = new Edge<>(fromVertex, toVertex);
if (fromVertex.outEdges.remove(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
edge.weight = weight;
fromVertex.outEdges.add(edge);
toVertex.inEdges.add(edge);
edges.add(edge);
}
```
#### 删除边
- 获取起点和终点, 其中一个为空则直接返回
- 从起点, 终点和HashSet中删除边
```
@Override
public void removeEdge(V from, V to) {
Vertex fromVertex = vertices.get(from);
if (fromVertex == null) return;
Vertex toVertex = vertices.get(to);
if (toVertex == null) return;
Edge edge = new Edge<>(fromVertex, toVertex);
if (fromVertex.outEdges.remove(edge)) {
toVertex.inEdges.remove(edge);
edges.remove(edge);
}
}
```
### 删除顶点
- 获取顶点, 没有则直接返回
- 使用迭代器迭代顶点inEdges和outEdges
- 在迭代器中删除
- 在终点或起点中删除
- 在HashSet中删除
```
public void removeVertex(V v) {
Vertex vertex = vertices.remove(v);
if (vertex == null) return;
for (Iterator> it = vertex.inEdges.iterator(); it.hasNext();) {
Edge edge = it.next();
edge.from.outEdges.remove(edge);
it.remove();
edges.remove(edge);
}
for (Iterator> it = vertex.outEdges.iterator(); it.hasNext();) {
Edge edge = it.next();
edge.to.inEdges.remove(edge);
it.remove();
edges.remove(edge);
}
}
```
### 图的遍历
#### 图的遍历需要一个入口
#### 广度优先搜索(Breadth First Search), 简称BFS
- 之前所学的二叉树层序遍历就是一种广度优先搜索
- 一层一层访问, 将直接能够访问的节点作为一层
- 使用队列
```
public void bfs(V v, Visitor visitor) {
if (visitor == null) return;
Vertex beginVertex = vertices.get(v);
if (beginVertex == null) return;
Queue> queue = new LinkedList<>();
Set> visitedVertices = new HashSet<>();
queue.offer(beginVertex);
visitedVertices.add(beginVertex);
while (!queue.isEmpty()) {
Vertex vertex = queue.poll();
if (visitor.visit(vertex.value)) return;
for (Edge edge : vertex.outEdges) {
if (visitedVertices.contains(edge.to)) continue;
queue.offer(edge.to);
visitedVertices.add(edge.to);
}
}
}
```
#### 深度优先搜索(Depth First Search), 简称DFS
- 之前所学的二叉树前序遍历就是一种深度优先搜索
- 从入口一直往深处访问, 直到不能再深时回到上一节点寻找其他路径
- 递归实现
```
public void dfs1(V v) {
Vertex vertex = vertices.get(v);
if (vertex == null) return;
Set> visitedVertices = new HashSet<>();
dfs1(vertex, visitedVertices);
}
private void dfs1(Vertex vertex, Set> visitedVertices) {
System.out.println(vertex);
visitedVertices.add(vertex);
for (Edge edge : vertex.outEdges) {
if (visitedVertices.contains(edge.to)) continue;
dfs1(edge.to, visitedVertices);
}
}
```
- 非递归实现(栈)
- 入口元素入栈访问
- 循环: 取出栈顶元素, 遍历outEdge, 将from, to分别入栈(注意去重), 入栈后马上break
```
public void dfs(V v) {
Vertex beginVertex = vertices.get(v);
if (beginVertex == null) return;
Stack> stack = new Stack<>();
Set> visitedVertices = new HashSet<>();
stack.push(beginVertex);
System.out.println(beginVertex);
visitedVertices.add(beginVertex);
while (!stack.isEmpty()) {
Vertex vertex = stack.pop();
for (Edge edge : vertex.outEdges) {
if (visitedVertices.contains(edge.to)) continue;
stack.push(edge.from);
stack.push(edge.to);
System.out.println(edge.to);
visitedVertices.add(edge.to);
break;
}
}
}
```
### AOV网(Activity On Vertex Network)
#### 一项大的工程常被分为多个小的子工程
- 子工程之间可能存在一定的先后顺序, 即某些子工程必须在其他的一些子工程完成后才能开始
#### 在现代化管理中, 人们常用有向图来描述和分析一项工程的计划和实施过程, 子工程被称为活动
- 以顶点表示活动, 有向边表示活动之间的先后关系, 这样的图简称为AOV网
#### 标准的AOV网必须是一个有向无环图
### 拓扑排序(Topological Sotr)
#### 前驱活动: 有向边起点的活动称为终点的前驱活动
- 只有当一个活动的前驱活动全部完成后, 这个活动才能进行
#### 拓扑排序就是将AOV网中所有活动排成一个序列, 使得每个活动的前驱都排在该活动的前面
#### 实现思路
- 使用卡恩算法
- 假设L是存放拓扑排序结果的列表
- 把所有入度为0的顶点放入L中, 然后把这些 顶点从图中去掉
- 重复上面一步操作, 直到找不到入度为0的顶点
- 如果结束后, L的元素个数与顶点树相同, 说明排序完成, 少于则说明原图中存在环, 无法进行排序
```
public List topologicalSort() {
List list = new ArrayList<>();
Queue> queue = new LinkedList<>();
Map, Integer> inSizes = new HashMap<>();
vertices.forEach((V value, Vertex vertex) -> {
int inSize = vertex.inEdges.size();
if (inSize == 0) {
queue.offer(vertex);
} else {
inSizes.put(vertex, inSize);
}
});
while (!queue.isEmpty()) {
Vertex vertex = queue.poll();
list.add(vertex.value);
for (Edge edge : vertex.outEdges) {
Integer inSize = inSizes.get(edge.to) - 1;
if (inSize == 0) {
queue.offer(edge.to);
} else {
inSizes.put(edge.to, inSize);
}
}
}
return list;
}
```
### 生成树(Spanning Tree)
- 生成树, 也称为支撑树
- 连通图的极小连通子图, 它含有图中全部的n个顶点, 恰好只有n - 1条边
### 最小生成树(Minimun Spanning Tree)
- 简称MST, 也称为最小权重生成树, 最小支撑树
- 是所有生成树中, 总权值最小的那棵
- 适用于有权的连通图
#### 最小生成树在许多领域都有重要的作用, 例如
- 要在n个城市之间铺设光缆, 使它们都可以通信
- 铺设光缆的费用很高,且各个城市之间因为距离不同等因素,铺设光缆的费用也不同
- 如何使铺设光缆的总费用最低?
#### 如果图的每一条边的权值都互不相同, 那么最小生成树将只有一个, 否则可能会有多个最小生成树
#### 求最小生成树的2个经典算法
- Prim
- Kruskal
#### 切分定理
- 切分: 把图中的节点分为两部分, 称为一个切分
- 横切边: 如果一个边的两个顶点, 分别属于切分的两部分, 这个边称为横切边
- 切分定理: 给定任意切分, 横切边中权值最小的边必然属于最小生成树
#### Prim
- 假设G=(V, E)是有权的连通图(无向), A是G中最小生成树的边集
- 算法从S={u0}(u0属于V), A={}开始, 重复执行下述操作, 直到S=V为止
- 找到切分C=(S, V-S)的最小横切边(u0, v0)并入合集A, 同时将v0
- 并入集合S
```
public Set> prim() {
Iterator> it = vertices.values().iterator();
if (!it.hasNext()) return null;
Vertex vertex = it.next();
Set> infos = new HashSet<>();
Set> addedVertices = new HashSet<>();
addedVertices.add(vertex);
MinHeap> heap = new MinHeap<>(vertex.outEdges, edgeComparator);
int verticesSize= vertices.size();
while (!heap.isEmpty() && addedVertices.size() < verticesSize) {
Edge edge = heap.remove();
if (addedVertices.contains(edge.to)) continue;
infos.add(edge.info());
addedVertices.add(edge.to);
heap.addAll(edge.to.outEdges);
}
return infos;
}
```
#### Kruskal
- 按照边的权重顺序(从小到大)将边加入生成树中, 直到生成树中含有V - 1条边为止(V是顶点数量)
- 若加入该边会与生成树形成环, 则不加入该边(使用并查集)
- 从第3条边开始, 可能会与生成树形成环
```
public Set> kruskal() {
int edgeSize = verticesSize() - 1;
if (edgeSize == -1) return null;
Set> infos = new HashSet<>();
MinHeap> heap = new MinHeap<>(edges, edgeComparator);
GenericUnionFind> uf = new GenericUnionFind<>();
while (!heap.isEmpty() && infos.size() < edgeSize) {
Edge edge = heap.remove();
if (uf.isSame(edge.from, edge.to)) continue;
infos.add(edge.info());
uf.union(edge.from, edge.to);
}
return infos;
}
```
### 最短路径
- 最短路径是指两顶点之间权值之和最小的路径
- 有向图, 无向图均适用, 不能有负权环
- 最短路径的典型应用之一: 路径规划问题
#### 求解最短路径的3个经典算法
- 单源最短路径算法
- Dijkstra
- Bellman-Ford
- 多源最短路径算法
- Floyd
#### Dijkstra
- 属于单源最短路径算法, 用于计算一个顶点到其他所有顶点的最短路径
- 使用前提: 不能有负权边
- 时间复杂度: 可优化至O(ElogV), E是边数量, V是节点数量
- 原理和生活中某些现象一样
- 把每一个顶点想象成是一块小石头
- 每一条边想象成是一条绳子, 绳子两端系着石头
- 当提起某个石头A时, 其他石头会跟着离开, 某一个其他的石头x被提起前最后绷直的绳子就是A到x的最短路径
- 算法执行流程就是不断地提起石头, 然后对连接石头的边进行松弛操作
- 松弛操作: 更新2个顶点之间的最短路径
- 松弛的意义: 尝试找出更短的最短路径
- 简单实现
```
@Override
public Map shortestPath(V begin) {
Vertex vertex = vertices.get(begin);
if (vertex == null) return null;
Map selectedPaths = new HashMap<>();
Map, E> paths = new HashMap<>();
for (Edge edge : vertex.outEdges) {
paths.put(edge.to, edge.weight);
}
while (!paths.isEmpty()) {
Entry, E> minEntry = getMinPath(paths);
Vertex minVertex = minEntry.getKey();
selectedPaths.put(minVertex.value, minEntry.getValue());
paths.remove(minVertex);
// 对边进行松弛操作
for (Edge edge : minVertex.outEdges) {
if (selectedPaths.containsKey(edge.to.value)) continue;
E newWeight = weightManager.add(minEntry.getValue(), edge.weight);
E oldWeight = paths.get(edge.to);
if (oldWeight == null || weightManager.compare(newWeight, oldWeight) < 0) {
paths.put(edge.to, newWeight);
}
}
}
selectedPaths.remove(begin);
return selectedPaths;
}
private Entry, E> getMinPath(Map, E> paths) {
Iterator, E>> it = paths.entrySet().iterator();
Entry, E> minEntry = it.next();
while (it.hasNext()) {
Entry, E> entry = it.next();
if (weightManager.compare(entry.getValue(), minEntry.getValue()) < 0) {
minEntry = entry;
}
}
return minEntry;
}
```
- 改造实现(能够返回路径信息)
```
public static class PathInfo {
protected E weight;
protected List> edgeInfos = new ArrayList<>();
public E getWeight() {
return weight;
}
public void setWeight(E weight) {
this.weight = weight;
}
public List> getEdgeInfos() {
return edgeInfos;
}
public void setEdgeInfos(List> edgeInfos) {
this.edgeInfos = edgeInfos;
}
@Override
public String toString() {
return "PathInfo [weight=" + weight + ", edgeInfos=" + edgeInfos + "]";
}
}
private Map> dijkstra(V begin) {
Vertex vertex = vertices.get(begin);
if (vertex == null) return null;
Map> selectedPaths = new HashMap<>();
Map, PathInfo> paths = new HashMap<>();
// 初始化paths
for (Edge edge : vertex.outEdges) {
PathInfo pathInfo = new PathInfo<>();
pathInfo.weight = edge.weight;
pathInfo.edgeInfos.add(edge.info());
paths.put(edge.to, pathInfo);
}
while (!paths.isEmpty()) {
Entry, PathInfo> minEntry = getMinPath(paths);
Vertex minVertex = minEntry.getKey();
PathInfo minPath = minEntry.getValue();
selectedPaths.put(minVertex.value, minPath);
paths.remove(minVertex);
for (Edge edge : minVertex.outEdges) {
if (selectedPaths.containsKey(edge.to.value)) continue;
relaxForDijkstra(edge, minPath, paths);
}
}
selectedPaths.remove(begin);
return selectedPaths;
}
/**
* 松弛操作
* @param edge 被松弛的边
* @param minPath 边前面的最短路径信息
* @param paths未加入selectedPaths的路径信息
*/
private void relaxForDijkstra(Edge edge, PathInfo minPath, Map, PathInfo> paths) {
E newWeight = weightManager.add(minPath.weight, edge.weight);
PathInfo oldPath = paths.get(edge.to);
if (oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) return;
if (oldPath == null) {
oldPath = new PathInfo<>();
paths.put(edge.to, oldPath);
} else {
oldPath.edgeInfos.clear();
}
oldPath.weight = newWeight;
oldPath.edgeInfos.addAll(minPath.edgeInfos);
oldPath.edgeInfos.add(edge.info());
}
```
#### Bellman-Ford
- Bellman-Ford也属于单源最短路径算法, 支持负权边, 还能检测负权环
- 原理: 对所有边进行V - 1次松弛操作(V是节点数量)
- 时间复杂度: O(EV), E是边数量, V是
```
private Map> bellmanFord(V begin) {
Vertex vertex = vertices.get(begin);
if (vertex == null) return null;
Map> selectedPaths = new HashMap<>();
PathInfo beginInfo = new PathInfo<>();
beginInfo.weight = weightManager.zero();
selectedPaths.put(begin, beginInfo);
int count = vertices.size() - 1;
for (int i = 0; i < count; i++) {
for (Edge edge : edges) {
PathInfo fromPath = selectedPaths.get(edge.from.value);
if (fromPath == null) continue;
relax(edge, fromPath, selectedPaths);
}
}
for (Edge edge : edges) {
PathInfo fromPath = selectedPaths.get(edge.from.value);
if (fromPath == null) continue;
if(relax(edge, fromPath, selectedPaths)) {
System.out.println("有负权环");
return null;
}
}
selectedPaths.remove(begin);
return selectedPaths;
}
private boolean relax(Edge edge, PathInfo fromPath, Map> paths) {
E newWeight = weightManager.add(fromPath.weight, edge.weight);
PathInfo oldPath = paths.get(edge.to.value);
if (oldPath != null && weightManager.compare(newWeight, oldPath.weight) >= 0) return false;
if (oldPath == null) {
oldPath = new PathInfo<>();
paths.put(edge.to.value, oldPath);
} else {
oldPath.edgeInfos.clear();
}
oldPath.weight = newWeight;
oldPath.edgeInfos.addAll(fromPath.edgeInfos);
oldPath.edgeInfos.add(edge.info());
return true;
}
```
#### Floyd
- Floyd属于多源最短路径算法, 能够求出任意2个顶点之间的最短路径, 支持负权边
- 时间复杂度(V3), 效率比执行V此Dijkstra算法要好
- 原理
- 从任意顶点i到任意顶点j的最短路径不外乎两种可能
- 直接从i到j
- 从i经过若干个顶点到j
- 假设dist(i, j)为顶点i到顶点j的最短路径的距离
- 对于每一个顶点k, 检查dist(i, k) + dist(k, j) < dist(i, j)是否成立
- 如果成立, 证明从i到k再到j的路径比i直接到j的路径更短, 设置 dist(i, j) = dist(i, k) + dist(k, j)
- 当遍历完所有的结点k, dist(i, j)中记录的便是i到j的最短路径的距离
```
private Map>> floyd() {
Map>> paths = new HashMap<>();
for (Edge edge : edges) {
Map> map = paths.get(edge.from.value);
if (map == null) {
map = new HashMap<>();
paths.put(edge.from.value, map);
}
PathInfo pathInfo = new PathInfo<>();
pathInfo.weight = edge.weight;
pathInfo.edgeInfos.add(edge.getInfo());
map.put(edge.to.value, pathInfo);
}
for (Vertex vertex2 : vertices.values()) {
for (Vertex vertex1 : vertices.values()) {
for (Vertex vertex3 : vertices.values()) {
if (vertex1.equals(vertex2) || vertex1.equals(vertex3) || vertex2.equals(vertex3)) continue;
PathInfo path12 = getPathInfo(vertex1, vertex2, paths);
if (path12 == null) continue;
PathInfo path23 = getPathInfo(vertex2, vertex3, paths);
if (path23 == null) continue;
PathInfo path13 = getPathInfo(vertex1, vertex3, paths);
E newWeight = weightManager.add(path12.weight, path23.weight);
if (path13 == null || weightManager.compare(newWeight, path13.weight) < 0) {
PathInfo pathInfo = new PathInfo<>();
pathInfo.weight = newWeight;
pathInfo.edgeInfos.addAll(path12.edgeInfos);
pathInfo.edgeInfos.addAll(path23.edgeInfos);
paths.get(vertex1.value).put(vertex3.value, pathInfo);
}
}
}
}
return paths;
}
```
---
## 递归(Recursion)
- 定义: 函数(方法)直接或间接调用自身. 它是一种编程技巧
### 递归的基本思想
- 拆解问题
- 把规模大的问题变成规模较小的同类型问题
- 规模较小的问题又不断变成规模更小的问题
- 规模小到一定程度可以直接得出它的解
- 求解
- 由最小规模问题的解得出较大规模问题的解
- 由较大规模问题的解不断得出规模更大问题的解
- 最后得出原来问题的解
- 凡是可以利用上述思想解决问题的, 都可以尝试使用递归
### 递归的使用套路
1. 明确函数的功能, 先不要去思考里面代码怎么写, 首先搞清楚这个函数是干嘛用的, 能完成什么功能
2. 明确原问题与子问题的关系
3. 明确递归基(边界条件), 递归的过程中, 子问题的规模在不断减小, 当小到一定程度时可以直接得出它的解
### 斐波那契数列问题
- 斐波那契数列: 1、 1、 2、 3、 5、 8、 13、 21、 34、 ……
#### 解决一: 直接使用递归
```
public static int fib0(int n) {
if (n <= 2) {
return 1;
}
return fib0(n - 1) + fib0(n - 2);
}
```
#### 优化一: 使用数组存放计算过的结果, 避免重复计算
```
public static int fib1(int n) {
if (n <= 2) return 1;
int[] array = new int[n + 1];
array[1] = 1;
array[2] = 1;
return fib1(n, array);
}
private static int fib1(int n, int[] array) {
if (array[n] == 0) {
array[n] = fib1(n - 1, array) + fib1(n - 2, array);
}
return array[n];
}
```
#### 优化二: 去除递归调用
```
public static int fib2(int n) {
if (n <= 2) return 1;
int[] array = new int[n + 1];
array[1] = array[2] = 1;
for (int i = 3; i <= n; i++) {
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}
```
#### 优化三: 只用到数组的前两个元素, 使用滚动数组
```
public static int fib3(int n) {
if (n <= 2) return 1;
int[] array = new int[]{1, 1};
for (int i = 3; i <= n; i++) {
array[i & 1] = array[(i - 1) & 1] + array[(i - 2) & 1];
}
return array[n & 1];
}
```
- n % 2 可以优化为 n & 1
#### 优化四: 直接使用两个变量
```
public static int fib4(int n) {
if (n <= 2) return 1;
int first = 1;
int second = 1;
for (int i = 3; i <= n; i++) {
second = first + second;
first = second - first;
}
return second;
}
```
### 上楼梯(跳台阶)
楼梯有n阶台阶, 上楼可以一步上1阶, 也可以一步上2阶, 走完n阶台阶共有多少种不同的走法?
- 假设n阶有f(n)种走法, 第一步有2种走法
- 如果上1阶, 那就还剩n - 1阶, 共有f(n - 1)种走法
- 如果上2阶, 那就还剩n - 2阶, 共有f(n - 2)种走法
- 所以f(n) = f(n - 1) + f(n - 2)
```
public static int climbStairs(int n) {
if (n <= 2) return n;
return climbStairs(n - 1) + climbStairs(n - 2);
}
public static int climbStairs1(int n) {
if (n <= 2) return n;
int first = 1;
int second = 2;
for (int i = 3; i <= n; i++) {
second = first + second;
first = second - first;
}
return second;
}
```
### 汉诺塔
编程实现把A的n个盘子移动到C柱上
- 每次只能移动一个盘子
- 大盘子只能放在小盘子下面
- 思路
- n = 1时, 直接将盘子从A移动到C
- n > 1时, 可以拆分成3大步骤
1. 将n - 1个盘子从A移动到B
2. 将n号盘子从A移动到c
3. 再将n - 1个盘子从B移动到C
```
public static void hanoi(int n, String p1, String p2, String p3) {
if (n == 1) {
move(n, p1, p3);
return;
}
hanoi(n - 1, p1, p3, p2);
move(n, p1, p3);
hanoi(n - 1, p2, p1, p3);
}
public static void move(int n, String from, String to) {
System.out.println("将" + n + "号盘子从" + from + "移动到" + to);
}
```
### 递归转非递归
- 递归调用的过程中, 会将每一次调用的参数, 局部变量都保存在对应的栈帧中
- 若递归调用深度较大, 会占用比较多的栈空间, 甚至会导致栈溢出
- 有些时候, 递归会存在大量的重复计算, 性能非常差
- 这时可以考虑将递归转为非递归(一定能转换)
- 递归转非递归的万能方法
- 自己维护一个栈, 来保存参数, 局部变量. 但是空间复杂度依然没有得到优化
- 在某些时候, 也可以重复使用一组相同的变量来保存每个栈帧的内容
### 尾调用(Tail Call)
- 尾调用: 一个函数的最后一个动作是调用函数
- 如果最后一个动作是调用自身, 称为尾递归
- 一些编译器能对尾调用进行优化, 以达到节省栈空间的目的
#### 尾调用优化
- 尾调用优化也叫做尾调用消除
- 如果当前栈帧上的局部变量等内容都不需要用了, 当前栈帧经过适当的改变后可以直接当作被尾调用的栈帧使用, 然后程序可以jump到被尾调用的函数代码
- 生成栈帧改变代码与jump的过程称作尾调用消除或尾调用优化
- 尾调用优化让位于尾位置的函数调用跟goto语句性能一样高
- 消除尾递归里的尾调用比消除一般的尾调用容易很多
- 比如JVM会消除尾递归里的尾调用, 但不会消除一般的尾调用(因为改变不了栈帧)
- 因此尾递归优化相对比较普遍, 平时的递归代码可以考虑尽量使用尾递归的形式
- 实例一: 阶乘
```
/**
* 尾递归优化
* @param n
* @return
*/
public static int factorial1(int n) {
return factorial1(n, 1);
}
private static int factorial1(int n, int result) {
if (n <= 1) return result;
return factorial1(n - 1, n * result);
}
```
- 实例二: 斐波那契数列
```
public static int fib5(int n) {
return fib5(n, 1, 1);
}
private static int fib5(int n, int first, int second) {
if (n <= 1) return first;
return fib5(n - 1, second, first + second);
}
```
---
## 回溯
### 回溯可以理解为: 通过选择不同的岔路口来通往目的地(找到想要的结果)
### 八皇后问题: 在8x8格的国际象棋上摆放八个皇后,使其不能互相攻击:任意两个皇后都不能处于同一行、同一列、同一斜线上, 求多少种有效的摆法
### 剪枝: 排除不必要的路
- 使用整形数组进行剪枝
```
private int[] queen;// 记录皇后摆放的位置. 索引是行, 值是列
private int ways;// 记录摆法
public static void main(String[] args) {
new Queen().placeNQueen(8);
}
public void placeNQueen(int n) {
if (n < 1) return;
queen = new int[n];
place(0);
}
public void place(int row) {
if (row == queen.length) {
ways++;
show();
return;
}
for (int col = 0; col < queen.length; col++) {
if (isValid(row, col)) {
queen[row] = col;// 摆放皇后
place(row + 1);// 进行下一行的摆放
}
}
}
/**
* 判断第row行第col列的摆放位置是否合法
* @param row
* @param col
* @return
*/
public boolean isValid(int row, int col) {
for (int i = 0; i < row; i++) {
if (queen[i] == col) return false;
if (row - i == Math.abs(col - queen[i])) return false;
}
return true;
}
```
- 使用布尔数组剪枝
```
private int[] queen;
private boolean[] cols;
private boolean[] leftTop;
private boolean[] rightTop;
private int ways;
public static void main(String[] args) {
new Queen2().placeNQueen(8);
}
public void placeNQueen(int n) {
if (n < 1) return;
queen = new int[n];
cols = new boolean[n];
leftTop = new boolean[(n << 1) - 1];
rightTop = new boolean[(n << 1) - 1];
place(0);
}
public void place(int row) {
if (row == cols.length) {
ways++;
show();System.out.println(ways);
return;
}
for (int col = 0; col < cols.length; col++) {
if (cols[col]) continue;
if (leftTop[row - col + cols.length - 1]) continue;
if (rightTop[row + col]) continue;
queen[row] = col;
cols[col] = true;
leftTop[row - col + cols.length - 1] = true;
rightTop[row + col] = true;
place(row + 1);
cols[col] = false;
leftTop[row - col + cols.length - 1] = false;
rightTop[row + col] = false;
}
}
```
- 使用位运算剪枝
```
private int[] queen;
private byte cols;
private short leftTop;
private short rightTop;
private int ways;
public static void main(String[] args) {
new Queen3().placeNQueen();
}
public void placeNQueen() {
queen = new int[8];
place(0);
}
public void place(int row) {
if (row == 8) {
ways++;
show();
System.out.println(ways);
return;
}
for (int col = 0; col < 8; col++) {
int cv = 1 << col;
if ((cv & cols) != 0) continue;
int lv = 1 << row - col + 7;
if ((lv & leftTop) != 0) continue;
int rv = 1 << row + col;
if ((rv & rightTop) != 0) continue;
queen[row] = col;
cols |= cv;
leftTop |= lv;
rightTop |= rv;
place(row + 1);
cols &= ~cv;
leftTop &= ~lv;
rightTop &= ~rv;
}
}
public void show() {
for (int i = 0; i < queen.length; i++) {
for (int j = 0; j < queen.length; j++) {
if (queen[i] == j) {
System.out.print("1 ");
} else {
System.out.print("0 ");
}
}
System.out.println();
}
}
```
---
## 贪心(Greedy)
### 贪心策略, 也称为贪婪策略
- 每一步都采取当前状态下最优的选择(局部最优解), 从而希望推导出全局最优的解
### 贪心的应用
- 哈夫曼树
- 最小生成树算法: Prim、Krukal
- 最短路径算法: Dijkstra
#### 练习一: 最优装载问题
- 有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值
- 海盗船的载重量为 W,每件古董的重量为 �i,海盗们该如何把尽可能多数量的古董装上海盗船?
- 比如 W 为 30, �i 分别为 3、 5、 4、 10、 7、 14、 2、 11
- 贪心策略:每一次都优先选择重量最小的古董
```
public static void selectAntique() {
int[] antiques = new int[]{3, 5, 4, 10, 7, 14, 2, 11};
int capacity = 30;
Arrays.sort(antiques);
int newCapacity = 0;
List selectedAntiques = new ArrayList<>();
for (int i = 0; i < antiques.length && (newCapacity += antiques[i]) <= capacity; i++) {
selectedAntiques.add(antiques[i]);
}
System.out.println(selectedAntiques);
}
```
#### 练习二: 零钱兑换
- 假设有 25 分、 10 分、 5 分、 1 分的硬币,现要找给客户 41 分的零钱,如何办到硬币个数最少?
- 贪心策略:每一次都优先选择面值最大的硬币
```
public static void changeMoney() {
int[] changes = new int[] { 25, 20, 5, 1 };
Arrays.sort(changes);
int money = 41;
int i = changes.length - 1;
int coins = 0;
while (money > 0) {
if (money >= changes[i]) {
money -= changes[i];
coins++;
} else {
i--;
}
}
System.out.println(coins);
}
```
- 如果面值是25 分、 20 分、 5 分、 1分
- 最终的解是 1 枚 25 分、 3 枚 5 分、 1 枚 1 分的硬币,共 5 枚硬币
- 实际上本题的最优解是: 2 枚 20 分、 1 枚 1 分的硬币,共 3 枚硬币
- 注意
- 贪心策略并不一定能得到全局最优解
- 因为一般没有测试所有可能的解, 容易过早做决定, 所以没法达到最佳解
- 贪图眼前局部的利益最大化, 看不到长远未来, 走一步看一步
- 优点: 简单、高效、不需要穷举所有可能,通常作为其他算法的辅助算法使用
- 缺点:鼠目寸光,不从整体上考虑其他可能,每次采取局部最优解,不会再回溯,因此很少情况会得到最优解
## 分治(Divide And Conquer)
### 分治,就是分而治之。它的一般步骤是
- 将原问题分解成若干规模较小的子问题(子问题和原问题结构一样,只是规模不一样)
- 子问题又不断分解成规模更小的子问题,直到不能再分解(直到可以轻易计算出子问题的解)
- 利用子问题的解推导出原问题的解
### 因此,分治策略非常适合使用递归
### 需要注意的是:子问题之间是相互独立
### 分治的应用
- 快速排序
- 归并排序
- Karatsuba(大数乘法)
### 主定理
- 分治策略通常遵守一种通用的模式
- 解决规模为n的问题,分解成a个规模为n/b的子问题,然后在O(n^d)时间内将子问题的解合起来
- 算法运行时间为: T(n) = aT(n/b) + O(n^d), a > 0, b > 1, d >= 0
- d > logba, T(n) = O(n^d)
- d = logba, T(n) = O(n^dlogn)
- d < logba, T(n) = O(n^logba)
#### 练习一: 最大连续子序列
- 给定一个长度为 n 的整数序列,求它的最大连续子序列和
- 比如 –2、 1、 –3、 4、 –1、 2、 1、 –5、 4 的最大连续子序列和是 4 + (–1) + 2 + 1 = 6
- 这道题也属于最大切片问题(最大区段, Greatest Slice)
- 子串、子数组、子区间必须是连续的,子序列是可以不连续的
- 分治解法
- 将序列[begin, end)均分分成[begin, mid), [mid, end)两个
- 最大连续子序列和S[i, j)有三种可能
- 存在于[begin, mid)中
- 存在于[mid, end)
- 一部分在[begin, mid), 一部分在[mid, end)
- 因此, 比较三者的大小关系就能求出该区间的最大连续子序列和
```
public int maxSubArray(int[] array) {
if (array == null || array.length == 0) return 0;
return maxSubArray(array, 0, array.length);
}
/**
* 求出[begin, end)之间的最大连续子序列之和
* @param begin
* @param end
* @return
*/
private int maxSubArray(int[] array, int begin, int end) {
if (end - begin < 2) return array[begin];
int mid = (begin + end) >> 1;
int max = 0;// 记录横跨[begin, mid)和[mid, end)的最大连续序列之和
int leftMax = Integer.MIN_VALUE;
int leftSum = 0;
for (int i = mid - 1; i >= begin; i--) {
leftSum += array[i];
leftMax = Math.max(leftMax, leftSum);
}
int rightMax = Integer.MIN_VALUE;
int rightSum = 0;
for (int i = mid; i < end; i++) {
rightSum += array[i];
rightMax = Math.max(rightMax, rightSum);
}
max = leftMax + rightMax;
return Math.max(max, Math.max(maxSubArray(array, begin, mid), maxSubArray(array, mid, end)));
}
```
## 动态规划(Dynamic Programming), 简称DP
### 求解最优化问题的一种常用策略
### 通常的使用套路(一步一步优化)
1. 暴力递归(自顶向下, 出现了重叠子问题)
2. 记忆化搜索(自顶向下)
3. 递推(自底向上)
### 动态规划的常用步骤
- 动态规划中的动态可以理解为是会变化的状态
- 步骤
1. 定义状态(状态是原问题、子问题的解), 比如定义dp(i)的含义
2. 设置初始状态(边界), 比如设置dp(0)的值
3. 确定状态转移方程, 比如确定dp(i)和dp(i - 1)的关系
### 可以用动态规划来解决的问题, 通常具备两个特点
- 最优子结构(最优化原理): 通过求解子问题的最优解, 可以获得原问题的最优解
- 无后效性
- 某阶段的状态一旦确定, 则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关)
- 在推导后面阶段的状态时, 只关心前面阶段的具体状态值, 不关心这个状态是怎么一步步推导出来的
#### 练习一: 零钱兑换
- 状态转移方程: 所以 dp(n) = min { dp(n – 25), dp(n – 20), dp(n – 5), dp(n – 1) } + 1
```
/**
* 暴力递归(自顶向下)
*/
public static int coin1(int n) {
if (n < 1) return Integer.MAX_VALUE;
if (n == 1 || n == 5 || n == 20 || n == 25) return 1;
int min1 = Math.min(coin1(n - 1), coin1(n - 5));
int min2 = Math.min(coin1(n - 20), coin1(n - 25));
return Math.min(min1, min2) + 1;
}
/**
* 记忆化搜索(自顶向下)
*/
public static int coin2(int n) {
int[] dp = new int[n + 1];
int[] faces = new int[]{1, 5, 20, 25};
for (int face : faces) {
if (face > n) break;
dp[face] = 1;
}
coin2(n , dp);
return coin2(n, dp);
}
private static int coin2(int n, int[] dp) {
if (n < 1) return Integer.MAX_VALUE;
if (dp[n] == 0) {
int min1 = Math.min(coin2(n - 1, dp), coin2(n - 5, dp));
int min2 = Math.min(coin2(n - 20, dp), coin2(n - 25, dp));
dp[n] = Math.min(min1, min2) + 1;
}
return dp[n];
}
/**
* 递推(自底向上)
*/
public static int coin3(int n) {
if (n < 1) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i < dp.length; i++) {
int min = dp[i - 1];
if (i >= 5) min = Math.min(dp[i - 5], min);
if (i >= 20) min = Math.min(dp[i - 20], min);
if (i >= 25) min = Math.min(dp[i - 25], min);
dp[i] = min + 1;
}
return dp[n];
}
/**
* 列出找钱方案
*/
public static int coin4(int n) {
if (n < 1) return -1;
int[] dp = new int[n + 1];
int[] faces = new int[n + 1];// 记录找n钱时, 最后一块拿的硬币
for (int i = 1; i < dp.length; i++) {
int min = dp[i - 1];
faces[i] = 1;
if (i >= 5 && dp[i - 5] < min) {
min = dp[i - 5];
faces[i] = 5;
}
if (i >= 20 && dp[i - 20] < min) {
min = dp[i - 20];
faces[i] = 20;
}
if (i >= 25 && dp[i - 25] < min) {
min = dp[i - 25];
faces[i] = 25;
}
dp[i] = min + 1;
}
print(n, faces);
return dp[n];
}
private static void print(int n, int[] faces) {
System.out.println("-----------"+n+"------------");
while (n > 0) {
System.out.print(faces[n] + " ");
n -= faces[n];
}
}
/**
* 通用实现
*/
public static int coins(int n, int[] faces) {
if (n < 1 || faces == null || faces.length == 0) return -1;
int[] dp = new int[n + 1];
for (int i = 1; i < dp.length; i++) {
int min = Integer.MAX_VALUE;
for (int face : faces) {
if (i < face || dp[i - face] == -1) continue;
min = Math.min(min, dp[i - face]);
}
if (min == Integer.MAX_VALUE) {
dp[i] = -1;
} else {
dp[i] = min + 1;
}
}
return dp[n];
}
```
#### 练习二: 最大连续子序列和
- 状态转移方程
- 如果 dp(i – 1) ≤ 0,那么 dp(i) = nums[i]
- 如果 dp(i – 1) > 0,那么 dp(i) = dp(i – 1) + nums[i]
```
public int maxSubArray(int[] array) {
if (array == null || array.length == 0) return 0;
int[] dp = new int[array.length];// dp[i]表示以array[i]元素为结尾的最大连续子序列和
dp[0] = array[0];
int max = dp[0];
for (int i = 1; i < array.length; i++) {
if (dp[i - 1] <= 0) {
dp[i] = array[i];
} else {
dp[i] = dp[i - 1] + array[i];
}
max = Math.max(max, dp[i]);
}
return max;
}
/**
* 空间复杂度优化: dp[i]的计算只考虑dp[i - 1]的值, 因此可以直接使用变量而不使用数组
*/
public int maxSubArray2(int[] array) {
if (array == null || array.length == 0) return 0;
int dp = array[0];// dp[i]表示以array[i]元素为结尾的最大连续子序列和
int max = dp;
for (int i = 1; i < array.length; i++) {
if (dp <= 0) {
dp = array[i];
} else {
dp += array[i];
}
max = Math.max(max, dp);
}
return max;
}
```
#### 练习三: 最长上升子序列
```
/*
* 给定一个无序的整数序列,求出它最长上升子序列的长度(要求严格上升)
* 比如 [10, 2, 2, 5, 1, 7, 101, 18] 的最长上升子序列是 [2, 5, 7, 101]、 [2, 5, 7, 18],长度是 4
*/
public static int lis(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int max = dp[0] = 1;
for (int i = 1; i < dp.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[j], dp[i]);
}
}
dp[i] += 1;
max = Math.max(dp[i], max);
}
return max;
}
/**
* 二分搜索法
* 把序列比作扑克牌
* 遍历序列, 拿到每一个元素
* 从左到右遍历牌堆, 如果牌顶 >= 元素, 则把元素放到牌顶
* 遍历到牌堆最后, 没有符合的牌顶, 则新建牌堆
* 寻找符合牌顶的过程可以用二分搜索进行优化
* 最后牌堆的数量就是最长上升子序列的长度
*/
public static int lis2(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] top = new int[nums.length];// 牌堆数组
int len = 0;// 牌堆数量
for (int num : nums) {
int begin = 0;
int end = len;
while (begin < end) {
int mid = (begin + end) >> 1;
if (num <= top[mid]) {
end = mid;
} else {
begin = mid + 1;
}
}
top[begin] = num;
if (begin == len) len++;
}
return len;
}
```
#### 练习四: 最长公共子序列
```
public static int lcs(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0
|| nums2 == null || nums2.length == 0) return 0;
return lcs(nums1, nums1.length, nums2, nums2.length);
}
private static int lcs(int[] nums1, int i, int[] nums2, int j) {
if (i == 0 || j == 0) return 0;
if (nums1[i - 1] == nums2[j - 1]) return lcs(nums1, i - 1, nums2, j - 1) + 1;
return Math.max(lcs(nums1, i, nums2, j - 1), lcs(nums1, i - 1, nums2, j));
}
/**
* 优化一: 去除递归
*/
public static int lcs1(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0
|| nums2 == null || nums2.length == 0) return 0;
int[][] dp = new int[nums1.length + 1][nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
for (int j = 1; j <= nums2.length; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[nums1.length][nums2.length];
}
/**
* 优化二: 使用滚动数组
*/
public static int lcs2(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0
|| nums2 == null || nums2.length == 0) return 0;
int[][] dp = new int[2][nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
int row = i & 1;
int prevRow = (i - 1) & 1;
for (int j = 1; j <= nums2.length; j++) {
if (nums1[i - 1] == nums2[j - 1]) {
dp[row][j] = dp[prevRow][j - 1] + 1;
} else {
dp[row][j] = Math.max(dp[prevRow][j], dp[row][j - 1]);
}
}
}
return dp[nums1.length & 1][nums2.length];
}
/**
* 优化三: 使用一维数组
*/
public static int lcs3(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0
|| nums2 == null || nums2.length == 0)
return 0;
int[] rowsNums = nums1, colsNums = nums2;
if (nums1.length < nums2.length) {
rowsNums = nums2;
colsNums = nums1;
}
int[] dp = new int[colsNums.length + 1];
for (int i = 1; i <= rowsNums.length; i++) {
int cur = 0;
for (int j = 1; j <= colsNums.length; j++) {
int leftTop = cur;
cur = dp[j];
if (nums1[i - 1] == nums2[j - 1]) {
dp[j] = leftTop + 1;
} else {
dp[j] = Math.max(dp[j - 1], dp[j]);
}
}
}
return dp[colsNums.length];
}
```
####: 练习五: 最长公共子串
- 子串是连续的子序列
- 状态转移方程
- dp(i, j)是以str1[i - 1], str2[j - 1]结尾的最长公共子串长度
- 如果str1[i - 1] = str2[j - 1], dp(i, j) = dp(i - 1, j - 1) + 1
- 如果str1[i - 1] != str2[j - 1], dp(i, j) = 0;
- 最长公共子串的长度为max{dp(i, j)}
```
/**
* dp(i, j)表示以str1[i - 1]和str2[j - 1]为结尾的最长公共子串
*/
public static int lcs(String str1, String str2) {
if (str1 == null || str1.length() == 0
|| str2 == null || str2.length() == 0) return 0;
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
int[][] dp = new int[chars1.length + 1][chars2.length + 1];
int max = 0;
for (int i = 1; i <= chars1.length; i++) {
for (int j = 1; j <= chars2.length; j++) {
if (chars1[i - 1] == chars2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
max = Math.max(max, dp[i][j]);
}
}
return max;
}
/**
* 优化: 一维数组
*/
public static int lcs1(String str1, String str2) {
if (str1 == null || str1.length() == 0
|| str2 == null || str2.length() == 0) return 0;
char[] chars1 = str1.toCharArray();
char[] chars2 = str2.toCharArray();
char[] rowsChars = chars1, colsChars = chars2;
if (chars1.length < chars2.length) {
rowsChars = chars2;
colsChars = chars1;
}
int[] dp = new int[colsChars.length + 1];
int max = 0;
for (int i = 1; i <= rowsChars.length; i++) {
int cur = 0;
for (int j = 1; j <= colsChars.length; j++) {
int leftTop = cur;
cur = dp[j];
if (rowsChars[i - 1] == colsChars[j - 1]) {
dp[j] = leftTop + 1;
} else {
dp[j] = 0;
}
max = Math.max(max, dp[j]);
}
}
return max;
}
```
#### 练习六: 背包问题
- 有 n 件物品和一个最大承重为 W 的背包,每件物品的重量是wi、价值是vi
- 在保证总重量恰好等于 W 的前提下,选择某些物品装入背包,背包的最大总价值是多少?
- 注意:每个物品只有 1 件,也就是每个物品只能选择 0 件或者 1 件
```
/**
* dp(i, j)表示最大承重为j, 有前i件物品可供选择时的最大价值
* dp(0, j) = 0, dp(i, 0) = 0;
* if(j < weights[i - 1]) dp(i, j) = dp(i - 1, j)最后一件物品不选
* else dp(i, j) = max(dp(i - 1, j), values[i - 1] + dp(i 1, j - weights[i - 1])); 选择最后一件物品
*/
public static int maxValue(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0
|| weights == null || weights.length == 0
|| values.length != weights.length
|| capacity <= 0) return 0;
int[][] dp = new int[values.length + 1][capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = 1; j <= capacity; j++) {
if (j < weights[i - 1]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], values[i - 1] + dp[i - 1][j - weights[i - 1]]);
}
}
}
return dp[values.length][capacity];
}
/**
* 优化: 使用一维数组
*/
public static int maxValue1(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0
|| weights == null || weights.length == 0
|| values.length != weights.length
|| capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i - 1]; j--) {
dp[j] = Math.max(dp[j], values[i - 1] + dp[j - weights[i - 1]]);
}
}
return dp[capacity];
}
/**
* 0 - 1背包恰好问题
*/
public static int maxValue2(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0
|| weights == null || weights.length == 0
|| values.length != weights.length
|| capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
// 初始化: 将不合理的值设置为负无穷大
for (int j = 1; j <= weights.length; j++) {
dp[j] = Integer.MIN_VALUE;
}
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i - 1]; j--) {
dp[j] = Math.max(dp[j], values[i - 1] + dp[j - weights[i - 1]]);
}
}
return dp[capacity];
}
```
---
## 布隆过滤器(Bloom Filter)
### 如果要经常判断 1 个元素是否存在,你会怎么做?
- 很容易想到使用哈希表(HashSet、 HashMap),将元素作为 key 去查找
- 时间复杂度: O(1),但是空间利用率不高,需要占用比较多的内存资源
### 如果需要编写一个网络爬虫去爬10亿个网站数据,为了避免爬到重复的网站,如何判断某个网站是否爬过?
- 显然, HashSet、 HashMap 并不是非常好的选择
### 是否存在时间复杂度低、占用内存较少的方案?
- 布隆过滤器(Bloom Filter)
### 特点
#### 它是一个空间效率高的概率型数据结构,可以用来告诉你:一个元素一定不存在或者可能存在
#### 它实质上是一个很长的二进制向量和一系列随机映射函数(Hash函数)
#### 优缺点
- 优点:空间效率和查询时间都远远超过一般的算法
- 缺点:有一定的误判率、删除困难
#### 常见应用
网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统、解决缓存穿透问题
### 原理
- 假设布隆过滤器由 20位二进制、 3 个哈希函数组成,每个元素经过哈希函数处理都能生成一个索引位置
- 添加元素:将每一个哈希函数生成的索引位置都设为 1
- 查询元素是否存在
- 如果有一个哈希函数生成的索引位置不为 1,就代表不存在(100%准确)
- 如果每一个哈希函数生成的索引位置都为 1,就代表存在(存在一定的误判率)
```
public class BloomFilter {
private int bitSize;// 二进制位的个数
private int hashSize;// 哈希函数的个数
private long[] bits;// 二进制向量
/**
* @param n数据规模
* @param p误判率
*/
public BloomFilter(int n, double p) {
double ln2 = Math.log(2);
bitSize = (int) (-n * Math.log(p) / Math.pow(ln2, 2));
hashSize = (int) ((bitSize * ln2) / n);
bits = new long[(bitSize + Long.SIZE - 1) / Long.SIZE];// 与分页计算相同
}
/**
* @returntrue: 修改过二进制向量
*/
public boolean put(T value) {
valueNotNullCheck(value);
// 根据value值, 通过不同的hash函数计算出不同的索引
int hash1 = value.hashCode();
int hash2 = hash1 >>> 16;
boolean result = false;
for (int i = 1; i <= hashSize; i++) {
int combinedHash = hash1 + (i * hash2);
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
// 生成一个二进位的索引
int index = combinedHash % bitSize;
// 将二进制向量中index位置的值设置为1
if (set(index)) result = true;
}
return result;
}
public boolean contains(T value) {
valueNotNullCheck(value);
// 根据value值, 通过不同的hash函数计算出不同的索引
int hash1 = value.hashCode();
int hash2 = hash1 >>> 16;
for (int i = 1; i <= hashSize; i++) {
int combinedHash = hash1 + (i * hash2);
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
// 生成一个二进位的索引
int index = combinedHash % bitSize;
// 将二进制向量中index位置的值设置为1
if (!get(index)) return false;
}
return true;
}
private void valueNotNullCheck(T value) {
if (value == null) throw new NullPointerException("Value cannot be null");
}
/**
* 设置index位置的值为1
*/
private boolean set(int index) {
// 在long数组中找到对应的value值
long value = bits[index / Long.SIZE];
int bitValue = 1 << (index % Long.SIZE);
bits[index / Long.SIZE] = value | bitValue;
return (value & bitValue) == 0;
}
/**
* 查看index位置的值
* @return true: 1, false: 0
*/
private boolean get(int index) {
long value = bits[index / Long.SIZE];
int bitValue = 1 << (index % Long.SIZE);
return (value & bitValue) != 0;
}
}
```
---
## 跳表
### 一个有序链表搜索、添加、删除的平均时间复杂度是多少?
- O(n)
- 链表没有像数组那样高效的随机访问(O(1)时间复杂度), 所以不能像有序数组那样使用二分搜索进行优化
- 想要将这个三个操作优化至logn, 就可以使用跳表
### 跳表, 又叫做跳跃表、跳跃列表,在有序链表的基础上增加了跳跃功能
### 对比平衡树
- 跳表的实现和维护会更加简单
- 跳表的搜索、删除、添加的平均时间复杂度是O(logn)
#### 跳表的搜索
- 从顶层链表的首元素开始, 从左往右搜索, 直到找到一个大于或等于目标的元素, 或者达到当前链表的尾部
- 如果元素等于目标元素, 找到返回
- 如果大于或者已经到达尾部, 则退回当前层前一个元素, 转入下一层
```
public V get(K key) {
keyNotNullCheck(key);
Node node = first;
int cmp = -1;
for (int i = level - 1; i >= 0; i--) {
while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {
node = node.nexts[i];
}
// 此时node.next == null || node.next.key > key, 判断是否相等: 相等直接返回, 不相等直接跳到下一层寻找
if (cmp == 0) return node.nexts[i].value;
}
return null;
}
```
#### 跳表的添加
- 用跳表搜索的方法, 记录每一层的前驱节点
- 在记录过程中如果已经找到相同元素, 覆盖返回
- 找完前驱节点后, 为新节点生成随机的层数, 插入(更新前驱后继)
- 更新层数
```
public V put(K key, V value) {
keyNotNullCheck(key);
// 找到所有层级的前驱节点, 如果找到相同的节点, 直接覆盖返回
Node node = first;
Node[] prevs = new Node[level];// 记录当前层的前驱节点
int cmp = -1;
for (int i = level - 1; i >= 0; i--) {
while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {
node = node.nexts[i];
}
if (cmp == 0) {
V oldValue = node.nexts[i].value;
node.nexts[i].value = value;
return oldValue;
}
prevs[i] = node;
}
// 插入新的节点
int newLevel = randomLevel();
Node newNode = new Node<>(key, value, new Node[newLevel]);
// 更新前驱后继
for (int i = 0; i < newLevel; i++) {
if (i >= level) {
first.nexts[i] = newNode;
} else {
newNode.nexts[i] = prevs[i].nexts[i];
prevs[i].nexts[i] = newNode;
}
}
size++;
// 更新层数
level = Math.max(level, newLevel);
return null;
}
```
#### 跳表的删除
- 用跳表搜索的方法, 记录每一层的前驱节点
- 若节点不存在, 直接返回
- 更新前驱节点的后继
- 更新层数
```
public V remove(K key) {
keyNotNullCheck(key);
Node node = first;
Node[] prevs = new Node[level];// 记录当前层的前驱节点
int cmp = -1;
for (int i = level - 1; i >= 0; i--) {
while (node.nexts[i] != null && (cmp = compare(key, node.nexts[i].key)) > 0) {
node = node.nexts[i];
}
prevs[i] = node;
}
if (cmp != 0) return null;// 不存在此节点
// 更新前驱后继
Node removedNode = node.nexts[0];
for (int i = 0; i < removedNode.nexts.length; i++) {
prevs[i].nexts[i] = removedNode.nexts[i];
}
// 更新节点数量
size--;
// 更新层数
int newLevel = level;
while (--newLevel >= 0 && first.nexts[newLevel]== null) level = newLevel;
return removedNode.value;
}
```
---
## B+树
### B+树是B树的变体, 常用于数据库和操作系统
- MySQL数据库的索引就是基于B+树实现的
- B+树的特点
- 分为内部节点(非叶子)、叶子节点2种节点
- 内部节点只存储key, 不存储具体数据
- 叶子节点存储key和具体数据
- 所有的叶子节点形成一条有序链表
- m阶B+树非根节点的元素数量x:m/2(向上) <= x <= m
### 操作系统读取硬盘数据的过程
1. 操作系统将LBA(逻辑块地址, 如设配号, 磁头号, 磁道号, 扇区号, 扇区计数)传送给磁盘驱动器并启动读取命令
2. 磁盘驱动器根据LBA将磁头移动到正确的磁道, 盘片开始旋转, 将目标扇区旋转到磁头下
3. 磁盘控制器将扇区数据等信息传送到一个处于磁盘界面的缓冲区
4. 磁盘驱动器向操作系统发出"数据就绪"信号
5. 操作系统从磁盘界面的缓冲区读取数据
### 磁盘完成IO操作的时间
- 寻道时间: 将读写磁头移动至正确的磁道上所需要的时间, 这部分时间代价最高
- 旋转延迟时间: 盘片旋转将目标扇区移动到读写磁头下方所需要的时间, 取决于磁盘转速
- 数据传输时间: 完成传输数据所需要的时间, 取决于接口的数据传输率, 通常远小于前两部分消耗时间
- 决定时间长短的大部分因素和硬件相关, 但所需系统的磁道数是可以通过操作系统来进行控制的, 减少所需移动的磁道数是减少整个硬盘读写时间的有效办法, 合理安排磁头的移动以减少寻道时间就是磁盘调度算法的目的所在
### MySQL的索引底层为何使用B+树?
- 为了减少IO操作数量, 一般把一个节点的大小设计成最小读写单位的大小
- 对比B树, B+树的优势是
- 每个节点存储的key数量更多, 树的高度更低
- 所有的具体数据都存在叶子节点上, 所以每次查询都要查到叶子节点, 查询速度稳定
- 所有的叶子节点构成了一个有序链表, 做区间查询更加方便
---
## 串
### 蛮力算法
```
/**
* 实现一
*/
public static int bruteForce(String text, String pattern) {
if (text == null || pattern == null) return -1;
int tlen = text.length(), plen = pattern.length();
if (tlen == 0 || plen == 0 || plen > tlen) return -1;
char[] tChars = text.toCharArray();
char[] pChars = pattern.toCharArray();
int ti = 0, pi = 0;
int tiMax = tlen - plen;
while (pi < plen && ti - pi <= tiMax) {
if (tChars[ti] == pChars[pi]) {
ti++;
pi++;
} else {
ti -= pi - 1;
pi = 0;
}
}
return pi == plen ? ti - pi : -1;
}
/**
* 实现二
*/
public static int bruteForce1(String text, String pattern) {
if (text == null || pattern == null) return -1;
int tlen = text.length(), plen = pattern.length();
if (tlen == 0 || plen == 0 || plen > tlen) return -1;
char[] tChars = text.toCharArray();
char[] pChars = pattern.toCharArray();
int tiMax = tlen - plen;
for (int ti = 0; ti <= tiMax; ti++) {
int pi = 0;
for (; pi < plen; pi++) {
if (tChars[ti + pi] != pChars[pi]) break;
}
if (pi == plen) return ti;
}
return -1;
}
```
- 时间复杂度: O(n^2)
### KMP
- KMP会预先根据模式串内容生成一张next表
- 当text[ti] != pattern[pi]时, ti不需要回溯, pi不一定回溯, pi = next[pi]
- next[pi] 是 pi 左边子串的真前缀后缀的最大公共子串长度
```
public static int kmp(String text, String pattern) {
if (text == null || pattern == null) return -1;
int tlen = text.length(), plen = pattern.length();
if (tlen == 0 || plen == 0 || plen > tlen) return -1;
char[] tChars = text.toCharArray();
char[] pChars = pattern.toCharArray();
int[] next = next1(pChars);
int ti = 0, pi = 0;
int tiMax = tlen - plen;
while (pi < plen && ti - pi <= tiMax) {
if (pi < 0 || tChars[ti] == pChars[pi]) {
pi++;
ti++;
} else {
pi = next[pi];
}
}
return pi == plen ? ti - pi : -1;
}
private static int[] next(char[] pattern) {
int[] next = new int[pattern.length];
int n = next[0] = -1;
int i = 0;
int iMax = next.length - 1;
while (i < iMax) {
if (n < 0 || pattern[i] == pattern[n]) {
next[++i] = ++n;
} else {
n = next[n];
}
}
return next;
}
/**
* 优化: i, n元素相同时, 若i + 1与n + 1元素也相同, 则i + 1失配时, n + 1也会失配
* 所以: next[i + 1] = next[n + 1];
*/
private static int[] next1(char[] pattern) {
int[] next = new int[pattern.length];
int n = next[0] = -1;
int i = 0;
int iMax = next.length - 1;
while (i < iMax) {
if (n < 0 || pattern[i] == pattern[n]) {
i++;
n++;
if (pattern[i] == pattern[n]) {
next[i] = next[n];
} else {
next[i] = n;
}
} else {
n = next[n];
}
}
return next;
}
```

- 时间复杂度: 主逻辑O(n), next表O(m), 总体O(n + m)

一键复制

编辑

Web IDE

原始数据

按行查看

历史