DAG是公認的下一代區塊鏈的標志。本文從算法基礎去研究分析DAG算法,以及它是如何運用到區塊鏈中,解決了當前區塊鏈的哪些問題。
關鍵字:DAG,有向無環圖,算法,背包,深度優先搜索,棧,BlockChain,區塊鏈
圖
圖是數據結構中最為復雜的一種,我在上大學的時候,圖的這一章會被老師划到考試范圍之外,作為我們的課后興趣部分。但實際上,圖在信息化社會中的應用非常廣泛。圖主要包括:
- 無向圖,結點的簡單連接
- 有向圖,連接有方向性
- 加權圖,連接帶有權值
- 加權有向圖,連接既有方向性,又帶有權值
圖是由一組頂點和一組能夠將兩個頂點相連的邊組成。
常見的地圖,電路,網絡等都是圖的結構。
術語
- 頂點:圖中的一個點
- 邊:連接兩個頂點的線段叫做邊,edge
- 相鄰的:一個邊的兩頭的頂點稱為是相鄰的頂點
- 度數:由一個頂點出發,有幾條邊就稱該頂點有幾度,或者該頂點的度數是幾,degree
- 路徑:通過邊來連接,按順序的從一個頂點到另一個頂點中間經過的頂點集合
- 簡單路徑:沒有重復頂點的路徑
- 環:至少含有一條邊,並且起點和終點都是同一個頂點的路徑
- 簡單環:不含有重復頂點和邊的環
- 連通的:當從一個頂點出發可以通過至少一條邊到達另一個頂點,我們就說這兩個頂點是連通的
- 連通圖:如果一個圖中,從任意頂點均存在一條邊可以到達另一個任意頂點,我們就說這個圖是個連通圖
- 無環圖:是一種不包含環的圖
- 稀疏圖:圖中每個頂點的度數都不是很高,看起來很稀疏
- 稠密圖:圖中的每個頂點的度數都很高,看起來很稠密
- 二分圖:可以將圖中所有頂點分為兩部分的圖
所以樹其實就是一種無環連通圖。
有向圖
有向圖是一幅有方向性的圖,由一組頂點和有向邊組成。所以,大白話來講,有向圖是包括箭頭來代表方向的。
常見的例如食物鏈,網絡通信等都是有向圖的結構。
術語
上面我們介紹了頂點的度數,在有向圖中,頂點被細分為了:
- 出度:由一個頂點出發的邊的總數
- 入度:指向一個頂點的邊的總數
接着,由於有向圖的方向性,一條邊的出發點稱為頭,指向點稱為尾。
- 有向路徑:圖中的一組頂點可以滿足從其中任意一個頂點出發,都存在一條有向邊指向這組頂點中的另一個。
- 有向環:至少含有一條邊的起點和終點都是同一個頂點的一條有向路徑。
- 簡單有向環:一條不含有重復頂點和邊的環。
- 路徑或環的長度就是他們包含的邊數。
圖的連通性在有向圖中表現為可達性,由於邊的方向性,可達性必須是通過頂點出發的邊的正確方向,與另一個頂點可連通。
鄰接表數組
可表示圖的數據類型,意思就是如何通過一個具體的文件內容,來表示出一幅圖的所有頂點,以及頂點間的邊。
鄰接表數組,以頂點為索引(注意頂點沒有權值,只有順序,因此是從0開始的順序值),其中每個元素都是和該頂點相鄰的頂點列表。
5 vertices, 3 edges
0: 4 1
1: 0
2:
3:
4:
背包
做一個背包集合,用來存儲與一個頂點連通的頂點集合,因為不在意存儲順序,並且只進不出,所以選擇背包結構來存儲。溫習一下背包
package algorithms.bag;
import java.util.Iterator;
// 定義一個背包集合,支持泛型,支持迭代
public class Bag<Item> implements Iterable<Item> {
private class BagNode<Item> {
Item item;
BagNode next;
}
BagNode head;
int size;
@Override
public Iterator<Item> iterator() {
return new Iterator<Item>() {
BagNode node = head;
@Override
public boolean hasNext() {
return node.next != null;
}
@Override
public Item next() {
Item item = (Item) node.item;
node = node.next;
return item;
}
};
}
public Bag() {
head = new BagNode();
size = 0;
}
// 往前插入
public void add(Item item) {
BagNode temp = new BagNode();
// 以下兩行代碼一定要聲明,不可直接使用temp = head,那樣temp賦值的是head的引用,對head的所有修改會直接同步到temp,temp就不具備緩存的功能,引發bug。。
temp.next = head.next;
temp.item = head.item;
head.item = item;
head.next = temp;
size++;
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return this.size;
}
public static void main(String[] args) {
Bag<String> bags = new Bag();
bags.add("hello");
bags.add("yeah");
bags.add("liu wen bin");
bags.add("seminar");
bags.add("1243");
System.out.println(bags.size);
// for (Iterator i = bags.iterator(); i.hasNext(); ) {
// System.out.println(i.next());
// }
// 由於Bag實現了Iterable接口,所以支持以下方式遍歷
for (String a : bags) {
System.out.println(a);
}
}
}
有向圖結構
下面代碼實現一個有向圖數據結構,並添加常用有向圖屬性和功能。
package algorithms.graph;
import algorithms.bag.Bag;
import ioutil.In;
import ioutil.StdOut;
import java.io.FileReader;
public class Digraph {
private final int V;// 頂點總數,定義final,第一次初始化以后不可更改。
private int E;// 邊總數
private Bag<Integer>[] adj;// {鄰接表}頂點為數組下標,值為當前下標為頂點值所連通的頂點個數。
public Digraph(int v) {
this.V = v;
this.E = 0;
adj = new Bag[V];
for (int i = 0; i < V; i++) {
adj[i] = new Bag<Integer>();
}
}
public Digraph(In in) {
this(in.readInt());
int E = in.readInt();
for (int i = 0; i < E; i++) {
int v = in.readInt();
int w = in.readInt();
addEdge(v, w);
}
}
public int V() {
return this.V;
}
public int E() {
return this.E;
}
/**
* v和w是兩個頂點,中間加一條邊,增加稠密度。
*
* @param v 大V是頂點總數,v是頂點值,所以並v不存在大小限制
* @param w 同上。
*/
public void addEdge(int v, int w) {
adj[v].add(w);
E++;
}
/**
* 返回一個頂點的連通頂點集合的迭代器
*
* @param v
* @return Bag本身就是迭代器,所以返回該頂點的連通頂點集合Bag即可。
*/
public Iterable<Integer> adj(int v) {
return adj[v];
}
/**
* 將圖中所有方向反轉
*
* @return 返回一個圖將所有方向反轉后的副本
*/
public Digraph reverse() {
Digraph R = new Digraph(V);
for (int v = 0; v < V; v++) {
for (int w : adj[v]) {// 遍歷原圖中跟v頂點連通的頂點w。
R.addEdge(w, v);
}
}
return R;
}
/**
* 按照鄰接表數組結構輸出有向圖內容
*
* @return
*/
public String toString() {
String s = V + " vertices, " + E + " edges\n";
for (int v = 0; v < V; v++) {
s += v + ": ";
for (int w : this.adj(v)) {
s += w + " ";
}
s += "\n";
}
return s;
}
public static void main(String[] args) {
Digraph d = new Digraph(5);
d.addEdge(0, 1);
d.addEdge(1, 0);
d.addEdge(2, 3);
d.addEdge(0, 4);
StdOut.println(d);
/**
輸出:
5 vertices, 3 edges
0: 4 1
1: 0
2:
3:
4:
*/
}
}
以上背包和有向圖代碼相關解釋請具體參照代碼中注釋。
可達性
上面提到了有向圖中的可達性和圖中的連通性的關系,可達性是連通性的特殊形式,對方向敏感,所以提到有向圖,不可不研究可達性。
可達性解答了“從一個頂點v到達另一個頂點w,是否存在一條有向路徑”等類似問題。
深度優先搜索
解答可達性問題,要借助深度優先搜索算法。為了更好的理解深度優先算法,先來搞清楚如何完全探索一個迷宮。
Tremaux搜索
完全探索一個迷宮的規則是:從起點出發,不走重復路線,走到終點走出迷宮。具體流程:
- 每當第一次到達一個新的頂點或邊時,標記上。
- 在走的過程中,遇到一個已標記的頂點或邊時,退回到上一個頂點。
- 當回退到的頂點已沒有可走的邊時繼續回退。
我想Tremaux搜索會給我們帶來一些啟發,回到圖的深度優先搜索算法。
package algorithms.graph;
import algorithms.bag.Bag;
import ioutil.StdOut;
/**
* 基於深度優先搜索(Depth First Search)解答有向圖頂點可達性問題。
*/
public class DigraphDFS {
private boolean[] marked;// 是否標記過
/**
* 算法:在圖中找到從某個頂點出發的所有頂點
*
* @param digraph
* @param start
*/
public DigraphDFS(Digraph digraph, int start) {
marked = new boolean[digraph.V()];// 初始化marked數組
dfs(digraph, start);
}
/**
* 算法:在圖中找到從某些頂點出發的所有頂點,這些頂點被作為一個集合傳入。
*
* @param digraph
* @param startSet
*/
public DigraphDFS(Digraph digraph, Iterable<Integer> startSet) {
marked = new boolean[digraph.V()];
for (int w : startSet) {
dfs(digraph, w);
}
}
/**
* 查詢某個頂點是否被標記(是否可達,因為標記過就是可達的)
*
* @param v
* @return
*/
public boolean marked(int v) {
return marked[v];
}
/**
* 深度優先搜索核心算法,通過標記,在圖中從v頂點出發找到有效路徑
* <p>
* 返回的是通過標記形成的一條有效路徑。
*
* @param digraph
* @param v
*/
private void dfs(Digraph digraph, int v) {
marked[v] = true;// 標記起點可達。
for (int w : digraph.adj(v)) {// 遍歷v頂點可達的一級頂點。
if (!marked[w]) dfs(digraph, w);// 如果發現w頂點未到達過,則繼續從w開始dfs(即向前走了一步)
}
}
public static void main(String[] args) {
Digraph d = new Digraph(5);// 初始化五個頂點的圖
d.addEdge(0, 1);
d.addEdge(1, 0);
d.addEdge(2, 3);
d.addEdge(0, 4);
Bag<Integer> startSet = new Bag<>();
startSet.add(2);
DigraphDFS reachable = new DigraphDFS(d, startSet);
for (int v = 0; v < d.V(); v++) {
if (reachable.marked(v)) {
StdOut.print(v + " ");
}
StdOut.println();
}
/**
* 輸出:
*
2
3
*/
}
}
startSet是入參條件,只有一個值為2,即在圖中找尋2的有效路徑,通過圖中的邊我們可以看出,2的有效路徑只有3,所以輸出是正確的。
可達性的一種應用:垃圾收集
我們都知道一般的對象垃圾收集都是計算它的引用數。在圖結構中,把對象作為頂點,引用作為邊,當一個對象在一段時間內未被他人引用的時候,這個頂點就是孤立的,對於其他有效路徑上的頂點來說它就是不可達的,因此就不會被標記,這時候,例如JVM就會清除掉這些對象釋放內存,所以JVM也是一直在跑類似以上這種DFS的程序,不斷找到那些未被標記的頂點,按照一定時間規則進行清除。
有向無環圖
不包含有向環的有向圖就是有向無環圖,DAG,Directed Acyclic Graph。
上面我們循序漸進的介紹了圖,有向圖,本節開始介紹有向無環圖,概念也已經給出,可以看出有向無環圖是有向圖的一種特殊結構。那么第一個問題就是
如何監測有向圖中沒有有向環,也就是如何確定一個DAG。
尋找有向環
基於上面的問題,我們要做一個尋找有向環的程序,這個程序還是依賴DFS深度優先搜索算法,如果找不到,則說明這個有向圖是DAG。
棧
先來補個坑,其實前面包括背包我在之前都寫過,但因為前面那篇文章是我第一篇博文,我還太稚嫩,沒有掌握好的編輯器,也沒有粘貼代碼,所以這里有必要重新填坑。
package algorithms.stack;
import ioutil.StdOut;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Stack<Item> implements Iterable<Item> {
private int SIZE;
private Node first;// 棧頂
public Stack() {// 初始化成員變量
SIZE = 0;
first = null;
}
private class Node {
private Item item;
private Node next;
}
// 棧:往first位置插入新元素
public void push(Item item) {
Node temp = first;
first = new Node();
first.item = item;
first.next = temp;
SIZE++;
}
// 棧:從first位置取出新元素,滿足LIFO,后進先出。
public Item pop() {
if (isEmpty()) throw new RuntimeException("Stack underflow");
Item item = first.item;
first = first.next;
SIZE--;
return item;
}
public boolean isEmpty() {
return first == null;
}
public int size() {
return this.SIZE;
}
@Override
public Iterator<Item> iterator() {
return new Iterator<Item>() {
Node node = first;
@Override
public boolean hasNext() {
return first != null;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
Item item = node.item;
node = node.next;
return item;
}
};
}
public static void main(String[] args){
Stack<String> stack = new Stack<>();
stack.push("heyheyhey");
stack.push("howau");
stack.push("231");
StdOut.println(stack.SIZE);
StdOut.println(stack.pop());
}
}
我們要做尋找有向環的程序的話,要依賴棧的結構,所以上面把這個坑給填了,下面回歸到尋找有向環的程序。(當然,你也可以直接使用java.util.Stack類)
package algorithms.graph;
import ioutil.StdOut;
import java.util.Stack;
public class DirectedCycle {
private boolean[] marked;// 以頂點為索引,值代表了該頂點是否標記過(是否可達)
private Stack<Integer> cycle; // 用來存儲有向環頂點。
// *****重點理解這里start****
private int[] edgeTo;// edgeTo[0]=1代表頂點1->0, to 0的頂點為1。
// *****重點理解這里end****
private boolean[] onStack;// 頂點為索引,值為該頂點是否參與dfs遞歸,參與為true
public DirectedCycle(Digraph digraph) {
// 初始化成員變量
marked = new boolean[digraph.V()];
onStack = new boolean[digraph.V()];
edgeTo = new int[digraph.V()];
cycle = null;
// 檢查是否有環
for (int v = 0; v < digraph.V(); v++) {
dfs(digraph, v);
}
}
private void dfs(Digraph digraph, int v) {
onStack[v] = true;// 遞歸開始,頂點上棧
marked[v] = true;
for (int w : digraph.adj(v)) {// 遍歷一條邊,v-> w
// 終止條件:找到有向環
if (hasCycle()) return;
// 使用onStack標志位來記錄有效路徑上的點,如果w在棧上,說明w在前面當了出發點,
if (!marked[w]) {
edgeTo[w] = v;// to w的頂點為v
dfs(digraph, w);
} else if (onStack[w]) {// 如果指到了已標記的頂點,且該頂點遞歸棧上。(棧上都是出發點,而找到了已標記的頂點是終點,說明出發點和終點相同了。)
cycle = new Stack<Integer>();
for (int x = v; x != w; x = edgeTo[x]) {//起點在第一次循環中已經push了,不要重復
cycle.push(x);// 將由v出發,w結束的環上中間的結點遍歷push到cycle中。
}
cycle.push(w);// push終點
}
}
onStack[v] = false;// 當遞歸開始結算退出時,頂點下棧。
}
public boolean hasCycle() {
return cycle != null;
}
public Iterable<Integer> cycle() {
return cycle;
}
public static void main(String[] args) {
Digraph d = new Digraph(6);
d.addEdge(0, 1);
d.addEdge(1, 2);
d.addEdge(2, 3);
d.addEdge(3, 0);
DirectedCycle directedCycle = new DirectedCycle(d);
if (directedCycle.hasCycle()) {
for (int a : directedCycle.cycle()) {
StdOut.println(a);
}
} else {
StdOut.println("DAG");
}
}
}
這段代碼不長但其中算法比較復雜,我盡力在注釋中做了詳細解釋,如有任何不明之處,歡迎隨時留言給我。
以上程序的測試用圖為
6 vertices, 4 edges
0: 1
1: 2
2: 3
3: 0
4:
5:
肉眼可以看出,這是一個0-1-2-3-0的一個有向環,所以以上程序的執行結果為:
3
2
1
0
先入棧的在后面,可以看出是0-1-2-3的有向環結構。如果我們將圖的內容改為:
6 vertices, 4 edges
0: 1
1: 2
2: 3
3:
4:
5: 0
則明顯最后一個拼圖3-0被我們打破了,變成了無所謂的5-0,這時該有向圖就不存在有向環。此時以上程序執行結果為:
DAG
DAG與BlockChain
上面一章節我們將DAG深挖了挖,我想到這里您已經和我一樣對DAG的算法層面非常了解,那么它和如今沸沸揚揚的區塊鏈有什么關聯呢?本章節主要介紹這部分內容。
在前面的文章中,我們已經了解了區塊鏈技術,無論是比特幣還是以太坊,都是基於一條鏈式結構,實現了去中心化的,點對點的,trustless的一種新型技術。然而這條鏈式結構在面臨業務拓展的時候屢屢遭受新的挑戰,例如塊存儲量問題,交易速度問題,數據總量過大,單節點存儲壓力等等。而DAG是基於圖的一種實現方式,之所以不允許有向環的出現,是因為DAG可以保證結點交易的順序,可以通過上面介紹過的有效路徑來找到那根主鏈。如果出現了有向環,那系統就亂了。如果沒有有向環的話,DAG中可以有多條有效路徑連接各個頂點,因此DAG可以說是更加完善,強大的新一代區塊鏈結構。

目前非常有名的采用DAG技術的區塊鏈產品有DagCoin,IOTA,ByteBall等,他們都是基於DAG,在性能和儲量上面有了全面的提升。
這里面仍然會有“分叉”的可能,處理方式也是相同的,看哪個結點能夠有新的后續,這個部分我們在講“叔塊”的時候說過。
區塊鏈采用DAG結構以后稱為了blockless,無塊化的結構,即我們不再將交易打包到塊中,以塊為單元進行存儲,而是直接將交易本身作為基本單元進行存儲。另外,DAG也有雙花的可能,也是上面“分叉問題”引起的,但它在確認有效路徑以后會自動恢復。同時,DAG是異步共識,具體機制還不了解,但它解決了交易性能問題。
總結
本文循序漸進地從圖到有向圖到有向無環圖,詳細地介紹了相關術語,api代碼實現,也補充入了背包和棧的代碼實現,重點研究了圖的深度優先搜索算法以及尋找有向環算法。最后對DAG和區塊鏈的關系進行了簡介,希望隨着技術發展,DAG有望成為真正的區塊鏈3.0。
參考資料
Algorithms 4th,網上資料
