我是自動化專業的應屆研究生,最終拿到了tplink、華爲、vivo等公司的ssp的offer,分享自己學習過的計算機基礎知識(C語言+操作系統+計算機網絡+linux)以及數據結構與算法的相關知識,保證看完讓你有所成長。
歡迎關注我,學習資料免費分享給你哦!還有其他超多學習資源,都是我自己學習過的,經過過濾之後的資源,免去你還在因爲擁有大量資源不知如何入手的糾結,讓你體系化學習。
圖的概念
圖就是有一些頂點,頂點之間有一些邊,這樣就構成了一個圖。所以對於圖這種數據結構,關鍵點就是頂點和邊,就是描述頂點和邊的關係。然後對於邊呢,對於某些問題來說,還可能有權重,以城市之間的航班爲例,各個城市就是頂點,航線就是邊,不同的航線票價不同,這個票價就是權重,所以根據邊有無權重,分爲有權圖和無權圖。根據邊是否是單方向,就是頂點A和B之間有一條邊,如果只能沿着這條邊單方向的從A到B或者從B到A,而不是既可以從A到B也可以從B到A的,這種圖就是有向圖。
無向圖:
有向圖:
有權圖:
在一個無向圖中,如果任意的兩個頂點,都可以找到一個路徑,讓他倆連通,那麼這個圖就是連通圖。否則就不是。
連通圖:任意兩個節點之間都可以找到通路
非連通圖:ABCD任意一個頂點都不能找到到達E和F的通路。但是ABCD是一個連通圖。
圖的鄰接矩陣的實現
所謂鄰接矩陣就是用二維矩陣(就是二維數組)來表示頂點之間的關係。二維矩陣是一個方陣,大小就是頂點數*頂點數,如果頂點i和頂點j之間有一條邊,如果是無向圖則,G[i][j]=1,G[j][i]=1。如果是有向圖,假設只是從i指向j那麼G[i][j]=1。對於有權圖,那麼這個矩陣的值就是權值,然後兩個頂點之間沒有邊,可以讓G[i][j]等於一個權值永遠不會相等的數來表示沒有邊。
代碼如下
typedef struct EdgeNode //邊的定義,方便爲圖插入邊,如果是有權圖,添加一個權重變量即可
{
int begin;
int end;
}* Edge;
#define MAX_NV 11 //定義圖的最大頂點數目,當然也可以使用動態數組生成一個二維數組
typedef struct Graph //如果是有權圖,添加一個權重變量即可
{
int Nv;//頂點數
int Ne;//邊數
int G[11][11];//邊的關係,兩個頂點i和j之間有邊,則置1
}* Graph;
Graph creatGraph(int Nv,int Ne)//輸入頂點數和邊數,創建一個圖,返回圖的指針。
{
Graph graph;
int i,j;
graph=(Graph)malloc(sizeof(struct Graph));
if(graph==NULL)
{
printf("內存不足\n");
return NULL;
}
graph->Ne=Ne;
graph->Nv=Nv;
for(i=0;i<graph->Nv;i++)//全部初始化爲沒有邊
{
for(j=0;j<graph->Nv;j++)
{
graph->G[i][j]=0;
}
}
return graph;
}
void insert(Graph graph,Edge e)//插入邊,這裏是無權圖,無向圖
{
if(graph==NULL||e==NULL)
{
return;
}
graph->G[e->begin][e->end]=1;
graph->G[e->end][e->begin]=1;
}
void destroy(Graph graph)//釋放圖的內存
{
free(graph);
}
圖的鄰接表實現
前面的鄰接矩陣對於頂點很多,邊特別少的情況(被叫做稀疏矩陣),矩陣中的很多元素都是0,是一種巨大的浪費。圖的鄰接表表示法,其實說到底就是一個散列而已,定義了一個頭部,頭部中包含圖中一共有多少頂點和邊,還有一個元素是指向各個頂點頭部,用來指示頂點之間的邊的關係。
與代碼對應一下:
typedef struct Gnode
{
int end;
struct Gnode* next;
}* Gnode;
typedef struct Graph
{
int Nv;//頂點數
int Ne;//邊數
struct Gnode G[20];
}* Graph;
struct Graph就是散列的管理結構,struct Gnode G[20];就是散列表的頭部,struct Gnode就是節點。
然後圖中的頂點與邊的對應關係是這樣的:G[i]就是表示第i頂點,然後向後遍歷這個鏈表,每個節點struct Gnode結構中都有end元素,它表示的就是第end頂點,就是i到end頂點之間存在一個邊。
假設有上圖所示的一個無向圖,那麼表示一個圖的鄰接表就是:
圖中的方框表示的就是struct Gnode節點。比如0與1和3之間有邊,那麼對於圖的管理結構G[0]這個鏈表就是表示的頂點0的邊,上圖可以看到這個鏈表是1,3.也就是表示0與1,0與3之間有邊。就這樣就可以表示出來了一個圖的頂點與邊的關係。
代碼實現:
Graph creatGraph(int Nv,int Ne)
{
Graph graph;
int i;
graph=(Graph)malloc(sizeof(struct Graph));
if(graph==NULL)
{
printf("內存不足");
return NULL;
}
graph->Ne=Ne;
graph->Nv=Nv;
for(i=0;i<Nv;i++)
{
graph->G[i].next=NULL;
}
return graph;
}
void insert(Graph graph,int begin,int end)//無向圖i與j有邊
//那麼在G[i]的鏈表裏插入邊,也得在G[j]的鏈表裏插入邊,如果是有向邊就可以只插入一個邊了。
{
Gnode g;
g=(Gnode)malloc(sizeof(struct Gnode));
if(g==NULL)
{
return;
}
g->next=graph->G[begin].next;
graph->G[begin].next=g;
g->end=end;
g=(Gnode)malloc(sizeof(struct Gnode));
if(g==NULL)
{
return;
}
g->next=graph->G[end].next;
graph->G[end].next=g;
g->end=begin;
}
void destroyGraph(Graph graph) //釋放圖的空間
{
int i;
Gnode g;
if(graph==NULL)
{
return;
}
else
{
for(i=0;i<graph->Nv;i++)
{
while(graph->G[i].next!=NULL)
{
g=graph->G[i].next;
graph->G[i].next=g->next;
free(g);
}
}
}
free(graph);
}
圖的深度優先遍歷
圖的深度優先遍歷,就是從一個頂點A開始出發,然後找到一個與A有邊的頂點B,將A標記爲已經訪問,然後去訪問B,將B標記爲訪問完成,然後找到一個與B有邊的頂點C,在去訪問C,然後標記已經訪問完成。之後發現C已經沒有可以訪問的頂點了,那麼就回退到B,繼續找與B相連,且沒有訪問的頂點,最後回退到A,在尋找,最後直到找不到合適的頂點,結束遍歷。
舉個例子來說,如上圖所示的一個圖,首先從0開始遍歷。將0標記爲已經訪問,然後找到與它相連的頂點1.
然後將1標記爲已經訪問,找到與它相連的2,
在2處,把2標記爲已經訪問,然後繼續搜尋,發現與它相連的1已經被訪問了,所以繼續尋找,找到了3
在3點,標記爲已訪問,然後發現與之相連的頂點0,1,2都訪問了,只剩下4了,所以去頂點4.
到達4點的時候,標記爲已經訪問,然後發現與之相連的3點訪問過了。沒有其他的邊了,那麼就回退到剛纔訪問4頂點之前的3頂點,看看還有沒有與之相連的頂點沒有訪問,發現3也沒有了,那就在回退到訪問3之前的節點2,發現2有節點5沒有訪問,然後訪問,與4一樣,沒有其他頂點可以訪問,那麼就回退到2,此時2的頂點也都被訪問過了,在回退到訪問2之前的1,最後回退到0,發現都沒有空餘節點訪問了,所以完成了深度優先遍歷。這個過程是不是與之前學過的一種數據結構很像,先進後出,沒錯就是棧,函數實現棧,就是遞歸,所以圖的深度優先遍歷,就是不斷遞歸的過程。
代碼如下:
void DFS(Graph graph,int *visit,int k)//從k頂點開始遍歷,visit爲訪問標記,訪問
//過了就置1
{
int i;
visit[k]=1;
printf(" %d",k);
for(i=0;i<graph->Nv;i++)
{
if(visit[i]==0&&graph->G[k][i]==1)
{
DFS(graph,visit,i);
}
}
}
圖的廣度優先遍歷
圖的廣度優先遍歷就比較簡單了,其實就是二叉樹的層次遍歷的思想,需要使用隊列這個數據結構,基本思路是這樣的:首先將第一個頂點入隊,標記爲已經訪問(切記一定是入隊就標記已經訪問,不可以出隊在標記訪問,否則會造成頂點的重複入隊)然後開始遍歷,循環結束的條件是隊列爲空,將隊列首個元素彈出,然後判斷與它相連的頂點是否被訪問過,沒有,則入隊,並且標記已經訪問。重複這個過程,就完成了遍歷。
代碼如下:
void BFS(Graph graph,int *visit,int k)
{
int i,j;
queue q;
q=createQueue();
if(q==NULL)
{
return;
}
printf("{");
add_que(q,k);
visit[k]=1;
while(!isEmpty(q))
{
i=pop_que(q);
printf(" %d",i);
for(j=0;j<graph->Nv;j++)
{
if(visit[j]==0&&graph->G[i][j]==1)
{
add_que(q,j);
visit[j]=1;
}
}
}
printf(" }\n");
destroyQueue(q);
}
圖的練習題
對於圖的基礎定義和圖的遍歷,大家可以通過下面這道題練習一下:
列出連通集
補充
補充:廣度優先遍歷中使用隊列的實現代碼如下:
typedef int Element;
typedef struct Qnode
{
Element data[50];
int rear;
int front;
};
#define True 1
#define False 0
typedef struct Qnode* queue;
queue createQueue();
int isFull(queue que);
int isEmpty(queue que);
void add_que(queue que,Element BT);
Element pop_que(queue que);
void destroyQueue(queue que);
queue createQueue()
{
queue que;
que=(queue)malloc(sizeof(struct Qnode));
if(que==NULL)
{
printf("空間不足");
return NULL;
}
que->rear=0;
que->front=0;
return que;
}
int isFull(queue que)
{
return ((que->rear+1)%50)==(que->front);
}
int isEmpty(queue que)
{
return ((que->rear==que->front));
}
void add_que(queue que,Element BT)
{
if(isFull(que)==True)
{
printf("隊列滿\n");
return;
}
que->rear=(que->rear+1)%50;
que->data[que->rear]=BT;
}
Element pop_que(queue que)
{
if(isEmpty(que)==True)
{
printf("隊列空\n");
return -1;
}
//注意這個出隊,是先front指向的位置加1
que->front=(que->front+1)%50;
return que->data[que->front];
}
void destroyQueue(queue que)
{
free(que);
}