1. 程式人生 > >圖的深度優先搜尋及拓撲排序

圖的深度優先搜尋及拓撲排序

本文將介紹圖的深度優先搜尋,並實現基於深度優先搜尋的拓撲排序(拓撲排序適用於有向無環圖,下面詳細介紹)。

1. 圖的深度優先遍歷要解決的問題

圖的深度優先搜尋與樹的深度優先搜尋類似,但是對圖進行深度優先搜尋要解決一個問題,那就是頂點的重複訪問,假設圖中存在一個環路A-B-C-A,那麼對頂點A進行展開後得到B,對B進行展開後得到C,然後對C進行展開後得到A,然後A就被重複訪問了。。。

這顯然是不對的!我們需要用一個狀態變數來記錄一個頂點被訪問和被展開的狀態。在《演算法導論》中,作者使用3種顏色來對此進行標記:WHITE——頂點還沒被訪問過,GRAY——頂點已經被訪問過但是其子結點還沒被訪問完,BLACK——頂點已經被訪問過而且其子結點已經被訪問完了。

2. 用棧實現圖的深度優先遍歷

在《演算法導論》中,作者使用了遞迴來實現深度優先遍歷。雖然遞迴寫起來更加簡潔而且容易理解,但是我並不建議在實際工程中這樣做,除非你的問題規模比較小。否則遞迴會讓你的程式萬劫不復。正如使用佇列實現廣度優先搜尋一樣(可以看到我的上一篇部落格圖的廣度優先遍歷),下面我將會使用棧來實現深度優先搜尋。

下面用一個例子來說明如何使用棧來實現深度優先遍歷。假設有下面一個圖。

首先,我們初始化一個空棧,然後將所有頂點入棧,此時所有頂點都沒有被訪問過,因此所有頂點都置為白色,如下所示。


上述初始化完成以後,頂點5位於棧底,頂點1位於棧頂,然後我們就進入迴圈操作,首先出棧一個頂點,該頂點為頂點1,因為頂點1是白色的,即沒有被訪問過,於是就將頂點1置為灰色,並重新入棧(因為下面要記錄結點的子結點被訪問完的次序),然後展開頂點1的所有頂點,只有頂點2,由於頂點2是白色即未被訪問過,於是將頂點2入棧,如下圖所示。


然後我們繼續迴圈,出棧一個頂點,即頂點2,因為頂點2是白色的,於是將其置為灰色,並重新入棧,然後展開得到頂點4和頂點5,由於頂點4和頂點5均為白色,於是將它們都入棧。如下圖所示。


然後還是繼續迴圈,出棧一個頂點,即頂點5,由於頂點5是白色的,於是將其置為灰色,並重新入棧,然後展開頂點5,發現頂點5沒有子結點,如下圖所示。


然後繼續迴圈,出棧頂點5,此時頂點5為灰色,說明頂點5的子結點已經被訪問完了,於是將頂點5置為黑色,並將其次序標記為1,說明他是第一個被展開完的結點,這一個記錄將會被應用到拓撲排序中。此時棧的狀態如下所示。


繼續迴圈,出棧頂點4,因為是白色的,所以將頂點4置為灰色並重新入棧,然後展開頂點4得到頂點5,因為頂點5是黑色的,於是不將其入棧。接下來繼續出棧頂點4,發現頂點4是灰色的,於是和前面一樣,將頂點4置為黑色並將其次序記錄為2。此時的棧如下圖所示。

好了,後面的迴圈的工作也跟上述類似,依次將頂點2、1出棧並置為黑色,直到頂點3展開發現沒有白色的子結點,也將其置為黑色。然後再到了頂點4和頂點5,因為是黑色,直接出棧不作任何處理了。此時棧為空,遍歷結束。

我們的程式需要的輸入的圖以鄰接表的形式表示,下面給出圖及頂點和鄰接表的定義。

typedef enum VertexColor
{
	Vertex_WHITE = 0,	// 未被搜尋到
	Vertex_BLACK = 1,	// 子結點都被搜尋完畢
	Vertex_GRAY = 2	// 子結點正在被搜尋
} VertexColor;

typedef struct GNode
{
	int number;	// 頂點編號
	struct GNode *next;
} GNode;

typedef struct Vertex
{
	int number;
	int f;			
	VertexColor color;	// 搜尋過程標記搜尋狀態
	struct Vertex *p;
} Vertex;

