1.图的定义
图(Graph):是由顶点的又穷非空集合和定点之间的边集组成,通常表示为G(V,E),其中G表示一个图,V是其顶点集合,E是其边集合。
有向图(Directed graphs): 任意两个点之间的边都是有向边。
入度(InDegree):以顶点为v为头的弧的数目称为入度。
出度(OutDegree):以顶点v为尾的弧的数目成为出度。
2.图的存储结构
常见的存储结构有五种:
- 邻接矩阵
- 邻接表
- 十字链表
- 邻接多重表
- 边集数组
邻接矩阵:图的定义G(V,E),顶点可以用一维数组来存储type vex[n]
,边可以理解为线,当然就可以用二维数组
type arc[n][n]
进行存储。对于无向表该矩阵一定是对称矩阵,如果顶点i,j之间存在边那么arc[i][j],arc[j][i]=1
否则arc[i][j]=0
存在的边:V0—-V1,V1—-V2 ,V2—V3 ,V3—V0即:
arc[0][1]=arc[1][0]=1;
arc[1][2]=arc[2][1]=1;
arc[2][3]=arc[3][2]=1;
arc[3][0]=arc[0][3]=1;
其矩阵应该为:
某个顶点i所在行或者所在列元素之和即其度。
对于有向表,矩阵不再是对称的,可以定义列顶点作为起点,行作为终点。那么顶点i所在行的元素和即为出度,列元素和即为入度。
由图可知:
arc[0][3]=1;
arc[1][0]=1;
arc[1][2]=1;
arc[2][0]=1;
arc[2][1]=1;
其矩阵为:
这种存储结构优缺点都十分明显,优点在于结构简单,能分清楚看出顶点的出度和入度,简单判断两个顶点间是否存在边。
但是创建是遍历数组复杂都n^2,动态扩展困难,很多的存储空间的浪费。
邻接表
基于邻接矩阵,顶点依然用一维数据存放,但不仅仅存放顶点数据,还存放一个存储由以该定点为弧头邻接点的一维链表。
这种数据结构下,图就由一个链表数组构成。链表数据结构为:
在例子图中
依然可以轻松看出每一个顶点的出度或者入度(如果链表存放的是以顶点Vi为弧头的邻接点则是出度,为弧尾则是入度)。在有向表的情况下也是一样。只关注一个方向进行记录就好。
虽然解决了邻接矩阵难以动态扩展和空间浪费的问题,但是依然存在一个问题,不能同时反应顶点的出度和入度,链表和数组相比,链表始终需要进行遍历(当然有算法可以减少查找次数),导致效率上始终比不上数组毕,竟数组只是一串连续的存储空间,很轻松就可以获取到目标值。
边集数组
这个办法思路更简单,用一个一维数组(或者链表)来存放所有边的信息,其结构是:
begin end weight(权,如果有用到的话)
这种方法会使得出度入度及顶点信息都不是很好获得所以不常用。
十字链表和多重链接表
克服了邻接表不能同时显示入度和出度的问题,实现比前几个都复杂所以留到后面再写,不然这篇就太长了= =b。
3.图的建立
是时候敲敲代码了,这里的建立方法使用邻接表的方式,本来我想用Python来做图这种数据类型,但是数据结构本来的目的就是接近底层,所以这里还是使用C/C++来实现尽管要麻烦一些 = =。
class Graph
{
public:
typedef struct item
{
int index;
int power;
item*next=NULL;
}NodeList,*Node;
int num;
Graph();
~Graph();
private:
NodeList vexs[20];//最多支持20个顶点
};
Graph::Graph()
{
int temp=0;
int counts = 0;
cout << "输入顶点个数:" << endl;
cin >> num;
for (int i = 1; i < num + 1; i++)
{
Node ptr = &vexs[i-1];
cout << "输入第"<<i<<"个顶点信息" << endl;
counts = 0;
while (1)
{
cout << "输入链接下标" << endl;
cin >> temp;
if (temp == -1)
{
ptr->next = NULL;
cout << "第" << i << "个节点录入完成" << endl;
break;
}
else
{
if (counts > 0)
{
ptr->next = (Node)malloc(sizeof(NodeList));
ptr = ptr->next;
}
ptr->index = temp;
cout << "输入该边的权" << endl;
cin >> temp;
ptr->power = temp;
counts++;
}
}
}
}
Graph::~Graph()
{
}
这个版本很早以前写的,但还是可以用。
这里又存在了一个数组,又可以进行抉择,追求速度可以就是用数组,想更高效的利用存储空间就可以利用链表。即顶点链表中每一个节点都包含着一个边链表,实现方法如下:
首先因为是两个链表,直接整逻辑会显得很复杂,所以这里将它拆分为两个类,各自实现添加操作。
/*******************边集链表*********************/
class Node//边信息
{
public:
char date;
int wight;//权 暂未使用
Node*next;
};
class LinkList
{
public:
Node * head;//头指针
Node * tail;//尾部指针
int lenth=0;//链表长度
LinkList(char *s);//用一个char数组初始化链表
Node* add_one(char s);//返回一个当前指针
void display();
bool del(int index);
};
LinkList::LinkList(char*s)
{
if (s == NULL)
{
cout << "create a empty LinkList!" << endl;
head = NULL;
tail = NULL;
}
else
{
head = (Node*)malloc(sizeof(Node));
Node *ptr = head;
while (*s!='\0')
{
lenth++;
ptr->date = *s++;
ptr->next = (Node*)malloc(sizeof(Node));
ptr = ptr->next;
}
ptr->date = '#';
ptr->next = NULL;
tail = ptr;
}
}
Node* LinkList::add_one(char s)
{
Node *ptr;
Node*curr;
if (lenth == 0)//链表未曾进行过添加
{
head = (Node*)malloc(sizeof(Node));
ptr = head;
ptr->date = s;
ptr->next= (Node*)malloc(sizeof(Node));
ptr = ptr->next;
ptr->date = '#';
ptr->next = NULL;
tail = ptr;
lenth++;
return head;
}
else
{
ptr = tail;
ptr->date = s;
curr = ptr;
ptr->next = (Node*)malloc(sizeof(Node));
ptr = ptr->next;
ptr->date = '#';
ptr->next = NULL;
tail = ptr;
lenth++;
return curr;
}
}
bool LinkList::del(int index)
{
if (index > lenth || index < 0)return false;
Node*ptr=head;
if (index == 0){
head = head->next;
free(ptr);
}
else {
for (int i = 0; i < index - 1; i++)
{
ptr = ptr->next;
}
Node*temp = ptr->next;
ptr->next = ptr->next->next;
free(temp);
}
lenth--;
return true;
}
void LinkList::display()
{
Node*ptr = head;
for (int i = 0; i < lenth; i++)
{
cout << "LinkList[" << i << "]:" << ptr->date<<" ";
ptr = ptr->next;
}
cout << endl;
}
这个链表就用于存放顶点集,其构造方法和display方法是用于测试该表链是否有问题,构造图主要使用的是add_one()方法
自动增加一个节点。
/****************顶点集链表******************/
class vex//顶点信息
{
public:
LinkList *current;
vex *next;
char date;
};
class VexLinkList
{
public:
vex*head;
vex*tail;
int lenth = 0;
VexLinkList(char *s);
vex* add_one(char s);
};
VexLinkList::VexLinkList(char *s)//顶点集
{
if (s == NULL)
{
head = NULL;
tail = NULL;
cout << "create a empty VexLinkList!" << endl;
}
else
{
head = (vex*)malloc(sizeof(vex));
vex*ptr = head;
while (*s!='\0')
{
lenth++;
ptr->date = *s++;
ptr->next = (vex*)malloc(sizeof(vex));
ptr = ptr->next;
}
ptr->date = '#';
ptr->next = NULL;
tail = ptr;
}
}
vex* VexLinkList::add_one(char s)
{
vex*curr;
if (lenth == 0)
{
head = (vex*)malloc(sizeof(vex));
head->date = s;
head->next = (vex*)malloc(sizeof(vex));
tail = head->next;
tail->date = '#';
tail->next = NULL;
lenth++;
return head;
}
else
{
tail->date = s;
curr = tail;
tail->next = (vex*)malloc(sizeof(vex));
tail = tail->next;
tail->date = '#';
tail->next = NULL;
lenth++;
return curr;
}
}
其实链表结构就那样,所以这两个链表其实差不多只是节点类型上有区别;add_one()方法依然自动添加一个节点。
图类型结构
class Graph
{
public:
VexLinkList *t;//顶点集
int sum;//顶点总数
Graph();
};
Graph::Graph()
{
char temp;
t = (VexLinkList*)malloc(sizeof(VexLinkList));
t->lenth = 0;
while (1)
{
cout << "input the vex # means none" << endl;
cin >> temp;
fflush(stdin);
if (temp == '#')break;
vex*cur=t->add_one(temp);
cout << "now input the date in the vex" << endl;
cur->current = (LinkList*)malloc(sizeof(LinkList));
cur->current->lenth = 0;
while (1)
{
cin >> temp;
if (temp == '#')break;
cur->current->add_one(temp);
}
}
}
这里设定了一个默认的终止字符’#’碰到’#’字符就认为当前的顶点集链表或者边集链表输入完毕。
数据结构图:
最后
相对于树的建立,感觉上图的建立还算比较简单,只是相关的概念非常多,遍历(深度,广度)相较二叉树稍显复杂,这里立一个flag,深度遍历,广度遍历,十字链表和多重邻接表下周来写,本来以为写这些应该很快,结果发现。。。好慢啊= =。最后吐槽一下markdown编辑器,真是帮人学习HTML CSS。