Learn and practice algorithms and data structures. 来源包括但不限于《算法导论》, hankerrank, leecode, 北京大学 PKU JudgeOnline, etc.
编译与运行主要在Linux
上进行, 但代码都应属于平台无关的, 理论上不限于操作系统/编译器等.
Ubuntu 18.04 LTS
:WSL(Windows Subsystem for Linux) on Win10 1803-17134.228
cmake version 3.10.2
gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0
go version go1.10.1 linux/amd64
虽说都是实验代码, 但大部分提供了封装好的golang pkg
, 可直接导入作为基础库使用. 内部进行实验时, 也尽可能地拆分成pkg
以解耦.
学习各种排序的实验代码,主要参考《算法导论 第3版》,
- 包括:
- 比较排序算法: 插入排序(Insertion Sort)、归并排序(Merge Sort)、堆排序(Heap Sort)、快速排序(Quick Sort)
- 非比较排序算法: 计数排序(Counting Sort)、基数排序(Radix Sort)、桶排序(Bucket Sort)
- 其他算法: 选择第
n-th
元素(Select n-th Element)、基于Binary Search Tree的排序(Tree Sort)
- 详见:
pkg - mysorts
- maxsubarray
解决最大子数组问题(Maximum Subarray Problem)的实验代码.divide_and_conquer_algorithm.go
: O(n*log(n)) 实现《算法导论 第3版》 ch4.1 最大子数组问题 中介绍的基于分治法(Divide and Conquer Alogrithm)的实现. 其基本原理为将原数组分解为两个子数组, 分别求解两个子数组的最大子数组及同时跨两个子数组的最大子数组, 取最大值为最大子数组. 递归地执行这个过程直至求到最终解.violent_solution.go
: O(n^2) 实现暴力方法求解, 即遍历所有子数组的可能, 从而取出最大子数组.kadane_algorithm.go
: O(n) 实现《算法导论 第3版》 习题4.1-5 中所描述的线性算法. 基本原理为同时记录到目前为止的max subarray
以及以当前为止为ending
的max subarray
, 遍历下一个元素时, 要得到最大子数组, 要么为max subarray so far
, 要么为 max(max subarray ending here
+new element
,new element
). 伪代码可参考Maximum Subarray Problem - Kadane's algorithm.
- cc_heaps
《算法导论 第3版》 ch6.5 介绍的基于最大堆/最小堆的优先队列的设计.- 最大优先队列Max Priority Queue一般支持
Insert/Maximum/ExtractMax/IncreaseKey/Delete
等操作(最小优先队列对应的为Insert/Minimum/ExtractMin/DecreaseKey/Delete
). - 其基本原理仍然是基于最大堆/最小堆(与堆排序的原理非常类似), 即
Insert
时将新元素放在数组末尾然后进行up
操作, 而ExtractMax/Delete
时将对应元素与末尾元素交换后, 对当前元素进行down
(i.e.MaxHeapity
)操作, 从而维护堆的性质. 特殊处在于IncreaseKey/DecreaseKey
操作, 需要记录针对每一个HeapNode
的handle
才能发起此操作. 实验中的handle
直接用的是HeapNode
的指针来表示.(std::priority_queue干脆就没有暴露这个接口, 所以看起来接口简单很多.) - NOTE:
- 此处的实现代码, 基于
BinaryHeap
进行实现的, 且仅实现了MaxPriorityQueue
. 而实际上同样可以基于d-ary heap
来实现, 区别只是2-dry
或d-ary
. Golang
中目前不支持泛型, 也没有class/object
等概念, 不太好实现一个封装起来的PriorityQueue
.Golang
源码库中的实现是给出了一些heap
的操作函数Fix/Init/Push/Pop
等, 而把内部的数据结构实现交给了使用者. 实现和使用思路都与一般的面向对象思路不同, 个人感觉是不太直观的, 所以这里用c++
来实现了此处的实验代码, 看起来或许更直观些.- 另外, 下面的包
minspanningtree
中实现Prim
算法时用到了MinHeap
, 除借助golang
的"container/heap"
中提供的方法外, 也自己实现了一份在my_min_binary_heap.go
中, 可供参考.
- 另外, 下面的包
- 当前代码中暂未考虑效率问题(如
object
的多处拷贝等), 实际使用的话应考虑优化, 如多采用std::move
等. 另可参考boost::heap::d_ary_heap
, Fibonacci heap等.
- 此处的实现代码, 基于
g++ -std=c++11 main.cc && ./a.out
- 最大优先队列Max Priority Queue一般支持
- binarysearchtree
实现《算法导论 第3版》ch12 介绍的二叉搜索树的实验代码.- 二叉搜索树性质: 设
x
为二叉搜索树中的一个节点, 若y
是x
的左子树中的一个节点, 则y.key <= x.key
; 若y
是x
的右子树中的一个节点, 则y.key >= x.key
. - 二叉搜索树实现时, 一般每个
node
中都会记录parent/leftChild/rightChild
三个指针以维护树的结构, 同时node
中需记录key
以维护二叉搜索树的性质. 可选的node
中可能会记录额外的payload
. - 二叉搜索树一般至少会提供接口: (实现在
binary_search_tree.go
中)Minimum()/Maximum()
:O(h)
返回最小/最大key
的节点(同MinHeap/MaxHeap
中的Minimum()/Maximum()
)- 故Binary Search Tree也可以作为一个可以同时方便地查询
Max/Min
的Priority Queue来使用.
- 故Binary Search Tree也可以作为一个可以同时方便地查询
Successor()/Predecessor()
:O(h)
返回当前node
的Succesor/Predecessor
节点Successor
节点:>= node.key
的最小key
节点Predecessor
节点:<= node.key
的最大key
节点
Insert()
:O(h)
插入一个新的节点(总是会插入为tree
的leaf
)Delete()
:O(h)
删除一个新的节点. 实现上最复杂的一个接口, 主要是要删除的节点同时存在leftChild
和rightChild
时的情况比较复杂.Search()
:O(h)
搜索一个指定key
的node
.InorderTreeWalk()
:O(n)
中序遍历, 即总是按照node.leftChild => node => node.rightChild
的顺序递归遍历.- 注: 由于二叉搜索树的性质, 中序遍历的结果总是按照
key
升序排序的. 也即可以通过二叉搜索树来实现排序. 我的实验代码见 mysorts - tree sort.
- 注: 由于二叉搜索树的性质, 中序遍历的结果总是按照
PreorderTreeWalk()
:O(n)
先序遍历, 即总是按照node => node.leftChild => node.rightChild
的顺序递归遍历.postorderTreeWalk()
:O(n)
后续遍历, 即总是按照node.leftChild => node.rightChild => node
的顺序递归遍历.
- 注: 以上分析的
O(n)
中的n
为树中的总的节点数,O(h)
中的h
为树的高度, 最坏情况下h == n
, 但平均情况接近最好情况即lg(n)
. 也即平均情况下Binary Search Tree的接口的运行时间为O(lg(n))
.
- 二叉搜索树性质: 设
- rbtree
实现《算法导论 第3版》ch13 介绍的红黑树的实验代码.- 红黑树的性质(满足红黑性质的二叉搜索树即为红黑树):
- 每个
node
都有一个color
的属性, 要么是红色, 要么是黑色; root node
总是黑色的;- 每个
leat node
总是黑色的(为了实现的简便, 一般总是用一个sentinal nil node
作为实现时的nil
, 也即每个leaf node
都是这个sentinal nil node
, 只要它置为黑色即可满足此性质); - 如果一个
node
是红色的, 那么它的两个子node
总是黑色的; - 对于每个
node
, 从该node
到其所有后代leaf node
的简单路径上, 均包含相同数目的黑色node
.
- 每个
- 黑高(Black-Height)的定义: 从某个
node
出发(不包含该node
)到达一个leaf node
的任意一条简单路径上的黑色node
数目, 即为黑高(Black-Height), 一般记作bh(node)
.- 定义
root
节点的黑高为红黑树的黑高.
- 定义
- 红黑树的
Insert/Delete
除了类似二叉搜索树的操作外, 都需要去维护红黑性质; 而维护了红黑性质后, 红黑树非常接近平衡树, 从而可以带来较好的平均性能. - 红黑树提供的接口与二叉搜索树完全一致, 复杂度分析也同样一致. 只是由于红黑树几乎为平衡二叉树, 树的高度总是为
log(n)
的, 所以接口的运行时间总是可以做到O(log(n))
. - 非常好的学习资料(红黑树的
Insert
和Delete
过程比较复杂, 以下资料个人认为讲的远好于书上. 而可能本质上就是需要枚举出所有的可能情况, 相对来讲书上枚举的不够详细. 跟着教程自己推导一遍, 收货颇丰):
- 红黑树的性质(满足红黑性质的二叉搜索树即为红黑树):
-
graph
通常使用符号 G(V, E) 来表示一张图, 其中 V 为点数, E 为边数. 此pkg
定义了一堆表示graph
的类型与通用接口, 支持包括邻接链表(Adjacency List)和邻接矩阵(Adjacency Matrix)两种图的表示方法. 其中点通过从0
开始的uint
来表示, 所以邻接链表和邻接矩阵都基于了基础的slice
来实现. 基础概念可参考《算法导论 第3版》 ch22.1 图的表示.
注: 图论中的各种涉及到路径的算法, 通常都基于point-to-point
来讨论, 而不是edge-to-edge
, 从graph
的表现形式就可以反映出这一点. -
graphsamples
构建好的graph
的例子, 方便进行各种测试. 详见 graphsamples. -
bfs
- O(V+E)
- 实现《算法导论 第3版》ch22.2 广度优先搜索 中的算法描述, Breadth First Search. 基本思路为搜索过程中从
queue
(借助其先入先出的特性)头上取下一次迭代的初始节点, 并将迭代到的节点存储到queue
尾, 从而实现广度优先. 搜索过程中的tree
的信息及depth
等通过节点属性的形式保存在一个节点数组中. - 提供了基于
bfs
的生成level graph
的实现, 以供dinic
算法使用.
-
cmd/test_bfs
执行package bfs
代码的main
. -
dfs
- O(V+E)
- 实现《算法导论 第3版》ch22.3 深度优先搜索 中的算法描述. 本书章节中的伪码是基于递归的描述, 比较清晰易懂也容易实现. 实际
coding
时同时参考Depth-first search - Wikipedia实现了基于栈的实现. 注: 基于栈的实现一般来讲结果会和递归的实现不太一样, 主要是遍历的次序关系. - 《算法导论 第3版》ch22.4 拓扑排序(Topological Sorting) 也是依赖 Depth-first search - Wikipedia 的方法来实现的, 故其实现也放在了
dfs
包中. 由于graph
中若存在环是无法拓扑排序的, 所以拓扑排序只能基于Directed Acyclic Graph, 依据此也可以通过dfs
判断一个graph
是否为Directed Acyclic Graph.- 拓扑排序的一个典型应用: 比如要做一个Continuous Delivery比较复杂的Pipeline(Pipeline上许多步骤间有依赖关系), 那么便可以先把步骤的依赖关系图先画出来, 算出拓扑排序, 再按照拓扑排序的顺序来梳理Pipeline的流程.
- 《算法导论 第3版》ch22.5 强连通分量(Strongly Connected Component) 同样依赖2次 Depth-first search - Wikipedia 实现, 于是实验代码也放在
dfs
包中. 第一次基于默认graph
进行DFS
, 然后以timestampF
倒序作为第二次遍历的node
顺序; 第二次基于graph
的转置(反转所有edge
)来进行dfs
, 然后以timestampF
正序来进行遍历并输出Strongly Connected Component, 以每个dfs tree
的root
作为切分.- 注: 第一次
dfs
后的timestampF
倒序, 看起来为上述拓扑排序的结果的倒序即可. 但实际上主要区别在于拓扑排序的graph
必须无环, 而切分强连通分量几乎必然有环(否则就只好每个node
为一个强连通分量). 所以并不能直接调用拓扑排序的函数实现.
- 注: 第一次
-
levelgraph
Dinic
算法所要用到的分层图, 即以Depth-first search - Wikipedia在graph
上进行搜索, 以每个node
的depth
作为level
. -
flownetwork
描述maximum flow problem的流网络, 主要包含基于directed graph
的图以及图上每两个node
间(i.e.edge
)的容量. 需要注意的是两个node
间只能有单向的edge
, 不能有反向. 同时为了描述maximum flow problem问题, 也记录了入点和出点.
NOTE:flownetwork
与weightedgraph
非常相似, 所以其实完全可以基于weightedgraph
来实现. 此处仅仅是因为flownetwork
有点类似于描述maximum flow problem的一个专用数据结构, 所以减少了依赖. 有必要的时候可以进行 refactor 以去除重复代码. -
maxflow
maximum flow problem的算法实现, 包括FordFulkerson
,EmondsKarp
,Dinic
, etc. 在其内部的_test.go
中以Drainage Ditches问题作为典型的测试用例.FordFulkerson
: O(E|f|) 基础的最大流问题解决方法. 定义了flow
,residual network
,augmenting path
等重要的基础概念, 以及解决问题的一般思路.EmondKarp
: O(V(E^2)) 基于FordFulkerson
, 在如何寻找augmenting path
的方法上进行了扩展优化, 即以Breadth First Search来寻找点到点的最短路径, 效率更高.Dinic
: O((V^2)E) 依然是基于FordFulkerson
的方法, 最主要的区别在于在生成residual network
后, 先采用Breadth First Search来生成分层图(level graph
, 以每个node
的depth
作为其层次), 再在level graph
上寻找blocking flow
(即直到不能再找到新的flow
), 以此blocking flow
作为residual network
上的augmenting flow
.
-
cmd/test_maxflow
调用maxflow
以解决maximum flow problem, 支持从stdin
来构造flownetwork
, 以更容易测试新的问题. -
weightedgraph
即在package graph
定义的directed/undirected graph
的基础操作上, 为每个edge
增加一个weight
值. -
minspanningtree
《算法导论 第3版》ch23 中所描述的最小生成树问题, 即在一张每条edge
都有其weight
的连通图上(一般是基于undirected graph
讨论), 找到经过的所有node
的sum(weight)
最小的生成树.- 此处实验中实现了书上提到的两种算法:
- Kruskal: O(E*log(V)) 按
weight
排序所有的edge
, 从最小weight
的edge
开始一条一条取出来生成树. 每条取出的edge
的两个node
不属于同一个集合, 才是有效的edge
. - Prim: O(E*log(V)) 借助了最小堆的概念, 从任意
node
开始构造最小堆, 每次从ExtractMin
的node
遍历邻接的node
, 并将经过的edge
的weight
作为遍历到的node
的key
以更新维护最小堆的性质, 于是每次ExtractMin
的node
与它的parent
间的edge
一定为最小生成树的一部分.- 注: 据说借助Fibonacci heap来实现可以进一步优化运行效率.
- Kruskal: O(E*log(V)) 按
- 注: 关于最小生成树的问题, 个人认为几个视频讲的比书上要好, FYI:
- 此处实验中实现了书上提到的两种算法:
-
shortestpaths
《算法导论 第3版》ch24 中所讨论的 Single-Source Shorest Paths 问题, 即在一张每条edge
都有其weight
的连通图上(一般是基于directed graph
, 但其实只要weight
非负, 也完全可以在undirected graph
上讨论), 找到从某个source-node
开始到其他任意node
的最短weight
路径.bellman-ford.go
: O(VE) 迭代V-1
次, 每次所有的edge
以更新每个node
上的cost
(从source-node
到当前node
). 最后再迭代一次所有的edge
以判断是非存在negative-cycle
.bellman-ford
算法实现比较简单, 但迭代次数较多, 主要优势是可以支持negative weight
(不能有negative-cycle
).dijkstra.go
: O((V+E)log(V)) 基于PriorityQueue
每次总是以最小weight
的node
开始relax
, 从而可以大大减少迭代的次数. 此处的复杂度是基于二叉堆实现的PriorityQueue
来分析的.Dijkstra
算法必须要求weight
为非负的.- 注意:
- 参照书上伪代码实现的此
Dijkstra
, 流程是首先将所有的node-priority pair
加入到PriroityQueue
中, 过程中有priority
发生变化时再DecreaseKey
, 所以会有非常大量的DecreaseKey
动作. - 但实际工程上实现时多半都是先只将
source-node
加入到PriorityQueue
中, 过程中再将新relax
到的node
加入PriorityQueue
, 所以会有大量的Push
动作,DecreaseKey
则大大减少, 同时PriorityQueue
的规模也是动态增加的而不是一开始加满. - 这也是为什么学术上经常会讨论如何优化
DecreaseKey
效率从而提升Dijkstra
的性能, 但却并不适用于工程实现.
- 参照书上伪代码实现的此
- 注意:
directed_acyclic_graph_shortest_paths.go
: O(V+E) 先将所有的node
使用dfs
拓扑排序, 再按照拓扑排序的顺序从source-node
开始遍历一次edge
即可. 适用于有向无环图的特殊情况, 复杂度主要是由于拓扑排序.
- 《算法导论 第3版》
- Package sort
- Package heap
- Maximum Subarray Problem
- Divide and Conquer Alogrithm
- Maximum Subarray Problem - Kadane's algorithm
- Priority Queue
- std::priority_queue
- Fibonacci heap
- Binary Search Tree
- Tree Traversal
- Breadth First Search
- Depth-first search - Wikipedia
- Topological Sorting
- Directed Acyclic Graph
- Continuous Delivery
- Strongly Connected Component
- maximum flow problem
- Drainage Ditches
- Youtube Minimum Spanning Tree
- Youtube Kruskal Algorithm
- Youtube Prim Algorithm
- Red Black Tree
- Youtube 红黑树的插入
- 红黑树的删除
- Single-Source Shorest Paths
- Dijkstra Algorithm