上次我們介紹了圖的鄰接矩陣存儲結構基本操作的實現,這次介紹圖的鄰接表存儲表示各基本操作的實現。
還是老規矩:
程序在碼雲上可以下載。
地址:https://git.oschina.net/601345138/DataStructureCLanguage.git
圖的ADT不再重複貼了,請參考:《數據結構編程筆記十八:第七章 圖 圖的鄰接矩陣存儲表示各基本操作的實現》
鄰接表就是一個結點數組,數組每個元素後面掛一個鏈表。這種“順序+鏈式”的風格很常見,比如第九章 查找中提到的散列表(鏈地址法P258頁)就有類似的設計。
本次程序用到的源文件清單:
my_constants.h 各種狀態碼
ALGraph.h 圖的鄰接表存儲結構表示定義
ALGraph.cpp 基於鄰接表的基本操作實現
DFS_BFS_ALGraph.cpp 基於鄰接表的深度優先遍歷和廣度優先遍歷算法實現
LinkedQueue.cpp 鏈隊列的實現
LinkedList.cpp 不帶頭結點單鏈表的實現
基本操作及遍歷測試.cpp 主函數,調用基本操作完成演示
本次程序的廣度優先遍歷用到了隊列,我使用了第三章的程序:鏈隊列。
鏈隊列在:《數據結構編程筆記九:第三章 棧和隊列 鏈隊列的實現》一文中有介紹。我對這篇文章中的程序進行了精簡,但主要函數沒有改動。 具體程序我放在總結後面。如有需要請到那裏查看。
爲了操作實現方便,我使用了單鏈表程序來簡化部分操作,但是這個單鏈表與《 數據結構編程筆記四:第二章 線性表 單鏈表的實現》一文介紹的單鏈表有所區別,本文用到的單鏈表不帶頭結點,很多操作與帶頭結點的單鏈表有區別。望讀者注意。具體程序我放在總結後面。如有需要請到那裏查看。
需要注意的是:爲了能夠複用單鏈表的部分操作,我把書上P163頁的存儲結構做了一些修改,具體看源代碼。
一起來看看程序的實現。
源文件:my_constants.h
//************************自定義符號常量***************************
#define OVERFLOW -2 //內存溢出錯誤常量
#define OK 1 //表示操作正確的常量
#define ERROR 0 //表示操作錯誤的常量
#define TRUE 1 //表示邏輯真的常量
#define FALSE 0 //表示邏輯假的常量
//***********************自定義數據類型****************************
typedef int Status; //指定狀態碼的類型是int
源文件:ALGraph.h
//---------------------圖的鄰接表存儲表示----------------------
//指定頂點類型是int
typedef int VertexType;
//指定弧的權值類型爲int
typedef int InfoType;
//最大頂點數
#define MAX_VERTEX_NUM 50
//圖的種類標誌
typedef enum {
// {有向圖 = 0,有向網 = 1,無向圖 = 2,無向網 = 3}
DG, DN, UDG, UDN
} GraphKind;
//存儲邊信息的結點
typedef struct ElemType{
//該弧所指向的頂點的位置,也就是從當前頂點出髮指向哪個頂點
//這個變量存儲的是指向頂點在AdjList數組中的下標
int adjvex;
//該弧相關信息的指針
//弧的相關信息就是存放權值的,當圖的類型是網的時候這個變量纔有意義
InfoType *info;
} ElemType;
typedef struct ArcNode{
//除了弧指針外的其他部分均屬於data
ElemType data;
//指向下一條弧的指針
struct ArcNode *nextarc;
} ArcNode;
//存儲頂點信息的結點
typedef struct VNode {
//頂點名稱
VertexType data;
//指向第一條依附該頂點的弧(存儲第一條邊結點地址的指針域)
ArcNode *firstarc;
} VNode, AdjList[MAX_VERTEX_NUM];
//圖的鄰接表存儲表示
typedef struct ALGraph {
//頂點向量,在數組中存儲了圖中所有的頂點
AdjList vertices;
//圖的當前頂點數和弧數(邊數)
int vexnum, arcnum;
//圖的種類標誌
GraphKind kind;
} ALGraph;
//爲了複用單鏈表中的插入、刪除等基本操作,採用宏定義方式配合引入的單鏈表實現
//完成與圖的鄰接表存儲結構的操作對接
//定義單鏈表的結點類型是圖的表結點的類型
#define LNode ArcNode
//定義單鏈表結點的指針域是表結點指向下一條弧的指針域
#define next nextarc
//定義指向單鏈表結點的指針是指向圖的表結點的指針
typedef ArcNode *LinkList;
源文件:ALGraph.cpp
//-----------------------------圖的基本操作------------------------------
/*
函數:LocateVex
參數:ALGraph G 圖G(鄰接表存儲結構)
VertexType u 頂點u
返回值:若G中存在頂點u,則返回該頂點在圖中位置;否則返回-1
初始條件:圖G存在,u和G中頂點有相同特徵
作用:在圖G中查找頂點u的位置
*/
int LocateVex(ALGraph G, VertexType u){
//i是臨時變量,循環控制用
int i;
//與圖中每個結點的名稱作對比
for(i = 0; i < G.vexnum; ++i) {
//在圖中找到了結點
if(G.vertices[i].data == u) {
//返回頂點在圖中的位置
return i;
}//if
}//for
//沒有在圖中找到頂點,返回-1
return -1;
}//LocateVex
/*
函數:CreateGraph
參數:ALGraph &G 圖的引用
返回值:狀態碼,操作成功返回OK。
作用:根據用戶的輸入構造圖(四種)。
*/
Status CreateGraph(ALGraph &G) {
//w是權值
//i、j、k全是臨時變量,循環用
int i, j, k, w;
//連接邊或弧的2頂點
VertexType va, vb;
//弧結點
ElemType e;
//確定圖的類型
printf("請輸入圖的類型(有向圖輸入0, 有向網輸入1,無向圖輸入2,無向網輸入3): ");
scanf("%d", &G.kind);
//確定圖的頂點數和邊數
printf("請輸入圖的頂點數,邊數: ");
scanf("%d,%d", &G.vexnum, &G.arcnum);
//從鍵盤輸入頂點值,構造頂點向量
printf("請輸入%d個頂點的值(用空格隔開):\n", G.vexnum);
for(i = 0; i < G.vexnum; ++i) {
//輸入頂點的值
scanf("%d", &G.vertices[i].data);
getchar(); //吃掉多餘字符
//初始化與該頂點有關的出弧鏈表
G.vertices[i].firstarc = NULL;
}//for
//如果是構造網,則需要繼續輸入權值,如果是構造圖則不需要
//枚舉變量中{有向圖 = 0,有向網 = 1,無向圖 = 2,無向網 = 3}
//G.kind % 2 != 0 表示構造的是網,否則是圖
if(G.kind % 2) { //if(G.kind % 2) <=> if(G.kind % 2 != 0)
printf("請輸入每條弧(邊)的弧尾、弧頭和權值(以逗號作爲間隔):\n");
}//if
else { //構造圖
printf("請輸入每條弧(邊)的弧尾和弧頭(以逗號作爲間隔):\n");
}//else
//構造相關弧鏈表
for(k = 0; k < G.arcnum; ++k){
//如果構造的是網,則需要接收權值
//if(G.kind % 2) <=> if(G.kind % 2 != 0)
if(G.kind % 2) {
scanf("%d,%d,%d", &va, &vb, &w);
}//if
else { //構造的是圖,不需要接收權值
scanf("%d,%d", &va, &vb);
}//else
//先處理弧頭和弧尾部分,這是圖和網都需要執行的公共操作。
//弧尾
i = LocateVex(G, va);
//弧頭
j = LocateVex(G, vb);
//給待插表結點e賦值,圖沒有權值,網有權值。由於所有結點的初始化
//都用到e,所以這塊空間每輪新的循環都需要重置爲NULL(清零)。
//如果構造的是網,還會連接一小塊保存權值的空間,否則保持NULL。
e.info = NULL;
//弧頭
e.adjvex = j;
//如果構造的是網,則需要將權值存放到弧的相關信息info中
//注意:此時info的空間還沒有申請到
//if(G.kind % 2) <=> if(G.kind % 2 != 0)
if(G.kind % 2) {
//申請存放權值的空間
e.info = (InfoType *)malloc(sizeof(int));
//將弧的相關信息填入存放權值的空間中
*(e.info) = w;
}//if
//將弧結點插在第i個頂點的表頭
//本項操作調用的是單鏈表的插入算法,把G.vertices[i].firstarc當成了頭結點
//採用頭插法插入
ListInsert(G.vertices[i].firstarc, 1, e);
//如果構造的是無向圖或無向網,產生第2個表結點,並插在第j個元素(入弧)的表頭
//第二個表結點與第一個結點沿主對角線(左上至右下)對稱
if(G.kind >= 2) {
//主對角線對稱位置的結點權值不變,所以e.info不變,不必再賦值
//也就是說:鄰接矩陣中沿主對角線對稱的兩個結點在鄰接表中共用一塊權值空間
e.adjvex = i;
//插在對角線對稱位置頂點的表頭,也就是第j個位置。
ListInsert(G.vertices[j].firstarc, 1, e);
}//if
}//for
//操作成功
return OK;
}//CreateGraph
/*
函數:DestroyGraph
參數:ALGraph &G 圖的引用
返回值:狀態碼,操作成功返回OK。
作用:若圖G存在,銷燬圖G
*/
Status DestroyGraph(ALGraph &G) {
//i是臨時變量,循環控制用
int i;
//帶回從鄰接表中刪除的弧結點,如果是網,還需要繼續釋放存儲權值的存儲單元的空間
ElemType e;
//對於所有頂點
for(i = 0; i < G.vexnum; ++i) {
//如果被銷燬的是網,還需要繼續銷燬權值
//if(G.kind % 2) <=> if(G.kind % 2 != 0)
if(G.kind % 2) {
//頂點對應的弧或邊鏈表不空
while(G.vertices[i].firstarc) {
//刪除鏈表的第1個弧結點,並將值賦給e
ListDelete(G.vertices[i].firstarc, 1, e);
//釋放保存權值的內存空間
//需要注意:在構造網時爲了節約空間,讓沿主對角線對稱的兩個結點
//的權值信息指針e.info指向了同一塊內存區域。所以釋放內存空間的時候
//要注意已經釋放過的內存空間不可以再次釋放。所以在釋放內存空間之前
//需要進行判斷。但是C語言沒有提供判斷內存是否被釋放的函數。
//這就需要我們自己寫判斷條件。觀察鄰接矩陣不難發現:
//主對角線上方的結點和沿主對角線對稱的下方的結點之間存在一個規律:
//上方結點的列下標 > 對稱位置(下方)結點的列下標
//由於釋放弧結點時是按照頂點順序從小到大進行的,
//列下標大的弧結點(上三角)總比列下標小的弧結點(下三角)先釋放。
//所以判斷結點是否被釋放的標誌就是:頂點序號 > i
if(e.adjvex > i) {
free(e.info);
}//if
}//while
}//if
else {//若被銷燬的是圖,只銷毀弧鏈表就行了
//調用單鏈表銷燬函數銷燬弧鏈表
DestoryList(G.vertices[i].firstarc);
}//else
}//for
//重置頂點數爲0
G.vexnum = 0;
//重置邊或弧數爲0
G.arcnum = 0;
//操作成功
return OK;
}//DestroyGraph
/*
函數:GetVex
參數:ALGraph G 圖G
int v 某個頂點的序號v
返回值:頂點v的值
作用:若圖G存在,v是G中某個頂點的序號,返回頂點v的值
*/
VertexType& GetVex(ALGraph G, int v) {
//檢查頂點序號v是否越界
if(v >= G.vexnum || v < 0) {
//若越界訪問則直接退出程序
exit(ERROR);
}//if
//返回頂點v的值
return G.vertices[v].data;
}//GetVex
/*
函數:PutVex
參數:ALGraph &G 圖G的引用
VertexType v 待修改頂點v
VertexType value 將頂點v的值修改爲value
返回值:狀態碼,操作成功返回OK,失敗返回ERROR
作用:若圖G存在,v是G中某個頂點,對v賦新值value
*/
Status PutVex(ALGraph &G, VertexType v, VertexType value) {
//i記錄了在圖的鄰接表中定位頂點操作得到的v在頂點數組中的下標
int i;
//通過定位操作得到下標
i = LocateVex(G, v);
//得到下標之後需判斷下標是否合法:即圖中是否存在頂點v
//若圖中不存在頂點v,定位函數LocateVex返回-1
if(i > -1) { //圖中存在頂點v
//修改頂點v的值爲value
G.vertices[i].data = value;
//操作成功
return OK;
}//if
//操作失敗
return ERROR;
}//PutVex
/*
函數:FirstAdjVex
參數:ALGraph G 圖G
VertexType v 頂點v
返回值:若v在圖G中有鄰接頂點,返回v的第一個鄰接頂點的序號,否則返回-1。
作用:若圖G存在,v是G中某個頂點,返回v的第一個鄰接頂點的序號。
若頂點在G中沒有鄰接頂點,則返回-1
*/
int FirstAdjVex(ALGraph G, VertexType v) {
//工作指針p,指向頂點v弧鏈表中的頂點
ArcNode *p;
//i爲頂點v在圖G中的序號
int i;
//通過定位操作找出頂點v的序號
i = LocateVex(G, v);
//檢查圖中是否存在頂點v
if(i == -1) {
return -1;
}//if
//工作指針p指向序號i指示的頂點v
p = G.vertices[i].firstarc;
//如果v有鄰接頂點
if(p) { //if(p) <=> if(p != NULL)
//返回頂點v第一個鄰接頂點的序號
return p->data.adjvex;
}//if
else { //頂點v沒有鄰接頂點
//返回-1
return -1;
}//else
}//FirstAdjVex
/*
函數:equalvex
參數:ElemType a 頂點A
ElemType b 頂點B
返回值:狀態碼,操作成功返回OK,失敗返回ERROR
作用:判斷兩個弧結點是否有相同的鄰接點。
DeleteArc()、DeleteVex()和NextAdjVex()要調用的函數
*/
Status equalvex(ElemType a, ElemType b) {
//如果頂點A和頂點B的鄰接頂點相同
if(a.adjvex == b.adjvex) {
//返回1
return OK;
}//if
else {
//返回0
return ERROR;
}//else
}//equalvex
/*
函數:NextAdjVex
參數:ALGraph G 圖G
VertexType v v是G中某個頂點
VertexType w w是v的鄰接頂點
返回值:返回v的(相對於w的)下一個鄰接頂點的序號。
若w是v的最後一個鄰接點,則返回-1
作用:得到頂點v相對於頂點w的下一個鄰接頂點的序號。
*/
int NextAdjVex(ALGraph G, VertexType v, VertexType w) {
//p是工作指針
//p1在Point()中用作輔助指針
LinkList p, p1;
//e保存了弧結點的信息
ElemType e;
//找出頂點v在圖G中的序號v1
int v1 = LocateVex(G, v);
//找出頂點w在圖G中的序號,由e.adjvex保存
e.adjvex = LocateVex(G, w);
//p指向頂點v的鏈表中鄰接頂點爲w的結點
p = Point(G.vertices[v1].firstarc, e, equalvex, p1);
//沒找到w或w是最後一個鄰接點
if(!p || !p->next) {
return -1;
}//if
else { //找出了與頂點v鄰接的頂點w
//返回v的相對於w的下一個鄰接頂點的序號
return p->next->data.adjvex;
}//else
}//NextAdjVex
/*
函數:InsertVex
參數:ALGraph &G 圖G的引用
VertexType v 被插入的頂點v
返回值:狀態碼,操作成功返回OK,否則返回ERROR
作用:在圖G中增添新頂點v,但不增添與頂點相關的弧。
*/
Status InsertVex(ALGraph &G, VertexType v) {
//加入新的頂點,設置其名稱
G.vertices[G.vexnum].data = v;
//由於還沒有添加鄰接弧的信息,所以firstarc的值爲空
G.vertices[G.vexnum].firstarc = NULL;
//圖G的頂點數加1
G.vexnum++;
//操作成功
return OK;
}//InsertVex
/*
函數:DeleteVex
參數:ALGraph &G 圖G的引用
VertexType v 被刪除的頂點v
返回值:狀態碼,操作成功返回OK,否則返回ERROR
作用:刪除G中頂點v及其相關的弧
*/
Status DeleteVex(ALGraph &G, VertexType v) {
//i是臨時變量,存儲了以v爲出度的弧或邊數
//j是臨時變量,保存頂點v的序號
int i, j, k;
//臨時變量,用於設置查找以頂點v爲鄰接頂點的弧結點的條件
ElemType e;
//工作指針p,指向找到的以頂點v爲鄰接頂點的弧結點
//工作指針p1指向p的前驅
LinkList p, p1;
//j是頂點v的序號
j = LocateVex(G, v);
//檢查在圖G中是否找到頂點v
if(j < 0) {
return ERROR;
}//if
//i存儲了以v爲出度的弧或邊數(也就是弧鏈表長度)
i = ListLength(G.vertices[j].firstarc);
//邊或弧數-i
G.arcnum -= i;
//如果圖G是網,還需要另外銷燬保存權值的內存空間
if(G.kind % 2) {
//對應的弧或邊鏈表不空
while(G.vertices[j].firstarc) {
//刪除鏈表的第1個結點,並將值賦給e
ListDelete(G.vertices[j].firstarc, 1, e);
//考慮到無向網的權值空間共享問題,所以要先判斷
//權值空間是否已經被釋放再執行空間釋放操作
if(e.info != NULL) {
//釋放e.info指示的動態生成的權值空間
free(e.info);
e.info == NULL; //指針置空
}//if
}//while
}//if
else { //如果是圖
//只需要銷燬弧或邊鏈表就可以了,不用考慮權值問題
DestoryList(G.vertices[j].firstarc);
}//else
//頂點數減1
G.vexnum--;
//保存頂點採用了順序存儲結構,所以刪除頂點v之後,後面的頂點需要前移
for(i = j; i < G.vexnum; i++) {
G.vertices[i] = G.vertices[i + 1];
}//for
//之前的操作只是刪除了頂點v和其後面的弧鏈表。還有很多善後工作沒有做:
//1.之前的刪除操作只考慮了出度,沒考慮入度。我們不僅需要刪除掉v指向
// 其他頂點的信息,還需要刪除其他頂點指向頂點v的弧。
//2.如果頂點v不是頂點數組中的最後一個頂點,由於在刪除頂點v後,頂點v
// 後面的頂點需要做前移操作。此時後面頂點在頂點數組中的位序就會改變
// 但是與後面頂點相關的弧結點中存儲的鄰接頂點的位序並未隨之更新,
// 所以我們還需要將這些鄰接頂點的弧結點中的鄰接頂點的位序全部更新一遍。
// 保證刪除操作後存儲的圖的鄰接信息的正確性。
//3.如果是網,還需要考慮權值存儲空間的釋放問題。
//刪除以v爲入度的弧或邊且必要時修改表結點的頂點位置值
for(i = 0; i < G.vexnum; i++) {
//j保存了頂點v的序號,接下來要利用e找出所有與v鄰接的頂點
e.adjvex = j;
//p指向在G.vertices[i].firstarc指示的弧鏈表中以v爲鄰接點的弧結點
//p1指向p的前驅,若p指向首元結點,則p1爲NULL
p = Point(G.vertices[i].firstarc, e, equalvex, p1);
//頂點i的鄰接表上有v爲入度的結點
if(p) { //if(p) <=> if(p != NULL)
//p不指向首元結點
if(p1) { //if(p1) <=> if(p1 != NULL)
//從鏈表中刪除p所指結點
p1->next = p->next;
}//if
else { // p指向首元結點
//頭指針指向下一結點
G.vertices[i].firstarc = p->next;
}//else
//有向圖或者有向網
if(G.kind < 2) {
//邊或弧數-1
G.arcnum--;
//有向網直接釋放權值內存空間就行,不用考慮對稱位置的問題
if(G.kind == DN) {
//釋放動態生成的權值空間
free(p->data.info);
}//if
}//if
//釋放v爲入度的結點
free(p);
}//if
//解決由於結點前移造成後面結點弧的鄰接信息不正確的問題
//對於adjvex域>j的結點,其序號-1
for(k = j + 1; k <= G.vexnum; k++) {
//將以頂點v後面頂點爲鄰接頂點的頂點在圖中的序號
//保存在e.adjvex中
e.adjvex = k;
//使p指向以頂點v爲入度的弧結點
//p1指向p的前驅,若p指向頭結點,p1=NULL
p = Point(G.vertices[i].firstarc, e, equalvex, p1);
//判斷是否找到這樣的弧結點
if(p) { //if(p) <=> if(p != NULL)
//由於頂點v被刪除造成v後面的頂點前移。所以存儲在弧結點中的
//序號應該和前移之後的頂點序號保持一致,即序號-1
p->data.adjvex--;
}//if
}//for
}//for
//操作成功
return OK;
}//DeleteVex
/*
函數:InsertArc
參數:ALGraph &G 圖G的引用
VertexType v 圖中的頂點w (弧尾)
VertexType w 圖中的頂點v (弧頭)
返回值:狀態碼,操作成功返回OK,否則返回ERROR
作用:在G中增添弧<v,w>,若G是無向的,則還增添對稱弧<w,v>
*/
Status InsertArc(ALGraph &G, VertexType v, VertexType w) {
//臨時變量,存儲了弧結點的信息
ElemType e;
//i是頂點v在圖G中的序號
int i = LocateVex(G, v);
//j是頂點w在圖G中的序號
int j = LocateVex(G, w);
//檢查頂點v或w是否在圖G中存在,若不存在則終止函數向下執行
if(i < 0 || j < 0) {
return ERROR;
}//if
//插入一條邊之後圖G的弧或邊的數目加1
//注意:網中兩個頂點之間正向和反向的弧算一條,不要重複添加
G.arcnum++;
//在頂點v的弧鏈表中插入鄰接點w的信息
e.adjvex = j;
//圖沒有權值,所以將權值指針e.info的初始值設置爲NULL
e.info = NULL;
//如果是在網中插入弧,還需要設置弧的權值
if(G.kind % 2) { //if(G.kind % 2) <=> if(G.kind % 2 != 0)
//動態申請存放權值的空間
e.info = (InfoType *)malloc(sizeof(int));
//從鍵盤輸入權值的信息
printf("請輸入弧(邊)%d→%d的權值: ", v, w);
scanf("%d", &e.info);
}//if
//將e插在弧尾v的弧鏈表表頭
ListInsert(G.vertices[i].firstarc, 1, e);
//在無向圖或無向網中插入弧,還要加入反向的另一條弧
if(G.kind >= 2) {
//無向圖沒有權值,兩條弧的e.info都是NULL
//無向網的兩條弧共用一個權值存儲單元,e.info不變
e.adjvex = i;
//將e插在頂點w弧鏈表的表頭
ListInsert(G.vertices[j].firstarc, 1, e);
//注意:網中兩個頂點之間正向和反向的弧算一條,不要重複添加
}//if
//操作成功
return OK;
}//InsertArc
/*
函數:DeleteArc
參數:ALGraph &G 圖G的引用
VertexType v 圖中的頂點w (弧尾)
VertexType w 圖中的頂點v (弧頭)
返回值:狀態碼,操作成功返回OK,否則返回ERROR
作用:在G中刪除弧<v,w>,若G是無向的,則還刪除對稱弧<w,v>
*/
Status DeleteArc(ALGraph &G, VertexType v, VertexType w) {
//k記錄了刪除操作的結果
Status k;
//臨時變量e存儲了查找鄰接點的比較條件
ElemType e;
//i是頂點v(弧尾)在圖G中的序號
int i = LocateVex(G, v);
//j是頂點w(弧頭)在圖G中的序號
int j = LocateVex(G, w);
//檢查i和j是否合法
if(i < 0 || j < 0 || i == j) {
return ERROR;
}//if
//刪除<v,w>這條弧
//設置在頂點v的弧鏈表中插入的弧結點的鄰接點的序號爲j
e.adjvex = j;
//從頂點v的弧鏈表中刪除鄰接頂點w的弧結點
k = DeleteElem(G.vertices[i].firstarc, e, equalvex);
//刪除弧<v,w>成功
if(k) { //if(k) <=> if(k != ERROR)
//圖中弧或邊數減1
G.arcnum--;
//如果是網,還需要釋放權值存儲單元的空間
if(G.kind % 2) {
free(e.info);
}//if
//如果是無向網或無向圖則還需要刪除對稱弧<w,v>
if(G.kind >= 2) {
//設置w的鄰接點爲v
e.adjvex = i;
//刪除w到v的弧
DeleteElem(G.vertices[j].firstarc, e, equalvex);
}//if
//操作成功
return OK;
}//if
else { // 沒找到待刪除的弧
return ERROR;
}//else
}//DeleteArc
/*
函數:Display
參數:ALGraph &G 圖G的引用
VertexType v 圖中的頂點w (弧尾)
VertexType w 圖中的頂點v (弧頭)
返回值:狀態碼,操作成功返回OK,否則返回ERROR
作用:以鄰接矩陣形式輸出鄰接表
*/
void Display(ALGraph G) {
//工作指針p
ArcNode *p;
//打印圖的類型
switch(G.kind) {
case DG: printf("此圖爲有向圖!\n");
break;
case DN: printf("此圖爲有向網!\n");
break;
case UDG:printf("此圖爲無向圖!\n");
break;
case UDN:printf("此圖爲無向網!\n");
}//switch
//打印圖的總頂點數,總邊數以及每條邊
printf("\n圖中共有%d個頂點,%d條弧(邊),它們分別是:\n", G.vexnum, G.arcnum);
printf("+----+-----------------------------------------------\n");
printf("|頂點| 鄰接頂點(和權值) \n");
printf("+----+-----------------------------------------------\n");
for(int i = 0; i < G.vexnum; i++){
printf("| %d |", G.vertices[i].data);
//p指向某個頂點弧鏈表的首元結點
p = G.vertices[i].firstarc;
//遍歷整個弧鏈表
while(p) { //while(p) <=> while(p != NULL)
//打印弧頭和弧尾的信息
printf(" →%d ", G.vertices[p->data.adjvex].data);
//如果圖G是網,還需要打印出權值
if(G.kind % 2) {
printf(",權值:%d ", *(p->data.info));
}//if
//p指向弧鏈表下一個結點
p = p->nextarc;
}//while
//輸出完一個頂點之後換行
printf("\n+----+-----------------------------------------------\n");
}//for
//輸出換行,使結果美觀
printf("\n");
}//Display
源文件:DFS_BFS_ALGraph.cpp
//------------------------元素訪問函數----------------------
/*
函數:print
參數:VertexType v 頂點v
返回值:無
作用:元素訪問函數,供遍歷使用。
*/
void print(VertexType v) {
printf(" %d ", v);
}//print
//------------------------深度優先遍歷----------------------
//訪問標誌數組(長度等於最大頂點數)
int visited[MAX_VERTEX_NUM];
//訪問函數(通過全局變量的方式傳遞可以減少參數傳遞的次數)
void(*VisitFunc)(VertexType v);
/*
函數:DFS
參數:ALGraph G 圖G
int v 訪問圖G中的第v個頂點
返回值:無
作用:從第v個頂點出發遞歸地深度優先遍歷圖G。
*/
void DFS(ALGraph G, int v) {
//w是臨時變量,循環控制用
int w;
//設置訪問標誌爲TRUE,表示該頂點已訪問
visited[v] = TRUE;
//訪問第v個頂點
VisitFunc(G.vertices[v].data);
//依次訪問v的未被訪問的鄰接點
for(w = FirstAdjVex(G, G.vertices[v].data); w >= 0;
w = NextAdjVex(G, G.vertices[v].data, G.vertices[w].data)) {
//對v的尚未訪問的鄰接點w遞歸調用深度優先遍歷算法DFS
if(!visited[w]) {
DFS(G, w);
}//if
}//for
}//DFS
/*
函數:DFS
參數:ALGraph G 圖G
void(*Visit)(char*) 指向遍歷函數的指針
返回值:無
作用:對圖G作深度優先遍歷。
*/
void DFSTraverse(ALGraph G, void(*Visit)(VertexType)) {
//使用全局變量VisitFunc,使DFS不必設函數指針參數
VisitFunc = Visit;
//初始化訪問標誌數組每個頂點的標誌值爲FALSE,表示所有頂點都未被訪問
for(int v = 0; v < G.vexnum; v++) {
visited[v] = FALSE;
}//for
//依次訪問圖中每個頂點
for(int v = 0; v < G.vexnum; v++) {
//對尚未訪問的頂點調用DFS
if(!visited[v]) {
DFS(G, v);
}//if
}//for
//遍歷完成後換行,使輸出美觀
printf("\n");
}//DFSTraverse
//------------------------廣度優先遍歷-----------------------
/*
函數:BFSTraverse
參數:ALGraph G 圖G
void(*Visit)(char*) 指向遍歷函數的指針
返回值:無
作用:按廣度優先非遞歸遍歷圖G。使用輔助隊列Q和訪問標誌數組visited。
*/
void BFSTraverse(ALGraph G, void(*Visit)(VertexType)) {
//v是臨時變量
int v, u, w;
//聲明隊列
Queue Q;
//設置結點訪問標記數組的初始值爲FALSE,表示沒有頂點被訪問過
for(v = 0; v < G.vexnum; ++v) {
visited[v] = FALSE;
}//for
//初始化輔助隊列Q,得到一個空隊列
InitQueue(Q);
//如果是連通圖,只要一次循環就可以遍歷全部頂點
for(v = 0; v < G.vexnum; v++) {
//v尚未訪問
if(!visited[v]) {
//設置頂點v的標誌爲TRUE,表示頂點v已被訪問
visited[v] = TRUE;
//調用訪問函數Visit訪問頂點v
Visit(G.vertices[v].data);
//v入隊列
EnQueue(Q, v);
//隊列不空
while(!QueueEmpty(Q)) {
//隊頭元素出隊並置爲u
DeQueue(Q, u);
//訪問u尚未訪問的鄰接頂點
for(w = FirstAdjVex(G, G.vertices[u].data); w >= 0;
w = NextAdjVex(G, G.vertices[u].data, G.vertices[w].data)) {
//w爲u的尚未訪問的鄰接頂點
if(!visited[w]) {
//設置頂點w的訪問標誌爲TRUE
visited[w] = TRUE;
//訪問頂點w
Visit(G.vertices[w].data);
//頂點w入隊
EnQueue(Q, w);
}//if
}//for
}//while
}//if
}//for
//遍歷結束
printf("\n");
}//BFSTraverse
源文件:基本操作及遍歷測試.cpp
//**************************引入頭文件*****************************
#include <stdio.h> //使用了標準庫函數
#include <stdlib.h> //使用了動態內存分配函數
#include "my_constants.h" //引入自定義的符號常量,主要是狀態碼
#include "ALGraph.h" //引入圖的鄰接表存儲結構定義
#include "LinkedList.cpp" //引入單鏈表實現,用到其中的插入、刪除等操作
#include "LinkedQueue.cpp" //引入鏈隊列實現
#include "ALGraph.cpp" //引入圖的鄰接表存儲結構基本操作實現
#include "DFS_BFS_ALGraph.cpp" //引入圖的深度優先遍歷和廣度優先遍歷實現
int main() {
printf("----------------圖的鄰接表存儲表示引用版演示程序------------------\n\n");
//臨時變量
int j, k, n;
//圖的鄰接表存儲方式
ALGraph g;
//臨時變量
VertexType v1, v2;
//創建圖g並打印初始狀態
printf("->測試圖G的創建:");
CreateGraph(g);
printf("圖G創建成功!\n");
printf("->打印創建後的圖G:\n");
Display(g);
//測試頂點和弧的插入
printf("->測試頂點和弧的插入:\n");
printf("->插入新頂點,請輸入頂點(將作爲有向圖弧尾)的值: ");
scanf("%d", &v1);
InsertVex(g, v1);
printf("->插入與新頂點有關的弧或邊,請輸入弧或邊數: ");
scanf("%d", &n);
for(int i = 0; i < n; i++) {
printf("->請輸入新的弧頭頂點的值: ");
scanf("%d", &v2);
//無向圖的對稱弧會自動被InsertArc函數創建,
//無需再次調用InsertArc函數創建對稱的弧結點
InsertArc(g, v1, v2);
}//for
//以鄰接矩陣形式打印圖G,查看插入邊之後圖G發生了什麼變化
printf("->插入新的頂點和弧之後,圖G的鄰接矩陣:\n");
Display(g);
printf("->基於鄰接表存儲結構的深度優先遍歷序列(從%d頂點開始):\n", g.vertices[0].data);
DFSTraverse(g, print);
printf("\n");
printf("->基於鄰接表存儲結構的廣度優先遍歷序列(從%d頂點開始):\n", g.vertices[0].data);
BFSTraverse(g, print);
printf("\n");
//測試無向網弧的刪除
printf("->測試刪除一條邊或弧,請輸入待刪除邊或弧的弧尾 弧頭(逗號隔開):");
scanf("%d,%d", &v1, &v2);
DeleteArc(g, v1, v2);
printf("->刪除弧後的圖G:\n");
Display(g);
//測試修改頂點的值
printf("->測試修改頂點的值,請輸入原值 新值(逗號隔開): ");
scanf("%d,%d", &v1, &v2);
PutVex(g, v1, v2);
printf("->修改頂點後的圖G:\n");
Display(g);
//測試刪除頂點及相關的弧或邊
printf("->測試刪除頂點及相關的弧或邊,請輸入頂點的值: ");
scanf("%d", &v1);
DeleteVex(g, v1);
printf("->刪除頂點後的圖G:\n");
Display(g);
//測試銷燬圖G
printf("->測試銷燬圖G:");
DestroyGraph(g);
printf("銷燬成功!");
printf("演示結束");
}//main
我測試用的是書上P157頁的無向圖G2。
測試程序的輸入和輸出:
----------------圖的鄰接表存儲表示引用版演示程序------------------
->測試圖G的創建:請輸入圖的類型(有向圖輸入0, 有向網輸入1,無向圖輸入2,無向網輸入3): 2
請輸入圖的頂點數,邊數: 5,6
請輸入5個頂點的值(用空格隔開):
1 2 3 4 5
請輸入每條弧(邊)的弧尾和弧頭(以逗號作爲間隔):
1,2
1,4
2,3
2,5
3,4
3,5
圖G創建成功!
->打印創建後的圖G:
此圖爲無向圖!
圖中共有5個頂點,6條弧(邊),它們分別是:
+----+-----------------------------------------------
|頂點| 鄰接頂點(和權值)
+----+-----------------------------------------------
| 1 | →4 →2
+----+-----------------------------------------------
| 2 | →5 →3 →1
+----+-----------------------------------------------
| 3 | →5 →4 →2
+----+-----------------------------------------------
| 4 | →3 →1
+----+-----------------------------------------------
| 5 | →3 →2
+----+-----------------------------------------------
->測試頂點和弧的插入:
->插入新頂點,請輸入頂點(將作爲有向圖弧尾)的值: 6
->插入與新頂點有關的弧或邊,請輸入弧或邊數: 2
->請輸入新的弧頭頂點的值: 1
->請輸入新的弧頭頂點的值: 2
->插入新的頂點和弧之後,圖G的鄰接矩陣:
此圖爲無向圖!
圖中共有6個頂點,8條弧(邊),它們分別是:
+----+-----------------------------------------------
|頂點| 鄰接頂點(和權值)
+----+-----------------------------------------------
| 1 | →6 →4 →2
+----+-----------------------------------------------
| 2 | →6 →5 →3 →1
+----+-----------------------------------------------
| 3 | →5 →4 →2
+----+-----------------------------------------------
| 4 | →3 →1
+----+-----------------------------------------------
| 5 | →3 →2
+----+-----------------------------------------------
| 6 | →2 →1
+----+-----------------------------------------------
->基於鄰接表存儲結構的深度優先遍歷序列(從1頂點開始):
1 6 2 5 3 4
->基於鄰接表存儲結構的廣度優先遍歷序列(從1頂點開始):
1 6 4 2 3 5
->測試刪除一條邊或弧,請輸入待刪除邊或弧的弧尾 弧頭(逗號隔開):3,4
->刪除弧後的圖G:
此圖爲無向圖!
圖中共有6個頂點,8條弧(邊),它們分別是:
+----+-----------------------------------------------
|頂點| 鄰接頂點(和權值)
+----+-----------------------------------------------
| 1 | →6 →4 →2
+----+-----------------------------------------------
| 2 | →6 →5 →3 →1
+----+-----------------------------------------------
| 3 | →5 →2
+----+-----------------------------------------------
| 4 | →3 →1
+----+-----------------------------------------------
| 5 | →3 →2
+----+-----------------------------------------------
| 6 | →2 →1
+----+-----------------------------------------------
->測試修改頂點的值,請輸入原值 新值(逗號隔開): 3,7
->修改頂點後的圖G:
此圖爲無向圖!
圖中共有6個頂點,8條弧(邊),它們分別是:
+----+-----------------------------------------------
|頂點| 鄰接頂點(和權值)
+----+-----------------------------------------------
| 1 | →6 →4 →2
+----+-----------------------------------------------
| 2 | →6 →5 →7 →1
+----+-----------------------------------------------
| 7 | →5 →2
+----+-----------------------------------------------
| 4 | →7 →1
+----+-----------------------------------------------
| 5 | →7 →2
+----+-----------------------------------------------
| 6 | →2 →1
+----+-----------------------------------------------
->測試刪除頂點及相關的弧或邊,請輸入頂點的值: 4
->刪除頂點後的圖G:
此圖爲無向圖!
圖中共有5個頂點,6條弧(邊),它們分別是:
+----+-----------------------------------------------
|頂點| 鄰接頂點(和權值)
+----+-----------------------------------------------
| 1 | →6 →2
+----+-----------------------------------------------
| 2 | →6 →5 →7 →1
+----+-----------------------------------------------
| 7 | →5 →2
+----+-----------------------------------------------
| 5 | →7 →2
+----+-----------------------------------------------
| 6 | →2 →1
+----+-----------------------------------------------
->測試銷燬圖G:銷燬成功!演示結束
--------------------------------
Process exited with return value 0
Press any key to continue . . .
總結:
鄰接表的操作比鄰接矩陣要麻煩很多,除了鏈表的各項基本操作之外還多出一個info,這個info指向一個存儲權值的存儲單元,這個存儲單元需要動態的內存分配和釋放,而且只在圖的類型是網的時候才被創建和使用,如果是無向網,一個權值存儲單元要供兩個弧結點使用,釋放的時候也要小心,要防止釋放已經被釋放的內存空間。維護info很是麻煩。
下次的文章將介紹圖的最小生成樹算法的實現。希望大家繼續關注我的博客,再見!
附:鏈隊列和不帶頭結點單鏈表的程序實現如下:
源文件:LinkedQueue.cpp
//------------------隊列的鏈式存儲表示-----------------------
typedef int QElemType; //隊列元素爲二叉樹指針類型
typedef struct QNode{ //鏈隊列的C語言表示
QElemType data; //數據域
struct QNode * next; //指針域
}QNode,* QueuePtr;
typedef struct{
QueuePtr front; //隊頭指針
QueuePtr rear; //隊尾指針
}Queue;
//--------------------------隊列的相關函數(供非遞歸層序遍歷使用)----------------------------
/*
函數:MallocQNode
參數:無
返回值:指向新申請結點的指針
作用:爲鏈隊列結點申請內存的函數
*/
QueuePtr MallocQNode(){
//工作指針p,指向新申請的結點
QueuePtr p;
//if(!(p = (QueuePtr)malloc(sizeof(QNode)))) 相當於以下兩行代碼:
//p = (QueuePtr)malloc(sizeof(QNode));
//if(!p) <=> if(p != NULL)
//申請結點的內存空間,若失敗則提示並退出程序
if(!(p = (QueuePtr)malloc(sizeof(QNode)))){
printf("內存分配失敗,程序即將退出!\n");
exit(OVERFLOW);
}//if
//返回新申請結點的地址
return p;
}//MallocQNode
/*
函數:InitQueue
參數:Queue &Q 鏈隊列引用
返回值:狀態碼,操作成功返回OK
作用:構建一個空隊列 Q
*/
Status InitQueue(Queue &Q) {
//申請頭結點的內存空間,並使隊頭和隊尾指針同時指向它
Q.front = Q.rear = MallocQNode();
//由於頭結點剛剛初始化,後面還沒有元素結點
Q.front->next = NULL;
//頭結點數據域記錄了鏈隊列長度
//由於此時鏈隊列沒有數據節點,所以將頭結點數據域設爲0
Q.front->data = 0;
//操作成功
return OK;
}//InitQueue
/*
函數:DestoryQueue
參數:Queue &Q 鏈隊列引用
返回值:狀態碼,操作成功返回OK
作用:銷燬隊列Q
*/
Status DestoryQueue(Queue &Q){
//從頭結點開始向後逐個釋放結點內存空間
while(Q.front){ //while(Q.front) <=> while(Q.front != NULL)
//隊尾指針指向被刪除結點的後繼結點
Q.rear = Q.front->next;
//釋放Q.front指向的被刪除結點的空間
free(Q.front);
//隊頭指針後移,指向下一個待刪除結點
Q.front = Q.rear;
}//while
//操作成功
return OK;
}//DestoryQueue
/*
函數:QueueEmpty
參數:Queue Q 鏈隊列Q
返回值:狀態碼,若Q爲空隊列,則返回TRUE;否則返回FALSE
作用:判斷隊列Q是否爲空
*/
Status QueueEmpty(Queue Q){
//隊頭指針和隊尾指針均指向鏈隊列頭結點表示鏈隊列爲空
if(Q.rear == Q.front){
return TRUE;
}//if
else {
return FALSE;
}//else
}//QueueEmpty
/*
函數:EnQueue
參數:Queue &Q 鏈隊列Q的引用
QElemType e 被插入的元素e
返回值:狀態碼,操作成功後返回OK。
作用:插入元素e爲Q的新的隊尾元素
*/
Status EnQueue(Queue &Q, QElemType e){
//申請一個新的結點,並使p指向這個新結點
QueuePtr p = MallocQNode();
//將待插入元素e保存到新結點數據域
p->data = e;
//由於新結點要插在隊尾,後面沒有其他結點,所以後繼指針域的值爲NULL
p->next = NULL;
//將新結點鏈入到隊尾
//隊列要求插入操作只能發生在隊尾
Q.rear->next = p;
//修正隊尾指針,使之指向p所指向的新插入的結點
Q.rear = p;
//由於插入一個結點,所以存儲在頭結點中的隊列長度+1
Q.front->data++;
//插入操作成功
return OK;
}//EnQueue
/*
函數:DeQueue
參數:Queue &Q 鏈隊列Q的引用
QElemType &e 帶回被刪除結點的元素e
返回值:狀態碼,操作成功後返回OK。
作用:若隊列不空,則刪除Q的隊頭元素,用e返回其值
*/
Status DeQueue(Queue &Q, QElemType &e){
//注意隊列爲空和隊列不存在的區別,隊列爲空,頭結點一定存在,
//隊列不存在時頭結點一定不存在
//對空隊列執行出隊操作沒有意義,出隊操作執行前要先檢查隊列是否爲空
if(QueueEmpty(Q)) { //if(QueueEmpty(Q)) <=> if(QueueEmpty(Q) != TRUE)
return ERROR;
}//if
//工作指針p指向隊頭第一個結點(不是頭結點,是頭結點的後繼)
//隊列要求刪除操作只能發生在隊頭,所以p指向的就是待刪除節點
QueuePtr p = Q.front->next;
//保存被刪除結點的值
e = p->data;
//在刪除操作執行前修正隊頭指針的位置,使之在刪除結點後指向新的隊頭結點
Q.front->next = p->next;
//若被刪除結點恰好是隊尾結點,那麼該結點被刪除後,隊列將會變成空隊列
//此時剛好滿足空隊列條件:Q.rear == Q.front,所以要修正隊尾指針的位置,使之指向頭結點
if(Q.rear == p) {
Q.rear = Q.front;
}//if
//在隊頭指針和隊尾指針的位置都調整好了之後就可以
//放心地釋放p指向的結點的內存空間了
free(p);
//由於從隊列中刪除了一個結點,頭結點存儲的隊列長度應當-1
Q.front->data--;
//操作成功
return OK;
}//DeQueue
/*
函數:Print
參數:ElemType e 被訪問的元素
返回值:狀態碼,操作成功返回OK,操作失敗返回ERROR
作用:訪問元素e的函數,通過修改該函數可以修改元素訪問方式,
該函數使用時需要配合遍歷函數一起使用。
*/
Status Print_Queue(QElemType e){
//指定元素的訪問方式是控制檯打印輸出
printf("%6.2f ",e);
//操作成功
return OK;
}//Print
/*
函數:QueueTraverse
參數:Queue Q 鏈隊列Q
Status (* visit)(QElemType) 函數指針,指向元素訪問函數。
返回值:狀態碼,操作成功返回OK,操作失敗返回ERROR
作用:調用元素訪問函數按出隊順序完成鏈隊列的遍歷,但並未真正執行出隊操作
*/
Status QueueTraverse(Queue Q, Status (* visit)(QElemType)) {
//對空隊列進行遍歷操作沒有意義,所以遍歷操作前要先判斷隊列是否爲空
//注意隊列爲空和隊列不存在的區別,隊列爲空,頭結點一定存在,
//隊列不存在時頭結點一定不存在
if(QueueEmpty(Q)) { //if(QueueEmpty(Q)) <=> if(QueueEmpty(Q) != TRUE)
return ERROR;
}//if
//工作指針p指向隊頭結點
QueuePtr p = Q.front->next;
//從隊頭結點開始依次訪問每個結點,直到隊尾
while(p) { //while(p) <=> while(p != NULL)
//調用元素訪問函數
if(!visit(p->data)) {
printf("輸出發生錯誤!\n");
return ERROR;
}//if
//工作指針p後移,指向下一個元素
p = p->next;
}//while
//輸出換行,使結果清楚美觀
printf("\n");
//操作成功
return OK;
}//QueueTraverse
源文件:LinkedList.cpp
//注意:此單鏈表不同於書上的單鏈表,書上的單鏈表是有頭結點的,
// 但是此單鏈表無頭結點,插入、刪除和很多操作會有所區別。
/*
函數:InitList
參數:LinkList &L 單鏈表引用
返回值:狀態碼,操作成功返回OK,操作失敗返回ERROR
作用:構造一個空的不帶頭結點的單鏈表L
*/
Status InitList(LinkList &L) {
//沒有頭結點的空單鏈表頭指針爲空
L = NULL;
//操作成功
return OK;
}//InitList
/*
函數:DestoryList
參數:LinkList &L 單鏈表引用
返回值:狀態碼,操作成功返回OK,操作失敗返回ERROR
作用:若線性表L已存在則將L重置爲空表。
*/
Status DestoryList(LinkList &L) {
//工作指針p
LinkList p;
//單鏈表L不空
while(L) {
//沒有頭結點的單鏈表頭指針L指向首元結點
//p指向首元結點
p = L;
//L指向p的後繼
L = L->next;
//釋放首元結點
free(p);
}//while
//操作成功
return OK;
}//DestoryList
/*
函數:ListEmpty
參數:LinkList L 單鏈表L
返回值:狀態碼,若L爲空表返回TRUE,否則返回FALSE
作用:判斷單鏈表L是否爲空
*/
Status ListEmpty(LinkList L) {
//頭指針直接指向首元結點,若頭指針爲NULL,則鏈表爲空。
if(L) { //if(L) <=> if(L != NULL)
return FALSE;
}//if
else {
return TRUE;
}//else
}//ListEmpty
/*
函數:ListLength
參數:LinkList L 單鏈表引用
返回值:返回L中數據元素個數
作用:得到表長
*/
int ListLength(LinkList L) {
//i是一個計數器,記錄了表長
int i = 0;
//工作指針p指向單鏈表的首元結點
LinkList p = L;
//遍歷整個單鏈表求出表長
while(p) { //while(p) <=> while(p != NULL)
//p指向下一個結點
p = p->next;
//找到一個結點計數器+1
i++;
}//while
//返回表長
return i;
}//ListLength
/*
函數:GetElem
參數:LinkList L 不帶頭結點的單鏈表的頭指針
int i 查找第i個元素
ElemType &e 使用e帶回第i個元素的值
返回值:狀態碼,找到第i個元素返回OK,否則返回ERROR
作用:當第i個元素存在時,其值賦給e並返回OK,否則返回ERROR
*/
Status GetElem(LinkList L, int i, ElemType &e) {
//臨時變量,記錄已經遍歷過的元素個數
int j = 1;
//工作指針p指向單鏈表首元結點
LinkList p = L;
//檢查參數i的值是否合法:i是否越界
if(i < 1) {
return ERROR;
}//if
//在單鏈表中從頭開始向後查找第i個元素
//沒到第i個元素,也沒到表尾
//while(j < i && p) <=> while(j < i && p != NULL)
while(j < i && p) {
//計數器j+1
j++;
//p指向下一個結點
p = p->next;
}//while
//找到了第i個元素所在結點,此時p指向該結點
if(j == i) {
//複製第i個元素的值到e
e = p->data;
//操作成功
return OK;
}//if
else {
return ERROR;
}//else
}//GetElem
/*
函數:LocateElem
參數:LinkList L 不帶頭結點的單鏈表的頭指針
ElemType e 查找元素e
Status(*compare)(ElemType, ElemType) 函數指針,指向元素比較函數compare()
返回值:返回單鏈表L中第1個與e滿足關係compare()的數據元素的位序。
若這樣的數據元素不存在,則返回值爲0。
作用:當第i個元素存在時,其值賦給e並返回OK,否則返回ERROR
*/
int LocateElem(LinkList L, ElemType e, Status(*compare)(ElemType, ElemType)) {
//臨時變量,計數器
int i = 0;
//工作指針p指向首元結點
LinkList p = L;
//在鏈表L中遍歷每一個結點,查找值爲e的元素
while(p) { //while(p) <=> while(p != NULL)
//計數器+1
i++;
//找到這樣的數據元素
//if(compare(p->data, e)) <=> if(compare(p->data, e) != 0)
if(compare(p->data, e)) {
return i;
}//if
//p指向下一個結點
p = p->next;
}//while
//若沒有找到值爲e的結點,返回0
return 0;
}//LocateElem
/*
函數:ListInsert
參數:LinkList &L 單鏈表引用
int i 在第i個位置之前插入
ElemType e 被插入的元素e
返回值:狀態碼,操作成功返回OK,否則返回ERROR。
作用:不帶頭結點的單鏈線性表L中第i個位置之前插入元素e
*/
Status ListInsert(LinkList &L, int i, ElemType e) {
//計數器,記錄查找過的結點數
int j = 1;
//p是工作指針,初始位置指向單鏈表首元結點
//s指向被插入結點
LinkList p = L, s;
//檢查參數i的值是否合法
if(i < 1) { //i越界
return ERROR;
}//if
//創建被插入結點並使s指向新結點
s = (LinkList)malloc(sizeof(LNode));
//將被插入的值e填充到新結點s的數據域中
s->data = e;
//注意:此鏈表不帶頭結點,做插入操作需判斷插入位置是否在表頭
// 如果是在表頭插入需要修改頭指針,不在表頭插入則不需要。
if(i == 1) { //插在表頭
//直接把鏈表原有結點掛到新結點後面
s->next = L;
//使頭指針L指向s指向的新結點,s成爲新的首元結點
L = s;
}//if
else { //插入位置不在表頭
//尋找第i-1個結點
while(p && j < i - 1) {
//p指向下一個結點
p = p->next;
//計數器j+1
j++;
}//while
//沒找到第i-1個結點
if(!p) { //if(!p) <=> if(p != NULL)
//操作失敗
return ERROR;
}//if
//將第i-1個結點後面的結點掛到s指向的結點後面
s->next = p->next;
//把s指向的結點連同後面的結點掛到原來的第i-1個結點後面
//s成爲新的第i個結點。
p->next = s;
}//else
//操作成功
return OK;
}//ListInsert
/*
函數:ListDelete
參數:LinkList &L 單鏈表引用
int i 在第i個位置之前刪除
ElemType &e 帶回被刪除的元素e
返回值:狀態碼,操作成功返回OK,否則返回ERROR。
作用:在不帶頭結點的單鏈線性表L中,刪除第i個元素,並由e返回其值
*/
Status ListDelete(LinkList &L, int i, ElemType &e) {
//計數器,記錄查找過的結點數
int j = 1;
//p是工作指針,初始值指向單鏈表的首元結點
//工作指針q指向被刪除結點
LinkList p = L, q = NULL;
//注意:我們使用的是不帶頭結點的單鏈表,頭指針直接指向首元結點
// 執行刪除操作時需要判斷刪除位置是否發生在表頭。
// 如果刪除操作在表頭執行,就需要修改頭指針,否則不需要。
if(i == 1) { //被刪除的是首元結點
//刪除首元結點後,刪除前的第二個元素結點將成爲新的首元結點。
//所以需要修改單鏈表頭指針L的指向,使其指向第二個結點,爲刪除做好準備。
L = p->next;
//保存被刪除結點p的值到e
e = p->data;
//釋放首元結點的內存空間
free(p);
}//if
else {
//尋找第i-1個結點,並令p指向它
//while(p->next && j < i - 1) <=> while(p->next != NULL && j < i - 1)
while(p->next && j < i - 1) {
//p指向下一個結點
p = p->next;
//計數器+1
j++;
}//while
//檢查是否找到第i-1個結點,若沒有找到則終止函數的執行
if(!p->next || j > i - 1) {
//操作失敗
return ERROR;
}//if
//找到了第i-1個結點,p指向第i-1個結點。
//q指向p的後繼,即第i個結點,也就是被刪除結點
q = p->next;
//將q指向的被刪除結點(第i個結點)後面的結點掛到p指示的
//第i-1個結點後面。也就是把q指向的第i個結點從鏈表中隔離出來。
p->next = q->next;
//保存被刪除結點的元素值
e = q->data;
//釋放q指向的被刪除結點的內存空間
free(q);
}//else
//操作成功
return OK;
}//ListDelete
/*
函數:ListTraverse
參數:LinkList L 不帶頭結點單鏈表L的頭指針
int i 在第i個位置之前刪除
ElemType &e 帶回被刪除的元素e
返回值:狀態碼,操作成功返回OK,否則返回ERROR。
作用:遍歷不帶頭結點的單鏈表L
*/
Status ListTraverse(LinkList L, Status(*Visit)(ElemType)) {
//工作指針p指向單鏈表的首元結點
LinkList p = L;
//依次對單鏈表中每個元素調用Visit函數進行訪問依次且僅一次。
while(p) { //while(p) <=> while(p != NULL)
//一旦Visit失敗則操作失敗
//if(!Visit(p->data)) <=> if(Visit(p->data) == ERROR)
if(!Visit(p->data)) {
return ERROR;
}//if
//p指向下一個結點
p = p->next;
}//while
//輸出換行使打印結果美觀
printf("\n");
}//ListTraverse
/*
函數:Point
參數:LinkList L 單鏈表L的頭指針(注意,單鏈表沒有頭結點)
ElemType e 在單鏈表中查找元素e所在位置
Status(*equal)(ElemType, ElemType) 函數指針,指向元素比較函數
LinkList &p 帶回指向e結點前驅結點的指針
返回值:狀態碼,操作成功返回OK,操作失敗返回ERROR
作用:查找表L中滿足條件的結點。如找到,返回指向該結點的指針,
p指向該結點的前驅(若該結點是首元結點,則p=NULL)。
如表L中無滿足條件的結點,則返回NULL,p無定義。
函數equal()的兩形參的關鍵字相等,返回OK;否則返回ERROR
*/
LinkList Point(LinkList L, ElemType e, Status(*equal)(ElemType, ElemType), LinkList &p) {
//i存儲了查找元素e得到的位置
int i, j;
//查找元素e得到元素的位置i,若i=0則說明單鏈表中未找到元素e
//若找到元素e,則L指向找到的結點
i = LocateElem(L, e, equal);
//在單鏈表中找到元素e
if(i) { //if(i) <=> if(i != 0)
//若找到的是首元結點
if(i == 1) {
//首元結點沒有前驅
p = NULL;
//返回指向首元結點的指針
return L;
}//if
//若找到的不是首元結點
//p指向首元結點
p = L;
//p指向找到結點的前驅
for(j = 2; j < i; j++) {
p = p->next;
}//for
//返回指向找到的結點的指針
return p->next;
}//if
//在單鏈表中沒找到值爲e的結點
return NULL;
}//Point
/*
函數:DeleteElem
參數:LinkList &L 單鏈表引用(注意,單鏈表沒有頭結點)
ElemType &e e攜帶比較條件
Status(*equal)(ElemType, ElemType) 函數指針,指向元素比較函數
返回值:狀態碼,刪除表L中滿足條件的結點成功返回TRUE;如無此結點,則返回FALSE。
作用:刪除表L中滿足條件的結點,並返回TRUE;如無此結點,則返回FALSE。
函數equal()的兩形參的關鍵字相等,返回OK;否則返回ERROR
*/
Status DeleteElem(LinkList &L, ElemType &e, Status(*equal)(ElemType,ElemType)) {
//工作指針p和q
LinkList p, q;
//從單鏈表L中找出符合條件的結點,使q指向此結點,p指向q的前驅
//若q指向首元結點,p=NULL
q = Point(L, e, equal, p);
//找到此結點,且q指向該結點
if(q) { //if(q) <=> if(q != NULL)
//該結點不是首元結點,p指向其前驅
if(p) { //if(p) <=> if(p != NULL)
//將p作爲頭指針,刪除第2個結點
ListDelete(p, 2, e);
}//if
else { // 該結點是首元結點
ListDelete(L, 1, e);
//操作成功
return TRUE;
}//else
}//if
//找不到此結點
return FALSE;
}//DeleteElem