typedef struct Graph
{
	GNode *LinkTable;
	Vertex *vertex;
	int VertexNum;
} Graph;
VertexColor是頂點的顏色列舉量定義。GNode是鄰接表的元素定義,其記錄了頂點的編號。鄰接表本質上就是一個指標陣列,其每個元素都是指向一個連結串列的第一個元素的指標。Vertex是頂點的資料結構,其屬性number為頂點編號,f是記錄其被訪問完的次序,color是狀態標識顏色,p指向搜尋完成後得到的深度優先遍歷樹中的頂點的前驅。Graph是圖的資料結構定義,其包含了一個頂點陣列,按編號升序排列,LinkTable是鄰接表。

下面給出用棧實現的深度優先搜尋的程式。

/**
* 深度優先搜尋,要求輸入圖g的結點編號從1開始
*/
void searchByDepthFirst(Graph *g)
{
	int VertexNum = g->VertexNum;
	Stack *stack = initStack();
	Vertex *vs = g->vertex;
	GNode *linkTable = g->LinkTable;
	int order = 0;

	for (int i = 0; i < VertexNum; i++)
	{
		Vertex *v = vs + i;
		v->color = Vertex_WHITE;
		v->p = NULL;
		push(&stack, v->number);
	}

	while (!isEmpty(stack))
	{
		int number = pop(&stack);
		Vertex *u = vs + number - 1;
		if (u->color == Vertex_WHITE) 
		{
			// 開始搜尋該結點的子結點
			u->color = Vertex_GRAY;
			push(&stack, number);
		}
		else if (u->color == Vertex_GRAY)
		{
			// 該結點的子結點已經被搜尋完了
			u->color = Vertex_BLACK;
			u->f = order++;
			continue;
		}
		else
		{
			continue;
		}
		GNode *links = linkTable + number - 1;
		links = links->next;
		while (links != NULL)
		{
			// 展開子結點併入棧
			Vertex *v = vs + links->number - 1;
			if (v->color == Vertex_WHITE)
			{
				v->p = u;
				push(&stack, links->number);
			}
			links = links->next;
		}
	}
}
程式先將所有頂點初始化為白色併入棧,然後開始迴圈。在迴圈中,每次出棧一個結點都要先對其顏色進行判斷,如果是灰色,標記其被訪問完成的次序並標為黑色,如果是黑色,不作任何處理,如果是白色,則置為灰色並重新入棧,然後展開其所有子結點並將白色的子結點入棧,並且記下這些白色頂點的前驅。當棧為空時,迴圈結束。

棧的操作可以參考其它資料,這裡不做詳述,下面給出一個簡單實現。

typedef struct Stack {
	int value;
	struct Stack *pre;
} Stack;
上面是棧的結構定義。
Stack* initStack()
{
	Stack *s = (Stack *)malloc(sizeof(Stack));
	s->pre = NULL;
	return s;
}

void push(Stack **s, int value)
{
	Stack *n = (Stack *)malloc(sizeof(Stack));
	n->pre = *s;
	n->value = value;
	*s = n;
}

int pop(Stack **s)
{
	if ((*s)->pre == NULL)
	{
		return INT_MAX;
	}

	int value = (*s)->value;
	Stack *pre = (*s)->pre;
	free(*s);
	*s = pre;
	return value;
}

int isEmpty(Stack *s)
{
	if (s->pre == NULL)
	{
		return 1;
	}
	return 0;
}

void destroyStack(Stack **s)
{
	while (*s != NULL)
	{
		Stack *pre = (*s)->pre;
		free(*s);
		*s = pre;
	}
}
上面是棧的方法,依次是初始化一個空棧,入棧,出棧,判斷是否為空棧,銷燬棧。

下面給出一個應用上述深度優先遍歷方法的例子程式碼和執行結果。

