Merge branch 'master' of github.com:nonstriater/Learn-Algorithms

This commit is contained in:
nonstriater
2025-06-13 15:03:18 +08:00
59 changed files with 1801 additions and 372 deletions
+7
View File
@@ -0,0 +1,7 @@
# 字符串匹配算法 KMP
KMP于1977年被提出,全称 Knuth–Morris–Pratt 算法; 名字分别是:Donald Knuth(K), James H. Morris(M), Vaughan Pratt(P).
KMP算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。
+1 -1
View File
@@ -25,7 +25,7 @@
## 滑动窗口
使用滑动窗口解决字符串问题,代码框架
使用滑动窗口解决字符串子串问题,代码框架
```
int left = 0, right = 0;
+1 -2
View File
@@ -3,7 +3,6 @@
Java 中 String 实现。
```
```
+32
View File
@@ -5,3 +5,35 @@
```Java
static AtomicInteger qpsCount = 100; //线程安全
static volatile long lastSenconds = System.currentTimeMillis()/1000;
//1 计数器
public static boolean tryAcquire() {
long current = System.currentTimeMillis()/1000;
if(current == lastSenconds){
if (qpsCount-- > 0) {//CAS api
return true;
} else {
//限流
return false;
}
} else{//下一个时间窗口
lastSenconds = current;
qpsCount = 100;
return true;
}
}
```
+29
View File
@@ -4,6 +4,7 @@
* 双向链表
* 双向循环链表
## 链表
### 应用场景
@@ -11,6 +12,21 @@
* [redis slowlog](Redis%20slowlog.md)
### 链表 VS 数组
特点
* 数组 : 内存连续, 更好利用局部性原理;内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,有界
* 链表 : 不存在数组的扩容问题, 空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问; 需要前后元素位置的指针,会消耗相对更多的储存空间
优缺点:
* 查询: 数组 O(1), 有序时可以用二分查找;
* 删除: 链表只需要移动指针 O(1) ,数组的话删除元素需要移动后续的元素 O(N)
### 扩缩容
简单说下编程语言 java, golang中 LinkList的扩缩容的策略。
@@ -23,12 +39,15 @@ java 中扩容,每次扩容新增原先容量的 1/2
这个就不介绍了。重点说下双向链表。
## 双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
双向链表克服了单链表中访问某个节点前驱节点(插入,删除操作时),只能从头遍历的问题。
缺点是: 多了1倍的额外的指针空间大小。
```
typedef int Value
typedef struct Entry{
@@ -38,6 +57,12 @@ typedef struct Entry{
```
应用场景
* mysql B+树 叶子节点就使用 双向链表,方便 `age<10` 类似条件查询,或者 倒序查询 如 `order by desc` ,从后向前遍历数据
* Java AQS 中的等待队列, 是一个双端 双向链表 的结构 (FIFO 结构)
## 循环链表
最后一个节点指针指向头节点的链表
@@ -54,3 +79,7 @@ typedef struct Entry{
+28
View File
@@ -0,0 +1,28 @@
# 数组
数据与链表差异 [参考这里](README.md)
## 循环数组
特点
* 长度固定,下表不会越界,使用2个指针标识 头尾 下标; 默认都是 0
* 方便用来实现 栈,队列;比如 队列实现时,新增元素,头下表+1; 删除元素,尾下标+1
比如Java的 ArrayBlockingQueue 就是一个 带有 takeIndex 和 putIndex 的环形数组。
缺点:头尾之间的元素不好维护,从中间删除了某个元素,会出现数组空隙。
数据结构
```
```
+58
View File
@@ -100,6 +100,64 @@ protected boolean removeEldestEntry(Map.Entry eldest) {
```
基于 LinkedHashMap实现的 LRU Cache
```Java
class LRUCache {
int cap;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.cap = capacity;
}
//访问元素
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
// 将 key 变为最近使用
makeRecently(key);
return cache.get(key);
}
//添加元素到cache
public void put(int key, int val) {
if (cache.containsKey(key)) {
// 修改 key 的值
cache.put(key, val);
// 将 key 变为最近使用
makeRecently(key);
return;
}
if (cache.size() >= this.cap) {
// 链表头部就是最久未使用的 key
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// 将新的 key 添加链表尾部
cache.put(key, val);
}
private void makeRecently(int key) {
int val = cache.get(key);
// 删除 key,重新插入到队尾
cache.remove(key);
cache.put(key, val);
}
}
```
+1
View File
@@ -63,6 +63,7 @@ Hash hashCode(char *key){
当然,还有其他的散列函数,如`平方取中法`, `随机数法`等。
## 碰撞解决
+13 -5
View File
@@ -1,13 +1,16 @@
# 二叉查找树
# 二叉查找树BST
二叉查找树(Binary search tree),也叫`有序二叉树(Ordered binary tree)`,`排序二叉树(Sorted binary tree)`。是指一个空树或者具有下列性质的二叉树:
二叉查找树(Binary search tree),也叫`有序二叉树(Ordered binary tree)`,`排序二叉树(Sorted binary tree)`。
是指一个空树或者具有下列性质的二叉树:
1. 若任意节点的左子树不为空,则左子树上所有的节点值小于它的根节点值
2. 若任意节点的右子树不为空,则右子树上所有节点的值均大于它的根节点的值
3. 任意节点左右子树也为二叉查找树
4. 没有键值(key)相等的节点
有序的二叉查找树,中序遍历结果是递增的。
有序的二叉查找树,中序遍历结果是递增的。 `左小右大`
```
@@ -23,12 +26,15 @@
```
### 删除节点
删除节点,需要重建排序树
1) 删除节点是叶子节点(分支为0),结构不破坏
2)删除节点只有一个分支(分支为1),结构也不破坏
3)删除节点有2个分支,此时删除节点
3)删除节点有2个分支,此时删除节点 ; 需要重建树
思路一: 选左子树的最大节点,或右子树最小节点替换
```
@@ -55,6 +61,7 @@ int bisearch_tree_delete(BiSearchTree **tree,ElemType node){
printf("树为空,或想要删除的节点不存在\n");
return -1;
}
//该节点为叶子节点,直接删除
if (!target->rChild && !target->lChild)
{
@@ -86,7 +93,8 @@ int bisearch_tree_delete(BiSearchTree **tree,ElemType node){
target->rChild=target->rChild->rChild;
free(del);
}
}
else{ //左右子树均不空,p,t 2个指针一前以后,将左子树最大的节点(肯定是一个最右的节点)替换到删除的节点后,还需要处理左子树最大节点的左子树
BiSearchTree *p=target,*t=target->lChild;
+6 -4
View File
@@ -1,6 +1,5 @@
# 字典树trie
## trie基本
`字典树`,英文名`Trie树`,Trie一词来自retrieve,发音为/tri:/ “tree”,也有人读为/traɪ/ “try”,
又称`单词查找树` 或 `前缀树`,Trie树,是一种树形结构(多叉树)。
@@ -81,9 +80,12 @@ trie树把每个关键字保存在一条路径上,而不是一个节点中
## trie树存储结构和基本操作
最简单实现 ---- 26个字母表 a-z (没有考虑数字,大小写,其他字符如=-*/)
子树用数组存储,浪费空间;如果系统中存在大量字符串,且这些字符串基本没有公共前缀,trie树将消耗大量内存
如果用链表存储,查询时需要遍历链表,查询效率有所降低
最简单实现 ---- 26个字母表 `a-z` (没有考虑数字,大小写,其他字符如 `=-*/`)
trie 树存储结构
* 用数组存储,浪费空间;如果系统中存在大量字符串,且这些字符串基本没有公共前缀,trie树将消耗大量内存
* 用链表存储,查询时需要遍历链表,查询效率有所降低
+1 -2
View File
@@ -1,5 +1,4 @@
## 伸展树 (splay tree)
# 伸展树 (splay tree)
伸展树是一种自平衡的二叉排序树。为什么需要这些自平衡的二叉排序树?
+1 -2
View File
@@ -1,5 +1,4 @@
## 后缀树(suffix tree)
# 后缀树(suffix tree)
###后缀树的应用
+20
View File
@@ -0,0 +1,20 @@
# Top-K 问题
问题描述:从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题
思路
1. 排序,全局都排序了,这也是这个方法复杂度非常高的原因 ; [排序算法参考这里](../../6%20Sort/REAME.md)
2. 冒泡排序,局部排序徐,只对最大的k个数排序
3. [堆排序](./堆.md), 只找最大的k个数,这k个数不需要排序; top-k大 问题就是用 小根堆, 小根堆 固定为 k 个元素大小 , 遍历 k-N (N为所有数据的个数),插入小根堆并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+196
View File
@@ -1,5 +1,201 @@
# 大顶堆 和 小顶堆
先来了解下 `堆` 结构; 堆也被称为`优先队列`,`二叉堆` ;
特点
* 堆总是一颗完全二叉树树, 使用数组作为其存储结构,因此也叫 `二叉堆`; 用链表存的就叫二叉树了
* 任一节点小于(或大于)其所有的孩子节点;
* 堆分小根堆和大根堆; 如果根节点大于所有孩子节点,这就是一颗大根堆,也就是根节点是堆上的最大值;如果节点小于所有的子节点,这就是一颗小跟堆,也即是根节点是堆上所有节点的最小值。
* 小根堆: 每次取出来的元素都是队列中值最小的
`堆用数组来存储`,因为是一颗完全二叉树,i节点的父节点索引就是(i-1)/2, 左右子节点小标是 2i+1,2i+2。
比如小根堆存储示例
![小根堆](./pq-1.jpg)
### 应用场景
* 优先队列 如iOS中的 NSOperationQueue 就是维护一个优先队列
* 堆排序
* [top-K 大(小)](Top-K%20问题.md) , top-k大 问题就是用 小根堆, 小根堆 固定为 k 个元素大小 , 遍历 k-N (N为所有数据的个数),插入小根堆并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。
### Java PriorityQueue
Java PriorityQueue 类就是 通过二叉小顶堆实现 ,也叫优先级队列实现。 有如下特点:
* 实现 Queue 接口
* 头部是基于自然排序或基于比较器的排序的最小元素
* 不是线程安全的,PriorityBlockingQueue在并发环境中使用
API 如下:
* boolean add(object):将指定的元素插入此优先级队列。
* boolean offer(object):将指定的元素插入此优先级队列。
* boolean remove(object):从此队列中删除指定元素的单个实例(如果存在)。
* Object poll():检索并删除此队列的*头部*,如果此队列为空,则返回null。
* Object element():检索但不删除此队列的*头部*,如果此队列为空,则返回null。
* Object peek():检索但不删除此队列的*头部*,如果此队列为空,则返回null。
* void clear():从此优先级队列中删除所有元素。
```
{
PriorityQueue<Employee> pq = new PriorityQueue();
pq.add(new Employee(1L, "AAA", LocalDate.now()));
pq.add(new Employee(4L, "BBB", LocalDate.now()));
pq.add(new Employee(3L, "DDD", LocalDate.now()));
pq.add(new Employee(7L, "GGG", LocalDate.now()));
pq.add(new Employee(2L, "CCC", LocalDate.now()));
while (true) {
Employee head = pq.poll();
System.out.println(head);
if (head == null) {
return;
}
}
}
```
输出
```
Employee [id=1, name=AAA, dob=2021-12-22]
Employee [id=2, name=CCC, dob=2021-12-22]
Employee [id=3, name=DDD, dob=2021-12-22]
Employee [id=4, name=BBB, dob=2021-12-22]
Employee [id=7, name=GGG, dob=2021-12-22]
```
如下,使用 PriorityQueue实现的 top-K 大问题,实现如下:
```Java
/**
* 小根堆实现
*/
public static int findMaxK(int[] nums, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>(k, (a, b) -> (a-b) );
for (int i = 0; i <nums.length ; i++) {
//取出前k个元素放入 PQ 中
if (i < k) {
pq.add(nums[i]);
continue;
}
Integer head = pq.peek();
if (head < nums[i]) {//维护 priorityQueue 中元素只有k个
pq.poll();
pq.add(nums[i]);
}
}
return pq.poll();
}
```
# 堆存储结构
堆的操作有:
* 建堆
* 插入:都是插入到数组最后,然后再调整满足堆次序
* 删除:删除总是发生在 A[0]处,也就是只删除根节点
这样难怪`堆`被称为 `优先队列`。 插入和删除分别在 数组尾部和头部,只是需要再次调整以满足堆次序。
### 堆调整
插入或删除元素以后, 如何进行堆调整, 已满足堆次序?
优先级队列的代码实现
```java
public class MaxPQ
<Key extends Comparable<Key>> {
// 存储元素的数组
private Key[] pq;
// 当前 Priority Queue 中的元素个数
private int N = 0;
public MaxPQ(int cap) {
// 索引 0 不用,所以多分配一个空间
pq = (Key[]) new Comparable[cap + 1];
}
/* 返回当前队列中最大元素 */
public Key max() {
return pq[1];
}
/* 插入元素 e */
public void insert(Key e) {...}
/* 删除并返回当前队列中最大元素 */
public Key delMax() {...}
/* 上浮第 k 个元素,以维护最大堆性质 */
private void swim(int k) {...}
/* 下沉第 k 个元素,以维护最大堆性质 */
private void sink(int k) {...}
/* 交换数组的两个元素 */
private void exch(int i, int j) {
Key temp = pq[i];
pq[i] = pq[j];
pq[j] = temp;
}
/* pq[i] 是否比 pq[j] 小? */
private boolean less(int i, int j) {
return pq[i].compareTo(pq[j]) < 0;
}
/* 还有 left, right, parent 三个方法 */
}
```
+8 -1
View File
@@ -1,6 +1,6 @@
# 红黑树 (red-black tree)
红黑树(Red Black Tree) 是一种自平衡二叉查找树。一种特化的AVL树,在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。 它可以在O(log n)时间内做查找,插入和删除。
红黑树(Red Black Tree) 是一种自平衡二叉查找树。一种特化的[AVL树](../3-平衡树AVL/README.md),在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。 它可以在O(log n)时间内做查找,插入和删除。
它的每个结点都被“着色”为红色或者黑色,这些结点的颜色被用来检测树的平衡性。
@@ -49,3 +49,10 @@
-12
View File
@@ -1,12 +0,0 @@
# 反向索引
正排索引如 mysql中的索引实现 ,通常使用 B+ 树实现。 反向索引如何来实现呢?
### 应用场景
* ES
* 推荐内容召回
+11 -6
View File
@@ -16,16 +16,25 @@
* 赫夫曼编码 Huffman
## [二叉查找树](2-二叉查找树/二叉查找树.md)
## 二叉树
快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历
[快速排序](../6%20Sort/README.md)就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历
## [二叉查找树BST](2-二叉查找树/二叉查找树.md)
有序的二叉树,中序遍历结果是递增的
## [AVL树](3-平衡树AVL/README.md)
绝对的平衡二叉树;
## [红黑树](9-红黑树%20R-B%20tree/红黑树.md)
弱平衡二叉树;使用广泛
## [字典树trie](4-字典树Trie/README.md)
@@ -48,10 +57,6 @@
## 赫夫曼编码 Huffman
+10 -4
View File
@@ -1,19 +1,24 @@
# 赫夫曼编码 Huffman
## 赫夫曼编码 Huffman
Huffman在1952年根据香农(Shannon)在1948年和范若(Fano)在1949年阐述的这种编码思想提出了一种不定长编码的方法,也称霍夫曼(Huffman)编码。
这是一个经典的压缩算法。通过`字符出现的频率`,`优先级`,`二叉树`进行的压缩算法。
这是一个经典的压缩算法。通过`字符出现的频率`,`优先级`,`二叉树` 进行的压缩算法。
对一个字符串,计算每个字符出现的次数,把这些字符放到优先队列(priority queue)
这这个priority queue转出二叉树
对一个字符串,计算每个字符出现的次数, 把这些字符放到优先队列(priority queue); 这个priority queue转出二叉树
需要一个字符编码表来解码,通过二叉树建立huffman编码和解码的字典表
举一个例子:
原始串:
二级制编码:
huffman编码:
### 存储结构和基本操作
```
@@ -42,6 +47,7 @@ struct node{
被编码的文本长度 unsigned int size
字符频率表 unsigned char freqs[NUM_CHARS]
###解压缩
1. 读取文件头
+14 -1
View File
@@ -1,4 +1,17 @@
# DFS 和 BFS
# DFS 和 BFS 搜索算法
DFS: 深度优先搜索,以深度为准则,先一条路走到底,直到达到目标; 没有达到目标又无路可走了,那么则退回到上一步的状态,走其他路。这便是回溯上来。
BFS:广度优先搜素,在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路,并重复这样的操作。
> DFS用递归的形式,用到了栈结构,先进后出; BFS选取状态用队列的形式,先进先出。
+17 -7
View File
@@ -9,12 +9,12 @@
### 应用场景
工业界有哪些应用场景
工业界有哪些应用场景?
* 匹配,如打车中司乘匹配引擎,如何做到效率最优
* 并行任务调度,一组任务,任务有优先级,如何合理安排任务调度,在最短时间内完成
* 导航路径规划,在使用导航软件时,用户在选择一个开始地点和目的地之后导航软件会给出各种如路程最短,不走高速,时长最短等方案
* 社区发现,在好友关系中,根据社区之间联系或紧密,利用图 louvain 算法或者其他算法对用户进行分群从而达到精准营销,个性化服务等
* 并行任务调度: 一组任务,任务有优先级,如何合理安排任务调度,在最短时间内完成
* 导航路径规划:在使用导航软件时,用户在选择一个开始地点和目的地之后导航软件会给出各种如路程最短,不走高速,时长最短等方案
* 社区发现: 在好友关系中,根据社区之间联系或紧密,利用图 louvain 算法或者其他算法对用户进行分群从而达到精准营销,个性化服务等
* 金融贷后催收:利用图算法找出符合条件的失联人的联系人,从而提高催收失联修复的覆盖率、有效联系率,助力不良资产的回收
* 套汇
@@ -37,16 +37,23 @@
# 图的存储结构
* 对象和指针
* 矩阵
* 邻接矩阵 (二维数组)
* 邻接表
图数据结构表示:
```
```
## 图的操作
#### 遍历
* 广度优先
* 深度优先
* 广度优先 BFS
* 深度优先 DFS
Dijkstra
A*
@@ -67,3 +74,6 @@ https://www.jiqizhixin.com/articles/2019-05-16-14
+31 -1
View File
@@ -1 +1,31 @@
# 最短路径
# 最短路径算法
最短路径问题: 寻找图(由结点和路径组成的)中两结点之间的最短路径
实现算法
* A* 算法
* Floyd
* Dijkstra(迪杰斯特拉)
* bellman-ford
* spfa
## A* 算法
`A*(A-Star)`算法是一种静态路网中求解最短路径最有效的直接搜索方法; 算法中的距离估算值与实际值越接近,最终搜索速度越快。
## Dijkstra:最短路径算法
Dijkstra 解决图中一点到其余各点到最短路径的问题
Dijkstra是荷兰的计算机科学家,提出”信号量和PV原语“,"解决哲学家就餐问题",”死锁“也是它提出来的)
+167 -91
View File
@@ -3,21 +3,32 @@
**排序的稳定性** 是指对于相等的元素,排序之后,任然保存2个元素的位置没有变,就是稳定的排序,反之就是不稳定排序。
**交换排序算法**
* 交换排序算法
* 桶排序
* 冒泡排序
* 插入排序
* 选择排序
* 希尔排序
* 快排 : 选最后一个作为pivot(基数), 将数据分为 左边小于 pivot, 右边大于 pivot
* 归并排序 :
* 堆排序
### 交换排序算法
**线性排序算法**
* 冒泡排序 :没轮确定一个最大的数排后面
* 选择排序 :每轮选择最小的数排前面
* 快排 : 选最后一个作为pivot(基数), 将数据分为 左边小于 pivot, 右边大于 pivot; 分治递归, 二叉树的前序遍历思路
* 归并排序 : 将无序的数组,分成2个子数组分别排序,然后再merge ; 分治递归, 二叉树的后续遍历思路
* 插入排序 :
* 希尔排序 :
* [堆排序](../4%20Tree/8-堆/堆.md) :
### 线性排序算法
* 桶排序
### 小结
常见的排序算法都是比较排序,比较排序的时间复杂度通常为 O(n^2) 或 O(nlogn)
但是如果带排序的数字有一些特俗性时,我们可以根据这来设计更加优化的排序算法。
# 交换排序算法
排序算法的复杂度由 `比较的次数` 和 `交换的次数` 一起决定。
@@ -31,6 +42,8 @@
在a【i,n】中最小的元素和 a[i]交换位置。空间复杂度O(1),时间复杂度 O(n^2)
![](./selectsort.gif)
### 冒泡排序
@@ -40,50 +53,41 @@
这样,即使是排好序的拿冒泡排序排序,比较的时间复杂度O(n^2)
### 插入排序
1. 第一个元素算作已经排好
2. 取下一个元素,从已经排好的序列元素中,从后向前扫描
3. 如果排好序的元素大于 新元素,排好序的元素移到下一个位置
4. 重复3,直到直到最后的插入位置
5. 重复2
类似插入扑克牌的效果
最坏的情况: 待排序的是一个逆序排放的数组,这样导致每一轮都要移动元素;此时复杂度是是0(n^2)
最好的情况: 待排序的是一已经顺序排放的数字,此时只需要做一轮比较就够了 0(n)。因此可以看到,**对大部分数据已经有序这样的数组排序,使用`插入排序`非常有优势**
空间复杂度O(1)
### 希尔排序
递减增量排序算法,对`插入排序`的改进,实质是分组插入排序,又叫`缩小增量排序`
1. 先将待排数列分割成若干子序列(增量为m)
2. 对每个子序列使用`插入排序`
3. 减小增量,再排序
4. 对全体元素做一次`插入排序`
`希尔排序`提升排序的奥秘就在于`数据元素越有序,使用插入排序效率越高`
![](./bubblesort.gif)
### 快速排序
时间复杂度 平均复杂度 O(n * logn) , 最坏 O(n^2)
空间复杂度
快速排序是对冒泡排序的改进,划分交换排序。
递归一次,pivot 左边都比它小,右边都比它大。这是递归,分治的思想。
> 快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历
> 快速排序就是个二叉树的前序遍历思路,归并排序就是个二叉树的后序遍历思路
代码框架如下
```C
void quicksort(int *a, int left, int right){
if (left<right){//加上这个,不然有死循环,造成堆栈溢出,这也是递归结束条件
int i = partion(a,left,right);//使得局部有序,i作为分隔
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
}
```
对 A[p...r] :
0. 选择最后一个元素作为 pivot (基准)
1. 分解:A[p..q-1] A[q+1..r],使得 A[p...q-1]< A[q] <A[q+1..r]
2. 解决:递归调用 快排,,对子数组A[p..q-1],A[q+1..r]排序
2. 解决:递归调用 快排,对子数组A[p..q-1],A[q+1..r]排序
3. 合并(子问题相互独立的,因此用分治算法就可以了)
具体步骤:
@@ -92,7 +96,8 @@
2. 重排数列,比 pivot 小得排左边,比pivot大的排右边,相等的随便。 一句话就是`挖坑填数`
3. 递归的,使用相同的方式,重排左右两边的子序列
扫描过程分2种:
扫描过程分2种:
1. 挖坑排序,2头向中间扫描,先从后向前找,再从前向后找。
2. 单向扫描
@@ -138,18 +143,52 @@
```
```Java
// pivot 选择 尾部节点, 代码写起来更加简单; 移动元素更方便
// 左右指针技巧
static int partition(int[] nums, int left, int right){
int pivot = nums[right];//选尾部节点作为 pivot
int end = right;
right--;
while (left < right) {
if (nums[left] <= pivot) {
left ++ ; //左边指针 窗口变小
continue;
}
//元素比 pivot 大,右边指针 窗口变小
//swap left & right
swap(nums, left, right);
right--;
}
// 跟 pivot 元素置换
int i = 0;
if (nums[left] <= pivot) {
//swap left+1 & pivot
i = left +1;
} else {//swap left & pivot
i = left ;
}
swap(nums, i, end);
return i;
}
```
平均复杂度 O(n * logn)
最坏 O(n^2)
空间复杂度
快速排序是对冒泡排序的改进,划分交换排序。
![](./qsort.gif)
### 归并排序merge
分治算法(divide-and-conquer),必然用到递归 ;
分治算法(divide-and-conquer),必然用到递归 ;
2个有序数组的合并操作是O(n)的复杂度
因此我们可以将无序的数组,分成2个子数组分别排序,然后再merge,依次类推
@@ -161,21 +200,85 @@
3. 合并
归并排序的代码框架如下:
归并排序的代码框架,套用二叉树的后续遍历思路,如下:
```
void sort(int[] nums, int lo, int hi) {
int mid = (lo + hi) / 2;
sort(nums, lo, mid);
sort(nums, mid + 1, hi);
void sort(int[] nums, int low, int high) {
int mid = (low + high) / 2;
sort(nums, low, mid);
sort(nums, mid + 1, high);
/****** 后序遍历位置 ******/
// 合并两个排好序的子数组
merge(nums, lo, mid, hi);
merge(nums, low, mid, high);
/************************/
}
//合并连个有序数组; 从后往前merge
public void merge(int[] arr,int low,int mid,int high,int[] tmp){
int i = 0;
int j = low,k = mid+1; //左边序列和右边序列起始索引
while(j <= mid && k <= high){
if(arr[j] < arr[k]){
tmp[i++] = arr[j++];
}else{
tmp[i++] = arr[k++];
}
}
//若左边序列还有剩余,则将其全部拷贝进tmp[]中
while(j <= mid){
tmp[i++] = arr[j++];
}
while(k <= high){
tmp[i++] = arr[k++];
}
for(int t=0;t<i;t++){
arr[low+t] = tmp[t];
}
}
```
![](./mergesort.gif)
### 插入排序
1. 第一个元素算作已经排好
2. 取下一个元素,从已经排好的序列元素中,从后向前扫描
3. 如果排好序的元素大于 新元素,排好序的元素移到下一个位置
4. 重复3,直到直到最后的插入位置
5. 重复2
类似插入扑克牌的效果
最坏的情况: 待排序的是一个逆序排放的数组,这样导致每一轮都要移动元素;此时复杂度是是0(n^2)
最好的情况: 待排序的是一已经顺序排放的数字,此时只需要做一轮比较就够了 0(n)。因此可以看到,**对大部分数据已经有序这样的数组排序,使用`插入排序`非常有优势**
空间复杂度O(1)
### 希尔排序
递减增量排序算法,对`插入排序`的改进,实质是分组插入排序,又叫`缩小增量排序`
1. 先将待排数列分割成若干子序列(增量为m)
2. 对每个子序列使用`插入排序`
3. 减小增量,再排序
4. 对全体元素做一次`插入排序`
`希尔排序`提升排序的奥秘就在于`数据元素越有序,使用插入排序效率越高`
![](./shellsort.gif)
@@ -183,40 +286,17 @@ void sort(int[] nums, int lo, int hi) {
利用堆这种数据结构设计的一种排序算法
先来了解下 `堆` 结构
堆分小根堆和大根堆
堆: 任一节点小于(或大于)其所有的孩子节点,如果是大于所有孩子节点,这就是一颗大根堆,也就是根节点是堆上的最大值;如果节点小于所有的子节点,这就是一颗小跟堆,也即是根节点是堆上所有节点的最小值。
堆也被称为`优先队列`
堆总是一颗完全树
`堆用数组来存储`,i节点的父节点就是(i-1)/2,左右子节点小标是 2i+1,2i+2。
堆的操作有:
建堆
插入:都是插入到数组最后,然后再调整满足堆次序
删除:删除总是发生在 A[0]处,也就是只删除根节点
这样难怪`堆`被称为 `优先队列`。插入和删除分别在 数组尾部和头部,只是需要再次调整以满足堆次序。
`堆`的应用场景有:
优先队列 如iOS中的NSOperationQueue 就是维护一个优先队列
堆排序
[堆结构参考这里](../4%20Tree/8-堆/堆.md)
我们来看看如何使用`堆` 来做排序?
1.将待排序数列看做一颗完全二叉树的存储结构
2.`堆化数组`,结束后,根a[0]变成了最小的值(小根堆)
3.取a[0]值,然后对`堆`做删除操作,此时,`堆`会重新 `堆化数组`,a[0]又是下一个最小的值。
删除操作通常是先把数组最后的元素提到a[0]位置,然后从根节点开始进行一次从上向下的调整;调整时,先从左右孩子中找最小的交换。如果父节点比每个节点都小就不用调整。(因此,在`堆排序`是可以直接让 a[0]和数组最后一个元素互换,但要先保存好a[0],或者a[n-1],这样导致了使用`堆排序`时,递增排序使用大根堆,递减排序使用小根堆。)
1. 将待排序数列看做一颗完全二叉树的存储结构
2. `堆化数组`,结束后,根a[0]变成了最小的值(小根堆)
3. 取a[0]值,然后对`堆`做删除操作,此时,`堆`会重新 `堆化数组`,a[0]又是下一个最小的值。删除操作通常是先把数组最后的元素提到a[0]位置,然后从根节点开始进行一次从上向下的调整;调整时,先从左右孩子中找最小的交换。如果父节点比每个节点都小就不用调整。因此,在`堆排序`是可以直接让 a[0]和数组最后一个元素互换,但要先保存好a[0],或者a[n-1],这样导致了使用`堆排序`时,递增排序使用大根堆,递减排序使用小根堆。
4. 循环3,就可以按从小到大的顺序取出所有数组元素。
堆排序主要时间花在建堆期间和`堆化数组`,找数列中最大树只需要O(1)时间复杂度
堆排序主要时间花在建堆期间和`堆化数组`,找数列中最大数只需要O(1)时间复杂度
void heap_sort(int *a, int length){
@@ -235,6 +315,10 @@ void sort(int[] nums, int lo, int hi) {
推排序还可以用来求 top-K 大(小)的问题。
#排序动画演示
接下来,我们看一组排序的动画,你看看能不能猜到他们使用了什么排序算法完成。
![](./qsort.gif)
@@ -255,7 +339,7 @@ void sort(int[] nums, int lo, int hi) {
# 线性排序算法
上面的算法都是基于比较的排序,时间复杂度最好也是 NlogN.而非基于比较的排序,可以突破NlogN的时间下限。当然,非比较的排序,也是需要有一些限定条件的。
上面的算法都是基于比较的排序,时间复杂度最好也是 NlogN. 而非基于比较的排序,可以突破NlogN的时间下限。当然,非比较的排序,也是需要有一些限定条件的。
### 桶排序 bucket sort
@@ -266,14 +350,6 @@ void sort(int[] nums, int lo, int hi) {
# 小结
常见的排序算法都是比较排序,比较排序的时间复杂度通常为 O(n^2) 或 O(nlogn)
但是如果带排序的数字有一些特俗性时,我们可以根据这来设计更加优化的排序算法。
+5
View File
@@ -0,0 +1,5 @@
# 外排序
+7 -13
View File
@@ -3,32 +3,26 @@
详细介绍每一种算法设计的思路,并为每种方法给出一个经典案例的详细解读,总结对应设计思路,最后给出其它案例,以供参考。
* [递归](递归.md)
* [分治算法](分治算法.md)
* [动态规划](动态规划.md)
* [回溯法](回溯法.md)
* [迭代法](迭代法.md)
* [穷举搜索法](穷举搜索法.md)
* [动态规划](动态规划.md)
* [贪心算法](贪心算法.md)
* [回溯法](回溯法.md)
* [分治算法](分治算法.md)
* [递归](递归.md)
* [贪心算法](贪心算法.md)
### 总结
贪心法、分治法、动态规划都是将问题归纳为根小的、相似的子问题,通过求解子问题产生全局最优解。
`贪心法`
`分治法`
`动态规划`
`贪心法`、`分治法`、`动态规划` 都是将问题归纳为根小的、相似的子问题,通过求解子问题产生全局最优解。
## 参考
《算法设计与分析基础》 Anany Levitin
http://www.chinaunix.net/old_jh/23/437639.html
+10 -10
View File
@@ -1,17 +1,17 @@
### 分治算法
# 分治算法
将一个难以直接解决的大问题,分割成一些规模较小的相同问题,各个击破,分而治之。
分治算法常用`递归`实现
分治算法常用[递归](./递归.md) 实现
1) 问题缩小的小规模可以很容易解决
2) 问题可以分解为规模较小相同问题
3) 子问题的解可以合并为该问题的解
4) 各个子问题相互独立,(如果这条不满足,转为`动态规划`求解)
1)问题缩小的小规模可以很容易解决
2)问题可以分解为规模较小相同问题
3)子问题的解可以合并为该问题的解
4)各个子问题相互独立,(如果这条不满足,转为`动态规划`求解)
分治法的步骤:
1. 分解
2. 解决
3. 合并
@@ -25,6 +25,6 @@
#### 其它案例
快速排序
归并排序
最大子数组和
* 快速排序
* [归并排序](../6%20Sort/README.md)
* 最大子数组和
+235 -18
View File
@@ -7,22 +7,81 @@ DP通常基于一个递推公式及一个(或多个)初始状态,当前子问
动态规划算法的关键在于解决冗余,以空间换时间的技术,需要存储过程中的各种状态。可以看着是`分治算法`+`解决冗余`
使用动态规划算法的问题的特征是`子问题的重叠性`,否则动态规划算法不具备优势
动态规划算法也可以说是 `记住求过的解来节省时间` ; 比如 Fibonacci数列 中,先直接从最小,最简单的 f(1) , f(2) 开始,自低向上一直到 f(20) , 这就是动态规划的思路
【初始状态】→【决策1】→【决策2】→…→【决策n】→【结束状态】
动态规划的核心思想就是穷举求最值; 动态规划问题的一般形式就是求最值,动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
### DP 应用场景
如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。
使用动态规划算法的问题的特征是`子问题的重叠性`,`最优子结构` ,否则动态规划算法不具备优势。
动态规划的核心思想就是穷举求最值; 动态规划问题的一般形式就是`求最值`,动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说:
* Fibonacci数列 [代码参考这里 递归](./递归.md)
* 最大子数组和
* 凑零钱问题
* 股票问题 [代码参考这里](https://github.com/nonstriater/deep-in-java/blob/master/src/main/java/com/nonstriater/deepinjava/algo/list/stock/BestChance.java)
* 打家劫舍问题 : num[i] 代表第i个房子中的现金数目,从房子中取钱的最大数目,约束是相邻房子的钱不能同时取出
* 接雨水问题 :num[i]表示柱子高度,计算下雨之后能接多少雨水
* 青蛙跳阶问题: 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。
* 最小编辑距离
* 最长递增子序列 (LIS Longest Increasing Subsequence),如`【5,6,7,3,2,8】` 最长子序列 `【5,6,7,8】`, 输出4
* 最长公共子序列 (LIS Longest public Subsequence)
* 最长回文子序列 (LIS Longest public Subsequence)
* 0-1 背包问题
[更多动态规划案例代码实现参考deep-in-java])(https://github.com/nonstriater/deep-in-java/tree/master/src/main/java/com/nonstriater/deepinjava/algo/framework/dynamic)
青蛙跳阶问题
```
想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。
同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。
要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去
即通用公式为: f(n) = f(n-1) + f(n-2)
那f(2) 或者 f(1) 等于多少呢?
当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;
当只有1级台阶时,只有一种跳法,即f(1)= 1;
```
* 状态
* 状态转移方程, 这一步最为困难
### DP VS 分治法
与[分治法](分治算法.md)不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。
若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。
### DP VS 回溯法
DP 和 回溯法 都会用到递归
动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划;而有些问题没有重叠子问题,也就是[回溯算法](回溯法.md)问题了,复杂度非常高是不可避免的
## DP 解题模板
基本步骤
* 划分问题
* 状态定义, 穷举「状态」, bad case
* 状态转移方程, 这一步最为困难 ; 暴力解法就是状态转移方程
* 状态压缩
### DP 解题模板
```
# 初始化 base case
dp[0][0][...] = base
@@ -35,23 +94,181 @@ for 状态1 in 状态1的所有取值:
```
####基本步骤
1. 划分问题
2. 选择状态
3. 确定决策并写出状态转移方程
4. 写出规划方程
## 最大子数组和
示例: 输入:nums = [-2,1,-3,4,-1,2,1,-5,4],连续子数组 [4,-1,2,1] 的和最大, 输出:6
dp[i] 表示 nums[i] 为结尾的「最大子数组和」; dp[n-1] 就是 nums 的「最大子数组和」
状态转移 : `dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);`
状态压缩:注意到 dp[i] 仅仅和 dp[i-1] 的状态有关
```Java
public static int largestSubSequenceSum2(int[] nums){
int n = nums.length;
if (n == 0) return 0;
// base case
int dp_0 = nums[0];
int dp_1 = 0;
int res = dp_0;
for (int i = 1; i < n; i++) {
// dp[i] = max(nums[i], nums[i] + dp[i-1])
dp_1 = Math.max(nums[i], nums[i] + dp_0);
dp_0 = dp_1;
// 顺便计算最大的结果, 保存到 res
res = Math.max(res, dp_1);
}
return res;
}
```
## 凑零钱问题
凑零钱问题[视频解读参考这里](https://www.ixigua.com/6881883015832666635?wid_try=1)
[实现代码这里](https://github.com/nonstriater/deep-in-java/tree/master/src/main/java/com/nonstriater/deepinjava/algo/framework/dynamic)
如果使用贪心策略,并不能得到最优解。
比如:给定一个面值list : 1,2,4,5,7,10; 给定一个 target : 14, 求 凑齐 target=14 , 最少的零钱数量
思路:
如想求 amount = 14 时的最少硬币数,
如果你知道凑出 amount = 13 的最少硬币数(子问题), 再加 1 个 1元面值 即可
如果你知道凑出 amount = 12 的最少硬币数(子问题), 再加 1 个 2元面值 即可
如果你知道凑出 amount = 10 的最少硬币数(子问题), 再加 1 个 4元面值 即可
如果你知道凑出 amount = 9 的最少硬币数(子问题), 再加 1 个 5元面值 即可
```Java
for (int coin : coins) {
// 计算子问题的结果
int subProblem = dp(coins, amount - coin);
// 子问题无解则跳过
if (subProblem == -1) continue;
// 在子问题中选择最优解,然后加一
res = Math.min(res, subProblem + 1);
}
```
通过备忘录消除子问题(不用递归了), dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出
如想求 amount = 14 时的最少硬币数, dp[14]
dp[0] = 0
dp[1] = 1
dp[2] = 1个2元的,dp[1] + 1个1元
...
dp[9] = dp[8] + 1个1元, dp[7] + 1个2元, dp[5] + 1个4元, dp[4] + 1个5元,dp[2] + 1个7元
1 + dp[i-coin] 从这些可选项里选择最小的
```Java
//对于 dp[i], 遍历可选项, 选择最小的
for(int coin : coins) {
if (i < coin) {
continue;
}
//dp[i] 保留最小的
dp[i] = Math.min(dp[i],dp[i-coin] + 1 )
}
```
完整代码如下:
```Java
//递归解法,处理重叠子问题, 使用 dp[amount+1] 备忘录
int coinChange2(int[] coins, int amount) {
int[] dp = new int[amount + 1];
// 数组大小为 amount + 1,初始值也为 amount + 1
// 为啥 dp 数组初始化为 amount + 1 呢?
// 因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),所以初始化为 amount + 1 就相当于初始化为正无穷
Arrays.fill(dp, amount + 1);
// base case
dp[0] = 0;
// 外层 for 循环在遍历所有状态的所有取值
for (int i = 0; i < dp.length; i++) {
// 内层 for 循环在求所有选择的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) {
continue;
}
dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
```
## 股票问题
num[i] 表示第 i 天的股票价格, 设计一个算法(交易策略) ,计算你能获得的最大收益,你最多可以完成 k 笔交易;
比如: [3,2,6,5,1,3]
k=1 , 第2天买入 2块钱, 第3天卖出 6块,利润 6-2 = 4块 是最大利润
k= 2 (可以交易2次) , [2,6] + [1,3] 是最大利润
动态规划思路如下:
* 状态定义:
* 状态转移:
* 状态压缩:
```
```
## 接雨水问题
num[i]表示柱子高度,计算下雨之后最多能接多少雨水
位置i能装多少?
位置 i 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 l_max 和 r_max;位置 i 最大的水柱高度就是 `min(l_max, r_max)`
思路:
* 暴力解法
* 备忘录解法
* 双指针解法
```
```
#### 最长递增子序列
最长递增子序列(LIS Longest Increasing Subsequence)
#### 其它案例
* 最短路径
* 0-1 背包问题
+35 -5
View File
@@ -13,9 +13,16 @@
回溯法求解问题时,一般是一边建树,一边遍历该树;且采用非递归方法。
#### 案例
* 迷宫问题
* 全排列
## 代码框架
```
```python
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
@@ -35,6 +42,33 @@ def backtrack(路径, 选择列表):
> n 个不重复的数,全排列共有 n! 个
```Java
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}
```
## 八皇后问题
@@ -48,9 +82,5 @@ def backtrack(路径, 选择列表):
#### 其它案例
* 迷宫问题
* 全排列
+1 -3
View File
@@ -1,6 +1,4 @@
## 穷举搜索法
# 穷举搜索法
或者叫蛮力法。对可能的解的众多候选按照某种顺序逐一枚举和检验。典型的问题如选择排序和冒泡排序。
+11 -9
View File
@@ -1,17 +1,19 @@
## 贪心算法
# 贪心算法
不追求最优解,只找到满意解。
#### 赫夫曼编码
#### 其它案例
找回零钱问题
装箱问题
* 跳跃游戏
* 射击气球
* 装箱问题
## 赫夫曼编码
+1 -2
View File
@@ -1,5 +1,4 @@
## 迭代法
# 迭代法
是一种不断用旧值递推新值的过程,分精确迭代和近视迭代。是用来求方程和方程组近似根的方法。
+125 -8
View File
@@ -1,12 +1,14 @@
# 递归
递归是一种设计和描述算法的有力工具。 递归算法执行过程分 `递推` 和 `回归` 两个阶段
递归是一种设计和描述算法的有力工具。 也是回溯法和动态规划的基础。
递归算法执行过程分 `递推` 和 `回归` 两个阶段
在 `递推` 阶段,将大的问题分解成小的问题
在 `回归` 阶段,获得最简单问题的解后,逐级返回,依次得到稍微复杂情况的解,知道获得最终的结果
1) 确定递归公式
2) 确定边界条件
1) 确定递归公式 , 比如 斐波那契数列 问题中的 `fib(n)=fib(n-1)+fib(n-2)`
2) 确定边界条件 bad case
> 自顶向下的递归,自底向上是迭代
@@ -14,6 +16,70 @@
递归运行效率较低,因为有函数调用的开销,递归多次也可能造成栈溢出。
### 递归公式
[快排](../6%20Sort/README.md)
```Java
void quicksort(int *a, int left, int right){
if (left<right)//加上这个,不然有死循环,造成堆栈溢出,这也是递归结束条件
{
int i = partion(a,left,right);//使得局部有序,i作为分隔
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
}
```
归并排
```Java
void sort(int[] nums, int lo, int hi) {
int mid = (lo + hi) / 2;
sort(nums, lo, mid);
sort(nums, mid + 1, hi);
/****** 后序遍历位置 ******/
// 合并两个排好序的子数组
merge(nums, lo, mid, hi);
/************************/
}
```
二叉树
```Java
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
// 前序遍历
traverse(root.left)
// 中序遍历
traverse(root.right)
// 后序遍历
}
```
### 递归树
画递归树,可以很方便地看到是否存在重叠子问题,如果有的话,就可以采用动态规划。
### 其它案例
* 斐波那契数列
* 阶乘计算
* 快速排序
* 链表翻转
* 很多树算法都是递归思想实现, 如 二叉树的最近公共祖先
* 梵塔问题 (三根针1,2,3表示,1号从小到大n个盘子,先要都移到3号上,不能出现大盘压小盘,找出移动次数最少的方案)
## 斐波那契数列
@@ -76,6 +142,7 @@ int fib(int n) {
递归解法
```
public static LinkedNode mergeSeqLink2(LinkedNode l1, LinkedNode l2){
if(l1 == null){
@@ -96,6 +163,7 @@ public static LinkedNode mergeSeqLink2(LinkedNode l1, LinkedNode l2){
```
非递归解法
```
public static LinkedNode mergeSeqLink(LinkedNode l1, LinkedNode l2){
if (l1 == null) return l2;
@@ -130,13 +198,62 @@ public static LinkedNode mergeSeqLink(LinkedNode l1, LinkedNode l2){
## 链表翻转
链表翻转 [代码参考这里](https://github.com/nonstriater/deep-in-java/blob/master/src/main/java/com/nonstriater/deepinjava/algo/link/ReverseLink.java)
```Java
//翻转链表前n个节点
static LinkedNode tmp;//需要一个外部的临时变量
public static LinkedNode reverseN(LinkedNode head, int n){
//LinkedNode tmp = null;
if (n == 1) {
tmp = head.next;
return head;
}
LinkedNode tail = reverseN(head.next, n-1);
head.next.next = head; //后n-1个节点 与 第 1 个节点 指向 翻转
head.next = tmp;
return tail;
}
```
## 二叉树的最近公共祖先
二叉树的最近公共祖先 [代码参考这里](https://github.com/nonstriater/deep-in-java/blob/master/src/main/java/com/nonstriater/deepinjava/algo/tree/CommonParent.java)
```Java
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 情况 1 :p, q 在 root 为根的树中
if (left != null && right != null) {
return root;
}
// 情况 2 :p, q 不在 在 root 为根的树中
if (left == null && right == null) {
return null;
}
// 情况 3 : p, q 只有1个在root 为根的树中
return left == null ? right : left;
}
```
#### 其它案例
阶乘计算
梵塔问题 (三根针1,2,3表示,1号从小到大n个盘子,先要都移到3号上,不能出现大盘压小盘,找出移动次数最少的方案)
快速排序
+13 -3
View File
@@ -4,11 +4,21 @@
* 回文判断
* 字符(串)统计
* [子串查找](1.1%20字符串-查找.md)
* [字符串修改](1.3%20字符串-修改.md) 如单词反转/移动等
* [字符删除](1.2%20字符串-删除.md)
* 字符串的压缩
* 字符串的排列和组合
* [子串查找](1.1%20字符串-查找.md)
* [字符删除](1.2%20字符串-删除.md)
* [字符串修改](1.3%20字符串-修改.md) 如单词反转/移动等
## Java 字符串处理 常用 API
```
String
charAt(index)
char[]
Map<Character,Integer>
```
## 判断字符串是否是回文
@@ -27,11 +27,14 @@ char char_first_appear_once(const unsigned char *source){
int hash[256]={0};
char *tmp = source;
if (tmp == NULL) return '\0';
//第一次建立hash表 key为字符,value为出现次数
while(*tmp != '\0'){
hash[*tmp]++;
tmp++;
}
//第二次扫描找到第一个value为1的key
tmp = source;
while(*tmp != '\0'){
if (hash[*tmp] == 1) return *tmp;
@@ -45,19 +48,6 @@ char char_first_appear_once(const unsigned char *source){
题目扩展:这里的字符换成整数,整数数量几十TB,海量数据处理,显然hash方法不可能,没有那么大得内容
## 请编写能直接实现strstr()函数功能的代码。
> strstr(str1,str2) 判断str2是否是str1的子串。
```
/*
@ret 有就返回第一次出现子串的地址,否则返回NULL
*/
char *strstr(const char *source, const char *target){
}
```
### 字符串中找出连续最长的数字子串
@@ -164,9 +154,68 @@ int lengthOfLongestSubstring(string s) {
## 求最大连续递增数字子串
## 对称子字符串的最大长度 (最长回文子串)
题目:输入一个字符串,输出该字符串中对称的子字符串的最大长度。比如输入字符串“google”,由于该字符串里最长的对称子字符串是“goog”,因此输出4。
分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。
```
int max_symmetrical_char_length(const char *scr);
```
* 思路一:蛮力法,3重循环(类似 求子数组的最大和 fmax(i,j)问题), fmax(i,j)区间i,j是最长的对称字符
* 思路二:遍历所有子串,然后判读是否对称 O(n^2)
* 思路三:有个O(n)复杂度的算法 http://www.cnblogs.com/McQueen1987/p/3559497.html 分析过程如下:
```
int max_symmetrical_char_length(const char *scr){
}
```
```Java
public String longestPalindrome(String s) {
String res = "";
for (int i = 0; i < s.length(); i++) {
// 以 s[i] 为中心的最长回文子串
String s1 = palindrome(s, i, i);
// 以 s[i] 和 s[i+1] 为中心的最长回文子串
String s2 = palindrome(s, i, i + 1);
// res = longest(res, s1, s2)
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
}
return res;
}
//实现一个函数来寻找最长回文串
String palindrome(String s, int l, int r) {
// 防止索引越界
while (l >= 0 && r < s.length()
&& s.charAt(l) == s.charAt(r)) {
// 向两边展开
l--; r++;
}
// 返回以 s[l] 和 s[r] 为中心的最长回文串
return s.substring(l + 1, r);
}
```
## 最小覆盖子串
(难度 hard) `滑动窗口`
给定 字符串 s, t ; 在字符串 s 里找出 包含 t 所有字母的最小子串
eg: s = "ADOBECODEBANC" t = "ABC" 输出: "BANC"
如“ads3sl456789DF3456ld345AA”中的“456789”就是所求。这道题在上一道题目的基础上增加了数字要递增的条件。思路跟上面差不多,碰到不递增的数字就相当于第二个数字串了。
@@ -193,24 +242,22 @@ int longest_common_subsequence(const char *s1,const char *s2, char *common){
```
## 对称子字符串的最大长度
题目:输入一个字符串,输出该字符串中对称的子字符串的最大长度。比如输入字符串“google”,由于该字符串里最长的对称子字符串是“goog”,因此输出4。
## 求最大连续递增数字子串
分析:可能很多人都写过判断一个字符串是不是对称的函数,这个题目可以看成是该函数的加强版。
如“ads3sl456789DF3456ld345AA”中的“456789”就是所求。这道题在上一道题目的基础上增加了数字要递增的条件。思路跟上面差不多,碰到不递增的数字就相当于第二个数字串了。
## 请编写能直接实现strstr()函数功能的代码。
> strstr(str1,str2) 判断str2是否是str1的子串。
```
int max_symmetrical_char_length(const char *scr);
```
* 思路一:蛮力法,3重循环(类似 求子数组的最大和 fmax(i,j)问题), fmax(i,j)区间i,j是最长的对称字符
* 思路二:遍历所有子串,然后判读是否对称 O(n^2)
* 思路三:有个O(n)复杂度的算法 http://www.cnblogs.com/McQueen1987/p/3559497.html 分析过程如下:
```
int max_symmetrical_char_length(const char *scr){
/*
@ret 有就返回第一次出现子串的地址,否则返回NULL
*/
char *strstr(const char *source, const char *target){
}
```
@@ -79,22 +79,6 @@ char *revert_by_word(char *source){
用C语言实现一个revert函数,它的功能是将输入的字符串在原串上倒序后返回。
## 左旋转字符串
>字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。
如把`字符串abcdef`左旋转2位得到`字符串cdefab` 。请实现字符串左旋转的函数。要求时间对长度为n的字符串操作的复杂度为O(n),辅助内存为O(1)。
```
char *left_rotate(char *str,int offset){
}
```
思路: 我们可以abcdef分成两部分,ab和cdef,内部逆序以后,整体再次逆序,就可以得到想要的结果了。
也就是跟上面的问题是同样的问题。
## 替换空格
@@ -138,6 +122,23 @@ char *replace_blank(char *source){
```
## 左旋转字符串
>字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。
如把`字符串abcdef`左旋转2位得到`字符串cdefab` 。请实现字符串左旋转的函数。要求时间对长度为n的字符串操作的复杂度为O(n),辅助内存为O(1)。
```
char *left_rotate(char *str,int offset){
}
```
思路: 我们可以abcdef分成两部分,ab和cdef,内部逆序以后,整体再次逆序,就可以得到想要的结果了。
也就是跟上面的问题是同样的问题。
## 字符串原地压缩
@@ -2,7 +2,6 @@
## 小写字母排在大写字母的前面
有一个由大小写组成的字符串,现在需要对他进行修改,将其中的所有小写字母排在大写字母的前面(大写或小写字母之间不要求保持原来次序),如有可能尽量选择时间和空间效率高的算法 c语言函数原型:
+12 -1
View File
@@ -10,12 +10,14 @@
* 链表中是否有环
链表结点定义如下:
链表结点定义如下:
```
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
```
@@ -110,6 +112,15 @@ struct ComplexNode
请完成函数`ComplexNode* Clone(ComplexNode* pHead)`,以复制一个复杂链表。
这个题目难点在于:
@@ -11,6 +11,19 @@
不需要考虑溢出。
用例:
```
2^3 = 8
0^3 = 0
2^0 = 1
2^-3 = 1/(2^3) = 0.125
0^-3
```
分析:这是一道看起来很简单的问题。可能有不少的人在看到题目后30秒写出如下的代码:
```
double Power(double base, int exponent)
@@ -35,15 +48,6 @@ double Power(double base, int exponent)
2^1 = 2
```
用例:
```
2 3 = 8
0 3 = 0
2 0 = 1
2 -3 = 1/(2^3) = 0.125
0 -3
```
使用递归实现的代码示例:
@@ -62,6 +66,25 @@ double Power(double base, unsigned int exponent)
```
## Sqrt(x)
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
示例 1:
```
输入:x = 4
输出:2
```
示例 2:
```
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
```
## 求根号2的值
并且按照我的需要列出指定小数位,比如根号2是1.141 我要列出1位小数就是1.1 2位就是1.14, 1000位就是1.141...... 等。。
@@ -80,8 +80,6 @@ int fib(int n) {
## 递减数列左移后的数组中找数
一个数组是由一个递减数列左移若干位形成的,比如{4,3,2,1,6,5}
@@ -25,8 +25,6 @@ void sort(int * a, int length)
`动态规划`
## 扑克牌的顺子
@@ -81,7 +81,6 @@ int findaddends(int *data,int length,int sum,int *a,int *b);
## 求子数组的最大和(最大字段和)
输入一个整形数组,数组里有正数也有负数。
@@ -91,7 +90,7 @@ int findaddends(int *data,int length,int sum,int *a,int *b);
例如输入的数组为`1, -2, 3, 10, -4, 7, 2, -5` ,和最大的子数组为`3, 10, -4, 7, 2` ,因此输出为该子数组的和18。
1. 蛮力法 fmax(i,j) 找出最大的值,3重循环 ,复杂度 0(n^3)
2. maxendindhere保存当前累加的和,如果<0,就把maxendinghere清零 , max保存最终的最大和
2. `动态规划` maxendindhere保存当前累加的和,如果<0, 就把maxendinghere清零 , max保存最终的最大和; 如果都是负数
```
int maxSumOfVector(int *data,int length){
@@ -87,13 +87,64 @@ topMaxK(int *a,int length, int k);
## 找第k大的数
比如 1,2,3,4,5,6,7和8这8个数字,低3大的数字是 6
比如 `1,2,3,4,5,6,7,8 `这 8个数字,第3大的数字是 6
```
int topK(int * a, int length, int k)
```
```Java
//冒泡实现
public int findK(int[] nums, int k){
//base case
if (nums== null || nums.length < k) {
return -1;
}
for(int i=1; i<=k; i++){
for(int j=0; j<nums.length-i; j++) {
int next = j+1;
if(nums[j]> nums[next]){
int tmp = nums[j];
nums[j] = nums[next];
nums[next] = tmp;
}
}
}
return nums[nums.length-k];
}
```
```Java
/**
* 小根堆实现
*/
public static int findMaxK(int[] nums, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>(k, (a, b) -> (a-b) );
for (int i = 0; i <nums.length ; i++) {
//取出前k个元素放入 PQ 中
if (i < k) {
pq.add(nums[i]);
continue;
}
Integer head = pq.peek();
if (head < nums[i]) {//维护 priorityQueue 中元素只有k个
pq.poll();
pq.add(nums[i]);
}
}
return pq.poll();
}
```
## 最长公共子序列 (动态规划的经典题目)
@@ -141,4 +192,35 @@ int one_appear_count(int n);
## 搜索旋转排序数组
整数数组 nums 按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
示例 1:
```
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
```
示例 2:
```
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
```
+39 -2
View File
@@ -20,6 +20,21 @@ struct SBinaryTreeNode // a node of the binary tree
};
```
二叉树递归框架
```Java
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
// 前序遍历
traverse(root.left)
// 中序遍历
traverse(root.right)
// 后序遍历
}
```
## 二叉树翻转
@@ -92,8 +107,7 @@ TreeNode invertTree(TreeNode root) {
## 二叉树两个结点的最低共同父结点
## 二叉树两个结点的最低共同父结点 (最近公共祖先)
设计一个算法,找出二叉树上任意两个结点的最近共同父结点。复杂度如果是O(n2)则不得分
@@ -103,6 +117,29 @@ TreeNode invertTree(TreeNode root) {
分析:求数中两个结点的最低共同结点是面试中经常出现的一个问题。这个问题至少有两个变种。
```
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 情况 1 :p, q 在 root 为根的树中
if (left != null && right != null) {
return root;
}
// 情况 2 :p, q 不在 在 root 为根的树中
if (left == null && right == null) {
return null;
}
// 情况 3 : p, q 只有1个在root 为根的树中
return left == null ? right : left;
}
```
## 求一个二叉树中任意两个节点间的最大距离
@@ -49,13 +49,36 @@
分析:这道题本质上还是考查二元树的遍历。对于一颗完全二叉树,要求给所有节点加上一个pNext指针,指向同一层的相邻节点;如果当前节点已经是该层的最后一个节点,则将pNext指针指向NULL;给出程序实现,并分析时间复杂度和空间复杂度。
## 二元树的最小深度
例如:输入二元树:
```
10
/ \
6 14
/ \
12 16
```
输出该树的最小深度2。
## 完全二叉树的节点个数
## 判断整数序列是不是二元查找树的后序遍历结果
题目:输入一个整数数组,判断该数组是不是某二元查找树的后序遍历的结果。
如果是返回true,否则返回false。
例如输入5、7、6、9、11、10、8,由于这一整数序列是如下树的后序遍历结果:
例如输入`5、7、6、9、11、10、8`,由于这一整数序列是如下树的后序遍历结果:
```
8
/ \
@@ -66,3 +89,9 @@
因此返回true。如果输入7、4、6、5,没有哪棵树的后序遍历的结果是这个序列,因此返回false。
@@ -1,16 +1,12 @@
# 二叉树-建树
## 把一个有序整数数组放到二叉树中?
分析:本题考察二叉搜索树的建树方法,简单的递归结构。
关于树的算法设计一定要联想到递归,因为树本身就是递归的定义。
而,学会把递归改称非递归也是一种必要的技术。
毕竟,递归会造成栈溢出,关于系统底层的程序中不到非不得以最好不要用。
但是对某些数学问题,就一定要学会用递归去解决。
关于树的算法设计一定要联想到递归,因为树本身就是递归的定义。而,学会把递归改称非递归也是一种必要的技术。
毕竟,递归会造成栈溢出,关于系统底层的程序中不到非不得以最好不要用。但是对某些数学问题,就一定要学会用递归去解决。
@@ -0,0 +1,27 @@
# 二叉搜索树
关于二叉查找树特性[参考这里](../4%20Tree/2-二叉查找树/二叉查找树.md)
## 验证二叉搜索树
## 二叉搜索树中的节点搜索
## 二叉搜索树中的插入操作
## 删除二叉搜索树中的节点
+280 -17
View File
@@ -2,6 +2,275 @@
这部分内容是算法问题合集,题目大多来自网络和书籍。我做了下简单的整理,很多题做了一些思路标记。
## 小结
面试中如何更好的写算法题
* ide 工具 & 文本框 ;文本框环境写算法是通常没有智能提示,有些基础数据结构可能要自己定义
* 语言基础语法:各常用集合的api,要能够默写
* 题目:熟悉各种题目,避免在题目理解上花太多时间跟面试官拉齐
* 编程思路和框架:拿到题目以后能快速套思路和代码框架
* 编程细节:多练习,总结
## 常用解题套路工具
* 数组,字符串问题:二分查找、 快慢指针、 左右指针、 滑动窗口、 前缀和数组、 差分数组。
* 二叉树问题:递归
* [动态规划问题](../8%20Algorithms%20Analysis/动态规划.md)
* [常见算法题目Java实现](https://github.com/nonstriater/deep-in-java/tree/master/src/main/java/com/nonstriater/deepinjava/algo)
## 刷题框架套路
### 遍历
```
# 数组遍历框架
void traverse(int[] arr) {
for (int i = 0; i < arr.length; i++) {
// 迭代访问 arr[i]
}
}
# 链表遍历框架
void traverse(ListNode head) {
ListNode p = head;
while(p.next != null)
...
p = p.next
}
```
### 递归
```Java
# 链表递归
void traverse(ListNode head) {
// 递归访问 head.val
traverse(head.next);
}
# 二叉树递归
void traverse(TreeNode root) {
traverse(root.left);
traverse(root.right);
}
# 多叉树递归
void traverse(TreeNode root) {
for (TreeNode child : root.children)
traverse(child);
}
# 图的递归:,用个布尔数组 visited 做标记就行了
```
比如,二叉树的最近公共祖先
```Java
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 情况 1 :p, q 在 root 为根的树中
if (left != null && right != null) {
return root;
}
// 情况 2 :p, q 不在 在 root 为根的树中
if (left == null && right == null) {
return null;
}
// 情况 3 : p, q 只有1个在root 为根的树中
return left == null ? right : left;
}
```
### 二分查找
[查找算法](../7%20Search/README.md)
```Java
// 1, 2, 2, 2, 2, 3
public int bsearch(int[] nums, int target){
if(nums == null) {
return -1;
}
int low = 0;
int high = nums.length;// 注意
while(low < high){
middle = (high + low ) / 2;
if(nums[middle] < target){
low = middle + 1;
} else if (nums[middle] == target) {
high = middle;//继续向左早
} else {
high = middle; // 注意 , 这里没有 -1
}
}
return low;
}
```
### 快慢指针
如下判断链表是否有环
```
public static boolean hasCycle(LinkNode head) {
if(head == null) {
return false;
}
LinkNode p = head, q = head;
while(p.next != null && p.next.next != null && q.next != null) {
p = p.next.next;
q = q.next;
if(p == q) {
return true;
}
}
return false;
}
```
### 左右指针
快排中 挖坑(pivot) 排序
```Java
// pivot 选择 尾部节点, 代码写起来更加简单; 移动元素更方便
// 左右指针技巧
static int partition(int[] nums, int left, int right){
int pivot = nums[right];//选尾部节点作为 pivot
int end = right;
right--;
while (left < right) {
if (nums[left] <= pivot) {
left ++ ; //左边指针 窗口变小
continue;
}
//元素比 pivot 大,右边指针 窗口变小
//swap left & right
swap(nums, left, right);
right--;
}
// 跟 pivot 元素置换
int i = 0;
if (nums[left] <= pivot) {
//swap left+1 & pivot
i = left +1;
} else {//swap left & pivot
i = left ;
}
swap(nums, i, end);
return i;
}
```
### 滑动窗口
双指针中有一类比较难的技巧就是`滑动窗口`
滑动窗口: 无重复字符的最长子串
```Java
//如 ”abcabbcbb“ 输出 3
public static int longestSubString(char[] s){
int left = 0, right = 0;
int res = 0; // 记录最长结果
Map<Character, Integer> window = new HashMap<Character, Integer>();
while (right < s.length) {
Character c = s[right];
//窗口变大
right++;
window.put(c, window.getOrDefault(c, 0) + 1); //java写的麻烦,不一定记得这个api
if (window.get(c) > 1) {
//判断左侧窗口是否要收缩
char d = s[left];
left ++;
window.put(d, window.get(d)-1);
}
// 在这里更新答案
res = res > (right-left) ? res: (right-left);
}
return res;
}
```
### 排序算法
[排序算法](../6%20Sort/README.md)
```Java
#快排 quickSort
void quicksort(int[] nums, int left, int right){
if (left<right){//加上这个,不然有死循环,造成堆栈溢出,这也是递归结束条件
//前序遍历位置
//使得局部有序,i作为分隔, i 前面的元素 小于 i 后面的元素
int i = partion(a,left,right);
quicksort(a,left,i-1);
quicksort(a,i+1,right);
}
}
#归并排序 mergeSort
void mergeSort(int[] nums, int left, int right){
int mid = (low + high) / 2;
sort(nums, low, mid);
sort(nums, mid + 1, high);
/****** 后序遍历位置 ******/
// 合并两个排好序的子数组
merge(nums, low, mid, high);
/************************/
}
```
### 动态规划 DP
## [字符串](1%20字符串.md)
@@ -14,7 +283,6 @@
* 字符串比较
* 子串
## [链表](2%20链表.md)
@@ -60,12 +328,13 @@
## [二叉树](7%20二叉树.md)
常见问题
* 遍历
* 翻转
* 子树
解题思路:递归
## [图](8%20图.md)
@@ -73,24 +342,18 @@
## 智力思维训练
## [智力思维训练](9%20智力思维训练.md)
## [《剑指offer》](剑指offer/README.md)
《剑指offer》里面给出了50到高质量的算法问题,很有学习的必要。
## [《编程之美》](编程之美/README.md)
[九度OJ](http://ac.jobdu.com/index.php)
[leetcode 题目](https://leetcode.com/)
# 参考
[九度OJ](http://ac.jobdu.com/index.php)
[leetcode 题目](https://leetcode.com/)
[labuladong 的算法小抄](https://labuladong.gitee.io/algo/1/2/)
[《剑指offer》](剑指offer/README.md) 《剑指offer》里面给出了50到高质量的算法问题,很有学习的必要。
[《编程之美》](编程之美/README.md)
@@ -1,16 +1,16 @@
## Bitmap
# Bitmap
也就是用1个(或几个)bit位来标记某个元素对应的value(如果是1bitmap,就只能是元素是否存在;如果是x-bitmap,还可以是元素出现的次数等信息)。使用bit位来存储信息,在需要的存储空间方面可以大大节省。应用场景有:
1. 排序(如果是1-bitmap,就只能对无重复的数排序)
2. 判断某个元素是否存在
1. 判断某个元素是否存在
2. 排序(如果是1-bitmap,就只能对无重复的数排序)
比如,某文件中有若干8位数字的电话号码,要求统计一共有多少个不同的电话号码?
分析:8位最多99 999 999, 如果1Byte表示1个号码是否存在,需要95MB空间,但是如果1bit表示1个号码是否存在,则只需要 95/8=12MB 的空间。这时,数字k(0~99 999 999)与bit位的对应关系是:
分析:8位最多99 999 999, 如果1Byte表示1个号码是否存在,需要95MB空间,但是如果1bit表示1个号码是否存在,则只需要 95/8=12MB 的空间。这时,数字 `k(0~99 999 999)`与bit位的对应关系是:
```
#define SIZE 15*1024*1024
@@ -1,4 +1,3 @@
# Bloom filter(布隆过滤器)
Bloom Filter是由Bloom在1970年提出的一种多哈希函数映射的快速查找算法。通常应用在海量数据处理中,一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合(容忍错误的场景)。
@@ -0,0 +1,20 @@
# Hash映射,分而治之
这里的`Hash映射`是指通过一种映射散列的方式,将海量数据均匀分布在对应的内存或更小的文件中
使用hash映射有个最重要的特点是: `hash值相同的两个串不一定一样,但是两个一样的字符串hash值一定相等`。哈希函数如下:
```
int hash = 0;
for (int i=0;i<s.length();i++){
hash = (R*hash +s.charAt(i)%M);
}
```
大文件映射成多个小文件。具体操作是,比如要拆分到100(M)个文件:
1. 对大文件中的每条记录求hash值,然后对M取余数,即 `hash(R)%M`,结果为K
2. 将记录R按结果K分配到第K个文件,从而完成数据拆分
这样,两条相同的记录肯定会被分配到同一个文件。
@@ -1,5 +1,4 @@
## 倒排索引(Inverted Index)
# 倒排索引(Inverted Index)
常规的索引是文档到关键词的映射,就是每个文档指向一个它所包含的索引项的序列,也就是文档文档指向了它包含的索引项序列,也就是文档指向它包含的哪些单词。
@@ -35,6 +34,15 @@ T2 = "it is a banana"
- 在加入新文章时,我们需要为新文章中的关键词索引和之前的合并
### 应用场景
* mysql中的索引实现,使用 B+ 树实现
* ES
* 推荐内容召回
### 参考
@@ -1,5 +1,4 @@
## 数据库索引
# 数据库索引
索引使用的数据结构多是B树或B+树。B树和B+树广泛应用于文件存储系统和数据库系统中,mysql使用的是B+树,oracle使用的是B树,Mysql也支持多种索引类型,如b-tree 索引,哈希索引,全文索引等。
+8 -8
View File
@@ -6,15 +6,15 @@
针对空间,就一个办法,大而化小,分而治之。常采用hash映射
* Hash映射/分而治之
* Bitmap
* Bloom filter(布隆过滤器)
* 双层桶划分
* Trie树
* 数据库索引
* [Hash映射,分而治之](Hash映射,分而治之.md)
* [Bitmap](Bitmap.md)
* [Bloom filter(布隆过滤器)](Bloomfilter.md)
* [双层桶划分](双层桶划分.md)
* [Trie树](../4%20Tree/4-字典树Trie/README.md)
* [数据库索引](Inverted%20Index/数据库索引.md)
* 倒排索引(Inverted Index)
* 外排序
* simhash算法
* [外排序](../6%20Sort/外排序.md)
* [simhash算法](simhash算法.md)
* 分布处理之Mapreduce
+1 -3
View File
@@ -1,6 +1,4 @@
## 双层桶划分
# 双层桶划分
双层桶不是一种数据结构,只是一种算法思维。分而治之思想。
+1 -2
View File
@@ -1,5 +1,4 @@
## 数据库系统中的算法
# 数据库系统中的算法
最近开始读《数据库系统实现》这本书,所以就想到把数据库里面用到的数据结构和算法做一个梳理。就有了这些文字。
+4 -5
View File
@@ -1,5 +1,4 @@
## 15个经典基础算法
# 15个经典基础算法
* A*寻路算法 :求解最短路径
@@ -15,9 +14,9 @@
* Hash
* 快速排序
* SPFA(shortest path faster algorithm) 单元最短路径算法
* 快递选择SELECT
* 快递选择SELECT
### Dijkstra 最短路径算法
**Dijkstra** 是荷兰的计算机科学家,提出”信号量和PV原语“,"解决哲学家就餐问题",”死锁“也是它提出来的
+56 -44
View File
@@ -22,6 +22,12 @@
* 链表
* 双向链表
### [数组](2%20List/数组.md)
* [数组数列问题](9%20Algorithms%20Job%20Interview/5%20数组数列问题.md)
数组和链表结构是基础结构,散列表、栈、队列、堆、树、图等等各种数据结构都基于数组和链表结构实现。
### [队列](2%20Queue/README.md)
* 队列
@@ -34,31 +40,30 @@
### [字符串算法](1%20String/README.md)
* 排序
* 查找
* 子串查找 [字符串常见题目参考这里](9%20Algorithms%20Job%20Interview/1%20字符串.md)
* BF算法
* KMP算法
* BM算法
* 正则表达式
* 数据压缩
* 排序
### [树](4%20Tree/README.md)
* 二叉树
* 二叉查找树
* 伸展树(splay tree 分裂树)
* 平衡二叉树AVL
* 红黑树
* B树,B+,B*
* R树
* Trie树(前缀树)
* 后缀树
* 最优二叉树(赫夫曼树)
* 二叉堆 (大根堆,小根堆)
* 二项树
* 二项堆
* 二叉树 [快速排序](6%20Sort/README.md)就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历
* [二叉查找树BST](4%20Tree/2-二叉查找树/二叉查找树.md) 有序的二叉树,中序遍历结果是递增的
* [平衡二叉树 AVL树](4%20Tree/3-平衡树AVL/README.md) 绝对平衡二叉树;
* [红黑树](4%20Tree/9-红黑树%20R-B%20tree/红黑树.md) 弱平衡二叉树;使用广泛
* [B树](4%20Tree/7-B树/B树.md)
* [B+树](4%20Tree/7-B树/B+树.md) mysql 索引使用 B+树 的数据结构
* [字典树trie](4%20Tree/4-字典树Trie/README.md) 字典树也叫前缀树,单词查找树
* [二叉堆](4%20Tree/8-堆/堆.md)
* [伸展树](4%20Tree/5-伸展树/伸展树.md)
* [后缀树](4%20Tree/6-后缀树/后缀树.md)
* 斐波那契堆(Fibonacci Heap)
* 最优二叉树(赫夫曼树)
### [图的算法](5%20Graph/README.md)
@@ -90,22 +95,25 @@
### [查找算法](7%20Search/README.md)
* 顺序表查找:顺序查找
* 有序表查找:二分查找
* 哈希表: O(1) [hashtable实现参考这里](../3%20Hash%20Table/README.md)
* 有序表查找:二分查找
* 顺序表查找:顺序查找, 复杂度O(N)
* 分块查找: 块内无序,块之间有序;可以先二分查找定位到块,然后再到`块`中顺序查找
* 动态查找: 二叉排序树,AVL树,B- ,B+(这里之所以叫 `动态查找表`,是因为表结构是查找的过程中动态生成的)
* 哈希表: O(1)
## [算法设计思想](8%20Algorithms%20Analysis/README.md)
* 迭代法
* 穷举搜索法
* 递推法
* 动态规划
* 贪心算法
* 回溯
* 分治算法
* [递归](8%20Algorithms%20Analysis/递归.md)
* [分治算法](8%20Algorithms%20Analysis/分治算法.md)
* [动态规划](8%20Algorithms%20Analysis/动态规划.md)
* [回溯法](8%20Algorithms%20Analysis/回溯法.md)
* [迭代法](8%20Algorithms%20Analysis/迭代法.md)
* [穷举搜索法](8%20Algorithms%20Analysis/穷举搜索法.md)
* [贪心算法](8%20Algorithms%20Analysis/贪心算法.md)
@@ -163,22 +171,21 @@
## 15个经典基础算法
* Hash
* 快速排序
* 快递选择SELECT
* BFS/DFS (广度/深度优先遍历)
* 红黑树 (一种自平衡的`二叉查找树`)
* KMP 字符串匹配算法
* DP (动态规划 dynamic programming)
* A*寻路算法: 求解最短路径
* Dijkstra:最短路径算法 (八卦下:Dijkstra是荷兰的计算机科学家,提出”信号量和PV原语“,"解决哲学家就餐问题",”死锁“也是它提出来的)
* 遗传算法
* 启发式搜索
* 图像特征提取之 SIFT 算法
* [KMP 字符串匹配算法](1%20String/KMP.md)
* [Hash](3%20Hash%20Table/README.md)
* [快速排序](6%20Sort/README.md)
* 快速选择SELECT
* [红黑树 (一种弱/自平衡的`二叉查找树`)](4%20Tree/9-红黑树%20R-B%20tree/红黑树.md)
* [BFS/DFS (广度/深度优先遍历)](5%20Graph/DFS%20和%20BFS.md)
* [`A*`寻路算法: 求解最短路径](5%20Graph/最短路径.md)
* Dijkstra:最短路径算法
* `SPFA(Shortest Path Faster Algorithm)` 单元最短路径算法
* 启发式搜索
* 遗传算法 `GA`
* [DP (动态规划 dynamic programming)](8%20Algorithms%20Analysis/动态规划.md)
* 图像特征提取之`SIFT` 算法 , 广泛的应用于图像识别,图像检索,3D重建等CV的各种领域
* 傅立叶变换
* SPFA(shortest path faster algorithm) 单元最短路径算法
## 推荐阅读
@@ -240,21 +247,26 @@ http://www.cs.usfca.edu/~galles/visualization/Algorithms.html
### 编程网站
* [leetcode](http://leetcode.com/)
* [leetcode](http://leetcode.com/)
* [codetop](https://codetop.cc/home) 企业高频面试题库,刷题必备
* [openjudge](http://openjudge.cn/) 开放在线程序评测平台,可以创建自己的OJ小组  
* [九度OJ](http://ac.jobdu.com/index.php)
* 这有个[ACM训练方案](http://www.java3z.com/cwbwebhome/article/article19/res041.html)
### 其它
### 网课
[高级数据结构和算法](https://www.coursera.org/learn/gaoji-shuju-jiegou/) 北大教授张铭老师在coursera上的课程。完成这门课之时,你将掌握多维数组、广义表、Trie树、AVL树、伸展树等高级数据结构,并结合内排序、外排序、检索、索引有关的算法,高效地解决现实生活中一些比较复杂的应用问题。当然coursera上也还有很多其它算法方面的视频课程。
[算法设计与分析 Design and Analysis of Algorithms](https://class.coursera.org/algorithms-001/lecture) 由北大教授Wanling Qu在coursera讲授的一门算法课程。首先介绍一些与算法有关的基础知识,然后阐述经典的算法设计思想和分析技术,主要涉及的算法设计技术是:分治策略、动态规划、贪心法、回溯与分支限界等。每个视频都配有相应的讲义(pdf文件)以便阅读和复习。
[OI Wiki](https://github.com/24OI/OI-wiki/) 主要内容是 OI / ACM-ICPC 相关的知识整理。
### 其它
[OI Wiki](https://github.com/24OI/OI-wiki/) 主要内容是 OI/ACM-ICPC 编程竞赛 (competitive programming) 相关的知识整理, 包括基础知识、常见题型、解题思路以及常用工具等内容。
[labuladong 的算法小抄](https://labuladong.gitee.io/algo/) 作者整理了很多的解题套路框架,看完获益良多
## 联系