【資料結構和演算法17】拓撲排序
這一節我們學習一個新的排序演算法,準確的來說,應該叫“有向圖的拓撲排序”。所謂有向圖,就是A->B,但是B不能到A。與無向圖的區別是,它的邊在鄰接矩陣裡只有一項(友情提示:如果對圖這種資料結構部不太瞭解的話,可以先看一下這篇博文:資料結構和演算法之 無向圖。因為拓撲排序是基於圖這種資料結構的)。
有向圖的鄰接矩陣如下表所示:
A |
B |
C |
|
A |
0 |
1 |
1 |
B |
0 |
0 |
1 |
C |
0 |
0 |
0 |
所以針對前面討論的無向圖,鄰接矩陣的上下三角是對稱的,有一半資訊是冗餘的。而有向圖的鄰接矩陣中所有行列之都包含必要的資訊,它的上下三角不是對稱的。所以對於有向圖,增加邊的方法只需要一條語句:
//有向圖中,鄰接矩陣中只有一項
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
}
如果使用鄰接表示意圖,那麼A->B表示A在它的連結串列中有B,但是B的連結串列中不包含A,這裡就不多說了,本文主要通過鄰接矩陣實現。
因為圖是有向的,假設A->B->C->D這種,那這就隱藏了一種順序,即要想到D,必須先過C,必須先過B,必須先過A。它們無形中形成了一種順序,這種順序在實際中還是用的挺廣泛的,比如,要做web開發,必須先學java基礎等等,這些都遵循一個順序,所以拓撲排序的思想也是這樣,利用有向圖特定的順序進行排序。但是拓撲排序的結果不是唯一的,比如A->B的同時,C->B,也就是說A和C都能到B,所以用演算法生成一個拓撲排序時,使用的方法和程式碼的細節決定了會產生那種拓撲排序。
拓撲排序的思想雖然不尋常,但是卻很簡單,有兩個必要的步驟:
1. 找到一個沒有後繼的頂點;
2.從圖中刪除這個頂點,在列表中插入頂點的標記
然後重複1和2,直到所有頂點都從圖中刪除,這時候列表顯示的頂點順序就是拓撲排序的結果了。
但是我們需要考慮一種特殊的有向圖:環。即A->B->C->D->A。這種必然會導致找不著“沒有後繼的節點”,這樣便無法使用拓撲排序了。
下面我們分析下拓撲排序的程式碼:
public void poto() {
int orig_nVerts = nVerts; //記錄有多少個頂點
while(nVerts > 0) {
//返回沒有後繼頂點的頂點
int currentVertex = noSuccessors(); //如果不存在這樣的頂點,返回-1
if(currentVertex == -1) {
System.out.println("ERROR: Graph has cycles!");
return;
}
//sortedArray中儲存排過序的頂點(從尾開始存)
sortedArray[nVerts-1] = vertexArray[currentVertex].label;
deleteVertex(currentVertex);//刪除該頂點,便於下一次迴圈,尋找下一個沒有後繼頂點的頂點
}
System.out.println("Topologically sorted order:");
for(int i = 0; i < orig_nVerts; i++) {
System.out.print(sortedArray[i]);
}
System.out.println("");
}
主要的工作在while迴圈中進行,這個迴圈直到定點數為0時才退出:
1. 呼叫noSuccessors()找到任意一個沒有後繼的頂點;
2. 如果找到一個這樣的頂點,把頂點放到sortedArray陣列中,並且從圖中刪除這個頂點;
3. 如果不存在這樣的頂點,則圖必然存在環。
最後sortedArray陣列中儲存的就是排過序的頂點了。下面我們分析下noSuccessor()方法和deleteVertes()方法:
//return vertex with no successors
private int noSuccessors() {
boolean isEdge;
for(int row = 0; row < nVerts; row++) {
isEdge = false;
for(int col = 0; col < nVerts; col++) {
if(adjMat[row][col] > 0) { //只要adjMat陣列中儲存了1,表示row->col
isEdge = true;
break;
}
}
if(!isEdge) {//只要有邊,返回最後一個頂點
return row;
}
}
return -1;
}
private void deleteVertex(int delVertex) {
if(delVertex != nVerts -1) {
for(int i = delVertex; i < nVerts-1; i++) { //delete from vertexArray
vertexArray[i] = vertexArray[i+1];
}
//刪除adjMat中相應的邊
for(int row = delVertex; row < nVerts-1; row++) {//delete row from adjMat
moveRowUp(row, nVerts);
}
for(int col = delVertex; col < nVerts-1; col++) {//delete column from adjMat
moveColLeft(col, nVerts-1);
}
}
nVerts--;
}
從上面程式碼可以看出,刪除一個頂點很簡單,從vertexArray中刪除,後面的頂點向前移動填補空位。同樣的,頂點的行列從鄰接矩陣中刪除,下面的行和右面的列移動來填補空位。刪除adjMat陣列中的邊比較簡單,下面看看moveRowUp和moveColLeft的方法:
private void moveRowUp(int row, int length) {
for(int col = 0; col < length; col++) {
adjMat[row][col] = adjMat[row+1][col];
}
}
private void moveColLeft(int col, int length) {
for(int row = 0; row < length; row++) {
adjMat[row][col] = adjMat[row][col+1];
}
}
這樣便介紹完了拓撲排序的所有過程了。下面附上完整的程式碼:
package graph;
/**
* 有向圖的拓撲排序:
* 拓撲排序是可以用圖模擬的另一種操作,它可以用於表示一種情況,即某些專案或事件必須按特定的順序排列或發生。
* 有向圖和無向圖的區別是:有向圖的邊在鄰接矩陣中只有一項。
* 拓撲排序演算法的思想雖然不尋常但是很簡單,有兩個步驟是必須的:
* 1. 找到一個沒有後繼的頂點
* 2. 從圖中刪除這個頂點,在列表的前面插入頂點的標記
* 重複這兩個步驟,直到所有頂點都從圖中刪除,這時,列表顯示的頂點順序就是拓撲排序的結果。
* 刪除頂點似乎是一個極端的步驟,但是它是演算法的核心,如果第一個頂點不處理,演算法就不能計算出要處理的第二個頂點。
* 如果需要,可以再其他地方儲存圖的資料(頂點列表或者鄰接矩陣),然後在排序完成後恢復它們。
* @author eson_15
* @date 2016-4-20 12:16:11
*
*/
public class TopoSorted {
private final int MAX_VERTS = 20;
private Vertex vertexArray[]; //儲存頂點的陣列
private int adjMat[][]; //儲存是否有邊界的矩陣陣列, 0表示沒有邊界,1表示有邊界
private int nVerts; //頂點個數
private char sortedArray[]; //儲存排過序的資料的陣列
public TopoSorted() {
vertexArray = new Vertex[MAX_VERTS];
adjMat = new int[MAX_VERTS][MAX_VERTS];
nVerts = 0;
for(int i = 0; i < MAX_VERTS; i++) {
for(int j = 0; j < MAX_VERTS; j++) {
adjMat[i][j] = 0;
}
}
sortedArray = new char[MAX_VERTS];
}
public void addVertex(char lab) {
vertexArray[nVerts++] = new Vertex(lab);
}
//有向圖中,鄰接矩陣中只有一項
public void addEdge(int start, int end) {
adjMat[start][end] = 1;
}
public void displayVertex(int v) {
System.out.print(vertexArray[v].label);
}
/*
* 拓撲排序
*/
public void poto() {
int orig_nVerts = nVerts; //remember how many verts
while(nVerts > 0) {
//get a vertex with no successors or -1
int currentVertex = noSuccessors();
if(currentVertex == -1) {
System.out.println("ERROR: Graph has cycles!");
return;
}
//insert vertex label in sortedArray (start at end)
sortedArray[nVerts-1] = vertexArray[currentVertex].label;
deleteVertex(currentVertex);
}
System.out.println("Topologically sorted order:");
for(int i = 0; i < orig_nVerts; i++) {
System.out.print(sortedArray[i]);
}
System.out.println("");
}
//return vertex with no successors
private int noSuccessors() {
boolean isEdge;
for(int row = 0; row < nVerts; row++) {
isEdge = false;
for(int col = 0; col < nVerts; col++) {
if(adjMat[row][col] > 0) {
isEdge = true;
break;
}
}
if(!isEdge) {
return row;
}
}
return -1;
}
private void deleteVertex(int delVertex) {
if(delVertex != nVerts -1) {
for(int i = delVertex; i < nVerts-1; i++) { //delete from vertexArray
vertexArray[i] = vertexArray[i+1];
}
for(int row = delVertex; row < nVerts-1; row++) {//delete row from adjMat
moveRowUp(row, nVerts);
}
for(int col = delVertex; col < nVerts-1; col++) {//delete column from adjMat
moveColLeft(col, nVerts-1);
}
}
nVerts--;
}
private void moveRowUp(int row, int length) {
for(int col = 0; col < length; col++) {
adjMat[row][col] = adjMat[row+1][col];
}
}
private void moveColLeft(int col, int length) {
for(int row = 0; row < length; row++) {
adjMat[row][col] = adjMat[row][col+1];
}
}
}
拓撲排序就介紹到這吧,如有錯誤之處,歡迎留言指正~
文末福利:“程式設計師私房菜”,一個有溫度的公眾號~
_____________________________________________________________________________________________________________________________________________________
-----樂於分享,共同進步!