Graph graph;
	graph.VertexNum = 5;
	Vertex v[5];
	Vertex v1; v1.number = 1; v1.p = NULL; v[0] = v1;
	Vertex v2; v2.number = 2; v2.p = NULL; v[1] = v2;
	Vertex v3; v3.number = 3; v3.p = NULL; v[2] = v3;
	Vertex v4; v4.number = 4; v4.p = NULL; v[3] = v4;
	Vertex v5; v5.number = 5; v5.p = NULL; v[4] = v5;
	graph.vertex = v;

	GNode nodes[5];
	GNode n1; n1.number = 1;
	GNode n2; n2.number = 2;
	GNode n3; n3.number = 3;
	GNode n4; n4.number = 4;
	GNode n5; n5.number = 5;

	GNode y; y.number = 5; 
	GNode e; e.number = 2; 
	GNode f; f.number = 5; 
        GNode g; g.number = 2;
	GNode h; h.number = 4; 

	n1.next = &e; e.next = NULL;
	n2.next = &h; h.next = &f; f.next = NULL;
	n3.next = &g; g.next = NULL;
	n4.next = &y; y.next = NULL;
	n5.next = NULL;
	nodes[0] = n1;
	nodes[1] = n2;
	nodes[2] = n3;
	nodes[3] = n4;
	nodes[4] = n5;
	graph.LinkTable = nodes;

	searchByDepthFirst(&graph);
	printf("\n");
	printPath(&graph, 2);
執行結果如下。

上述示例程式碼對本文前面給出的圖進行深度優先遍歷後,輸出頂點2的前驅子樹,得到上述結果,意思是頂點2的前驅頂點為3。

3. 圖的拓撲排序

圖的拓撲排序可以應用深度優先遍歷來實現。

首先,說明一下什麼是拓撲排序。拓撲排序是指將圖按前後順序組織排列起來,前後順序是指在排序結果中,前面的頂點可能是有一條簡單路徑通向後面的頂點的,而後面的頂點是肯定沒有一條簡單路徑通向前面的頂點的。拓撲排序適用於有向無環圖。下面給出一個拓撲排序的例子。

還是這個圖,這也是一個有向無環圖,拓撲排序的結果最前面的頂點肯定是一個根頂點,即沒有其它頂點指向他。這個圖的拓撲排序結果是1、3、2、4、5,或者1、3、2、5、4,等等,不存在指向關係的頂點間的順序是不確定的。我們可以將拓撲排序的圖看成是一個日程表,頂點1代表洗臉,頂點3代表刷牙,頂點2代表吃早餐,頂點4代表穿褲子,頂點5代表穿衣服,於是拓撲排序本質就是根據事件應該發生的先後順序組織起來。

圖的深度優先遍歷有一個特點,那就是,當一個頂點的子結點都被訪問完了,該頂點才會結束訪問,並開始向上回溯訪問它的父結點的其它子結點。這意味著,一個頂點的結束訪問時間與其子結點的結束訪問時間存在先後關係,而這個順序剛好與拓撲排序是相反的!簡單地說,在深度優先遍歷中,頂點A要結束訪問的前提是其子結點B、C、D...都被訪問完了,而在拓撲排序中,事件A完成之後其後面的事件B、C、D...才可以繼續進行。這也就是上面的深度優先搜尋為什麼要記錄結點被訪問完成的次序,因為這個次序倒過來就是拓撲排序的順序!

下面給出基於圖的深度優先搜尋實現的拓撲排序的程式。

/**
* 有向無環圖的拓撲排序
*/
void topologySort(Graph *g, int **order, int *n)
{
	searchByDepthFirst(g);
	*n = g->VertexNum;
	*order = (int *)malloc(sizeof(int) * *n);
	for (int i = 0; i < *n; i++)
	{
		(*order)[*n - 1 - g->vertex[i].f] = i + 1;
	}
}
同樣的,上述程式實現的拓撲排序要求的圖的頂點編號也是從1開始。

其實拓撲排序還有另一種實現方法,那就是從根頂點開始,刪除所有根頂點及與之相連的邊。此時就會產生新的根頂點,然後繼續重複上述操作,知道所有頂點都被刪除。

4. 總結

本文介紹瞭如何使用棧來實現圖的深度優先搜尋,並基於圖的深度優先搜尋實現了拓撲排序。其時間複雜度均為O(n)。完整程式可以參考我的github專案資料結構與演算法

這個專案裡面有本部落格介紹過的和沒有介紹的以及將要介紹的《演算法導論》中部分主要的資料結構和演算法的C實現,有興趣的可以fork或者star一下哦~ 由於本人還在研究《演算法導論》,所以這個專案還會持續更新哦~ 大家一起好好學習~