图的理论
有向图
我们可以把图上的任意一个顶点都可以作为入口。
无限图就是边的没有方向的,只有两个节点相连着,是可以来回的。
图的存储-邻接矩阵
图的表示方法之一:邻接矩阵
邻接矩阵都是从1开始算的。
比如说,从顶点1可以到达顶点5,那么1行5列就是1
从顶点2可以到达顶点3,那么2行3列就是1
以此类推
一般用邻接矩阵存储有向图,一般出来的矩阵都是稀疏的矩阵,就是0太多了,有效值1比较少了。所以,邻接矩阵存储有向图是比较浪费空间的,除非顶点之间的边特别多。
节点和节点还可以带有权值,就是带权值就有向图。
我们可以把节点理解为城市,边理解为城市与城市之间的距离。
就像我们刚才说的,顶点1可以到达顶点5,1行5列是1,表示这2个顶点之间是有一条有向边的。从1出发,可以到达5。
如果是带权值的有向图,顶点1到顶点5是20公里,那么1行5列存储的是20
邻接矩阵的另一个好处是:可以存储顶点的入度和出度
出度:从这个顶点出发,可以到达几个顶点,比如说1,可以到达2,4,5,所以1顶点的出度就是3
就是看,一行一行的,一行有几个1,就代表当前这个顶点的出度是多少
顶点1就是在第一行,第一行的2,4,5是1,所以,顶点1的出度是3。以此类推。
入度:有几条边可以到达当前的这个顶点
比如说,顶点4,从顶点1可以到达顶点4,从顶点2可以到达顶点4
所以顶点4的入度是2
我们看的是邻接矩阵的列。顶点4就是看第4列有多少个1,顶点4的入度就是多少。
图的存储-邻接表
左边这列的1,2,3,4,5,6表示有向图的6个顶点
右边对应的表示:比如说顶点1,顶点1可以到达顶点2,顶点4,顶点5
顶点2可以到达顶点3和顶点4
顶点3是没有出度的,以此类推
相比较于邻接矩阵,邻接表不用存储那么多无效的0值,内存空间的利用率是比较高的,但是邻接表的不好地方是只描述了顶点的出度,没有描述顶点的入度。
逆邻接表:存储顶点的入度,但是不存储顶点的出度
有没有一种数据结构,可以既存储顶点的入度,又可以存储顶点的出度?
十字链表
图的遍历
我们看成图或者三叉树都可以
图的深度优先遍历:
从A开始,一直向左遍历,直到为空,
然后回退到E,E没有右孩子,回退到B,B有右孩子,访问B的右孩子F,F-H,H后为空,然后回退到F,回退到B,回退到A,A下面有C,C下面没有,回退到C,回退到A,访问D,访问G
广度优先遍历:
相当于二叉树的层序遍历。
一层一层的来,层层向外扩张
我们再举一个例子:
从A开始进行深度优先遍历:
假设A先到达B,
A-B-F-I-H
H不能到达其他顶点了,回溯。
回溯到I,I也没有其他的路径,回溯到F,F可以访问G
F-G
然后G再进行深度,可以到B,但是B访问过了,所以就到C
G-C
C-D
D可以到C,但是C遍历过了,D可以到H,但是H遍历过了,所以D回溯
回溯到C,回溯到G,回溯到F,回溯到B,回溯到A,然后A可以访问E,
D-E
从A开始广度优先遍历:
先访问A所有相邻的顶点,A-B-D-E
然后看B这个顶点,跟B相邻的有:F
然后看D这个顶点,跟D相邻的有:C-H
然后看E这个顶点,跟E相邻的有:H,但是H被访问过了
然后处理F这个顶点,跟F相邻的有:G-I
然后处理C这个顶点:跟C相邻的有:B之前访问过了,D之前访问过了,
然后处理H这个顶点,跟H相邻的有:没有
然后处理G这个顶点,跟G相邻的有:C之前访问过了,B之前访问过了
然后处理I这个顶点,跟I相邻的有:H之前访问过了
广度优先遍历结束
有向图创建邻接表的代码
西安(3,5)表示西安可以到达安徽和福建
这样的有向图相当于是不带权值的。
左边这一列相当于是数组,数组里面的节点类型:地址域和数据域(城市的编号)
/**
* 实现不带权值的有向图,用邻接表组织
*/
public class Digraph
{
private ArrayList<Vertic> adj;//邻接表结构,向量数组
/**
* 邻接表初始化
*/
public Digraph() {//构造函数
this.adj = new ArrayList<>();
}
/**
* 从文件中读取城市定点和边的信息
* @param reader
*/
public void read(BufferedReader reader) throws Exception{
//把adj的第0号位置占用,让顶点编号和adj数组的位置一一对应
adj.add(new Vertic("", null));
String city = null;
String[] vertic = null;
for(;;){
city = reader.readLine();//读一行城市信息
if(city == null){//没找到
break;
}
//读边,读一行,出度,用逗号隔开的
LinkedList<Integer> list = new LinkedList<>();
vertic = reader.readLine().split(",");//,隔开
for (int i = 0; i < vertic.length; i++) {
list.add(Integer.parseInt(vertic[i].trim()));//字符串的前后空格去掉
}
adj.add(new Vertic(city, list));
}
}
/**
* 打印有向图的邻接表结构
*/
public void showAdj(){
for(int i=1; i < adj.size(); ++i){
System.out.print(adj.get(i).data + " : ");
for (Integer integer : adj.get(i).adjList) {
System.out.print(integer + " ");
}
System.out.println();
}
}
/**
* 定义有向图的定点类型
*/
static class Vertic{
public Vertic(String data, LinkedList<Integer> adjList) {
this.data = data;
this.adjList = adjList;
}
String data;//表示邻接表数组的数据(城市名称)
LinkedList<Integer> adjList;//描述定点出度的链表结构
}
深度优先和广度优先遍历邻接表的代码
/**
* 从指定的start顶点开始,深度遍历有向图
* @param start
*/
public void dfs(int start) {
System.out.println("深度优先遍历:");
boolean[] visited = new boolean[adj.size()];//判断顶点是否已经被遍历过了
dfs(start, visited);
}
private void dfs(int start, boolean[] visited) {
//表示当前顶点已经被访问过
if (visited[start]) {
return;
}
//访问当前顶点
System.out.print(adj.get(start).data + " ");
visited[start] = true;//已经访问过了
//深度遍历,访问其它顶点信息
for (int i = 0; i < adj.get(start).adjList.size(); ++i) {//访问当前顶点的后面的链表的第一个节点
dfs(adj.get(start).adjList.get(i), visited);
}
}
/**
* 从指定的start顶点开始,进行广度优先遍历
* @param start
*/
public void bfs(int start) {
System.out.println("广度优先遍历:");
boolean[] visited = new boolean[adj.size()];
LinkedList<Vertic> queue = new LinkedList<>();
//把入口顶点信息入队列
queue.offer(adj.get(start));
visited[start] = true;
while (!queue.isEmpty()) {
Vertic front = queue.poll();//取出队头元素
System.out.print(front.data + " ");
LinkedList<Integer> list = front.adjList;
for (int i = 0; i < list.size(); i++) {
//表示front.adjList.get(i)顶点没有被访问过
if (!visited[list.get(i)]) {
queue.offer(adj.get(list.get(i)));//入队
visited[list.get(i)] = true;//访问过了
}
}
}
}
不带权值有向图最短路径代码
/**
* 找start到end的最短路径信息
* @param start
* @param end
*/
public void shortestPath(int start, int end) {
int[] path = new int[adj.size()];//定义和节点一样多的数组,记录路径
boolean[] visited = new boolean[adj.size()];//是否已经访问过了
LinkedList<Integer> queue = new LinkedList<>();//队列
boolean flag = false;
//把入口顶点信息入队列
queue.offer(start);
visited[start] = true;//现在访问过了!
while (!queue.isEmpty()) {
int front = queue.poll();//出队
if (front == end) {
flag = true;
break;
}
LinkedList<Integer> list = adj.get(front).adjList;//拿到顶点的链表信息
for (int i = 0; i < list.size(); i++) {
//表示front.adjList.get(i)顶点没有被访问过
if (!visited[list.get(i)]) {
queue.offer(list.get(i));//放编号
//记录当前顶点的前驱顶点信息
path[list.get(i)] = front;
visited[list.get(i)] = true;//可以通行
}
}
}
if (!flag) {
System.out.println(adj.get(start).data + " => "
+ adj.get(end).data + " 不存在路径!");
return;
}
printShortestPath(start, end, path);
}
/**
* 打印start到end的最短的路径信息
* @param start
* @param end
* @param path
*/
private void printShortestPath(int start, int end, int[] path) {
if (start == end) {
System.out.print(adj.get(end).data + " -> ");
return;
}
printShortestPath(start, path[end], path);//先递归,后打印
System.out.print(adj.get(end).data + " -> ");
}
C++实现
#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <queue>
using namespace std;
#if 0
//实现一个有向图的邻接表结构
class Digraph
{
public:
//从配置文件读入顶点和边的信息,生成邻接表
void readFile(string filePath)
{
FILE* pf = fopen(filePath.c_str(), "r");
if (pf == nullptr)
{
throw filePath + " not exists!";
}
//占用第0号位置
vertics.emplace_back("");
while (!feof(pf))
{
char line[1024] = { 0 };
fgets(line, 1024, pf);
//增加一个节点信息
string linestr(line);
vertics.emplace_back(linestr.substr(0, linestr.size()-1));
fgets(line, 1024, pf);
char* vertic_no = strtok(line, ",");
while (vertic_no != nullptr)
{
int vex = atoi(vertic_no);
if (vex > 0)
{
vertics.back().adjList_.emplace_back(vex);
}
vertic_no = strtok(nullptr, ",");
}
}
fclose(pf);
}
//输出邻接表信息
void show() const
{
for (int i = 1; i < vertics.size(); i++)
{
cout << vertics[i].data_ << " : ";
for (auto no : vertics[i].adjList_)
{
cout << no << " ";
}
cout << endl;
}
cout << endl;
}
//图的深度优先遍历
void dfs()
{
vector<bool> visited(vertics.size(), false);
dfs(1, visited);
cout << endl;
}
//广度优先遍历
void bfs()
{
vector<bool> visited(vertics.size(), false);
queue<int> que;
que.push(1);
visited[1] = true;
while (!que.empty())
{
int cur_no = que.front();
que.pop();
cout << vertics[cur_no].data_ << " ";
for (auto no : vertics[cur_no].adjList_)
{
if (!visited[no])
{
que.push(no);
visited[no] = true;
}
}
}
cout << endl;
}
//求不带权值的最短路径问题 - 广度优先遍历
void shortPath(int start, int end)
{
vector<bool> visited(vertics.size(), false);
queue<int> que;
//记录顶点在遍历过程中的前后遍历关系
vector<int> path(vertics.size(), 0);
que.push(start);
visited[start] = true;
while (!que.empty())
{
int cur_no = que.front();
if (cur_no == end)
{
//找到end末尾节点
break;
}
que.pop();
//cout << vertics[cur_no].data_ << " ";
for (auto no : vertics[cur_no].adjList_)
{
if (!visited[no])
{
que.push(no);
visited[no] = true;
//当前节点处,记录是从哪一个节点过来的
path[no] = cur_no;
}
}
}
if (!que.empty())
{
//存在一条最短路径,怎么输出?
/*while (end != 0)
{
cout << vertics[end].data_ << " <= ";
end = path[end];
}*/
showPath(end, path);
}
else
{
cout << "不存在有效的最短路径!" << endl;
}
cout << endl;
}
private:
//深度优先遍历的递归接口
void dfs(int start, vector<bool>& visited)
{
//该start顶点已经遍历过了
if (visited[start])
{
return;
}
cout << vertics[start].data_ << " ";
visited[start] = true;
//递归遍历下一层节点
for (auto no : vertics[start].adjList_)
{
dfs(no, visited);
}
}
//输出最短路径信息
void showPath(int end, vector<int>& path)
{
if (end == 0) // 已经回溯到起始节点了
return;
showPath(path[end], path);
cout << vertics[end].data_ << " ";
}
private:
//顶点类型
struct Vertic
{
Vertic(string data)
: data_(data)
{}
string data_;//存储顶点的信息
list<int> adjList_;//邻接链表结构
};
private:
vector<Vertic> vertics;//邻接表结构
};
int main()
{
Digraph graph;
graph.readFile("data.txt");
graph.show();
graph.dfs();
graph.bfs();
cout << "================" << endl;
graph.shortPath(1, 3);
return 0;
}
#endif