642-图的理论和代码实现

本文详细介绍了有向图的概念,包括无限图、邻接矩阵和邻接表的存储方式,以及如何利用邻接矩阵计算顶点的入度和出度。针对邻接矩阵的缺点,提出了邻接表和逆邻接表作为优化方案。同时,文章探讨了深度优先遍历(DFS)和广度优先遍历(BFS)的算法,并提供了Java和C++的代码实现。此外,还讲解了不带权值的有向图中最短路径的查找方法。
摘要由CSDN通过智能技术生成

图的理论

在这里插入图片描述

有向图

在这里插入图片描述
我们可以把图上的任意一个顶点都可以作为入口。
无限图就是边的没有方向的,只有两个节点相连着,是可以来回的。

图的存储-邻接矩阵

在这里插入图片描述

图的表示方法之一:邻接矩阵
在这里插入图片描述
邻接矩阵都是从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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值