Mysql主键索引时间复杂度,数据结构与算法笔记二.md

## 排序

### 默认升序

### 冒泡排序(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

原始数据

按行查看

历史

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值