0
点赞
收藏
分享

微信扫一扫

数据结构与算法之美(十二)图、深度优先搜索、广度优先搜索

半夜放水 2022-03-26 阅读 57

目录

图(Graph)

(Graph):一种比树更复杂的非线性表结构
顶点(vertex):图中的元素
(edge):顶点之间的连接关系
无向图:边没有方向的图
(degree):跟顶点相连的边的条数
有向图:边有方向的图
入度(In-degree):在有向图中,有多少条边指向这个顶点,例如微博用户的粉丝数
出度(Out-degree):在有向图中,有多少条边以这个顶点为起点指向其他顶点,例如微博用户的关注数
带权图(weighted graph):每条边都有一个权重的图
边权重(weight):带权图中边的权重值,例如QQ好友间的亲密度
连通图:图中的所有顶点都是连通的,此时边数E>>顶点数V

图的存储

1.邻接矩阵(Adjacency Matrix)

邻接矩阵是一个二维数组,

  • 对于无向图,如果顶点i和顶点j之间有边,那么A[i][j]=1;
  • 对于有向图,如果顶点i到顶点j之间有边,那么A[i][j]=1;
  • 对于带权图,如果顶点i和顶点j之间有边,边权重为w,那么A[i][j]=w。

优点:

  • 简单直观
  • 获取顶点关系高效
  • 计算方便,可以将很多图运算转换成矩阵之间的运算

缺点:

  • 对于无向图、稀疏图来说,浪费存储空间

代码:

// 以邻接矩阵存储的图
class Graph
{
private:
	int vertexNum; // 顶点个数
	int edgeNum; // 边个数
	vector<vector<int>> adjMatrix; //邻接矩阵
public:
	Graph(vector<int> nums); // 构造函数
	~Graph() {}; // 析构函数
	void addEdge(int s, int t); // 无向图添加顶点s到顶点t的一条边,
   	void DFS(int s, int t);// 深度优先遍历图,搜索结点s到结点t的一条路径
   	void BFS(int s, int t);// 广度优先遍历图,搜索结点s到结点t的一条路径
};

Graph::Graph(vector<int> nums)
{
	int n = nums.size();
	vertexNum = n;
	adjMatrix.resize(n); // n行
	for(int i = 0; i < n; ++i)
	{
		adjMatrix[i].resize(n); //n列
	}
}

void addEdge(int s, int t)
{
	adjMatrix[s][t] = 1;
	adkMatrix[t][s] = 1; //无向图
	++edgeNum;
}

2. 邻接表(Adjacency List)

邻接表类似散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点,

  • 对于无向图,相连接指的是有边相连;
  • 对于有向图,相连接指的是这个指向其他节点的边(逆邻接表是存的指向这个节点的边)。

优点:

  • 存储节省空间(如果仍存不进内存的话,可以对顶点哈希分片)

缺点:

  • 查询两个顶点之间关系时低效 -> 可以用平衡二叉查找树、跳表、散列表、有序动态数组等替换链表,防止链过长,以实现更为高效的查找

代码:

// 邻接表里链表的节点
struct ListNode
{
	int val; // 第val个顶点
	ListNode *next;
};
// 邻接表里的一个元素
struct VertexNode
{
	int vertex; // 顶点值
	ListNode *firstedge; // 边
};

// 以邻接表存储的图
class Graph
{
private:
	int vertexNum = 0; // 顶点的个数
	int edgeNum = 0; // 边的个数
	vector<VertexNode> adjList; // 邻接表
public:
   	Graph(vector<int> nums);// 构造函数建立图,顶点的值在nums里
   	~Graph() {}; // 析构函数
   	void addEdge(int s, int t); // 无向图添加顶点s到顶点t的一条边
   	void DFS(int s, int t);// 深度优先遍历图,搜索结点s到结点t的一条路径
   	void BFS(int s, int t);// 广度优先遍历图,搜索结点s到结点t的一条路径
};

// 构造函数
Graph::Graph(vector<int> nums)
{
	int n = nums.size();
	vertexNum = n;
	for(int i = 0; i < n; ++i)
	{
		adjList[i].vertex = nums[i]; // 指针用->,对象用.
		adjList[i].firstedge = NULL;
	}
}
// 添加顶点s到顶点t的一条边
void Graph::addEdge(int s, int t)
{
	ListNode *node = new ListNode; // 创建一个Node,这里是node对象的指针
	// 将这个Node插入到adjlist[s]这个链表里
	node->val = t; // 目标节点是t
	node->next = adjList[s].firstedge;
	adjList[s].firstedge = node;
	// 边数+1
	++edgeNum;
}

图的搜索算法

搜索算法是指:从图中找出从一个顶点出发到另一个顶点的路径。

  • 暴力搜索算法(适合图不大的搜索):广度优先、深度优先
  • 启发式搜索算法:A*、IDA*等

广度优先搜索(BFS)

广度优先搜索(Breadth-First-Search),简称BFS,先找离其实顶点最近的、然后是次近的、依次往外搜索。

BFS可以找到顶点s和顶点t之间的最短路径。

BFS通过队列来实现,假设顶点数是V、边数是E,

  • 时间复杂度:普通图的最坏时间复杂度是 O ( V + E ) O(V+E) O(V+E),连通图(E>>V)的时间复杂度是 O ( E ) O(E) O(E)
  • 空间复杂度: O ( V ) O(V) O(V),主要消耗再visited数组、queue队列、prev数组,这三个数组的存储空间大小都不会超过V。

邻接矩阵存储下的BFS

void Graph::BFS(int s, int t)
{
	if(s == t) return; //同一个顶点(s和t不是顶点的值,而是第几个顶点)	
	
	vector<bool> visited(vertexNum, false); // visited记录顶点i是否被访问过
	visited[s] = true;
	
	queue<int> q; // q存储第k层的所有顶点
	q.push(s);
	
	vector<int> prev(vertexNum, -1); // prev记录从顶点s到顶点t的搜索路径,prev[i]存储的是顶点i是从哪个前驱顶点遍历过来的,所以需要递归地打印
	
	// bfs
	while(q.size() != 0)
	{
		int i = q.front();
		q.pop();
		for(int j = 0; j < vertexNum; ++j)
		{
			if(adjMatrix[i][j] == 1 && !visited[j]) //是i的相邻顶点、且没有被访问过
			{
				// 加入到搜索结果路径中
				prev[j] = i;
				// 判断是否到达了顶点t
				if(j == t) // 如果搜到了顶点t,就打印出搜索路径
				{
					print_route(prev, s, t);
					return;
				}
				// 如果还没搜到顶点t,将这层搜到的顶点加到q里,等下层继续搜
				q.push(j);
				// 也要将它的状态设为已被搜过
				visited[j] = true;
			}
		}
	}
}

print_route(vector<int> prev, int s, int t)
{
	// 递归打印顶点s->t的路径
	// 因为prev[i]里存的是哪个顶点到的顶点i,所以s->t的路径 = s->prev[t]的路径 + t
	if(prev[t] != -1 && s != t)
	{
		print_route(prev, s, prev[t]);
	}
	cout << t << " ";
}

邻接表存储下的BFS

void Graph::BFS(int s, int t)
{
	if(s == t) return;
	// visited
	vector<bool> visited(vertexNum, false);
	visited[s] = true;
	// queue
	queue<int> q;
	q.push(s);
	// prev
	vector<int> prev(vertexNum, -1);
	
	// bfs begin
	while(q.size() != 0)
	{
		int i = q.front();
		q.pop();
		ListNode* p = adjList[i].firstedge;
		while(p)
		{
			int j = p->val;
			if(!visited[j])
			{
				prev[j] = i;
				if(j == t)
				{
					print_res(prev, s, t);
					return;
				}
				q.push(j);
				visited[j] = true;
			}
			p = p->next;
		}
	}
}


print_route(vector<int> prev, int s, int t)
{
	// 递归打印顶点s->t的路径
	// 因为prev[i]里存的是哪个顶点到的顶点i,所以s->t的路径 = s->prev[t]的路径 + t
	if(prev[t] != -1 && s != t)
	{
		print_route(prev, s, prev[t]);
	}
	cout << t << " ";

深度优先搜索(DFS)

深度优先搜索(Depth-First-Search),简称DFS,类似走迷宫,一直走到底,不行再返回(回溯)。

DFS得到的不是最短路径。

DFS通过栈或递归来实现,假设顶点数是V、边数是E,

  • 时间复杂度:每条边最多被访问两遍,一次是遍历,一次是回退,所以时间复杂度是 O ( E ) O(E) O(E)
  • 空间复杂度: O ( V ) O(V) O(V),主要消耗再visited数组、递归调用栈、prev数组,这三个数组的存储空间大小都不会超过V。

邻接矩阵存储下的DFS

(1)递归实现

bool found = false; // 全局变量,或者类成员变量。作为递归终止条件,如果为true,表明找到了顶点t,不用再继续递归查找了 (不能直接用s==t来判断return吗)???

void Graph::DFS(int s, int t)
{
	if(s == t) return;
	found = false; // 递归终止条件:找到了顶点t
	vector<bool> visited(vertexNum, false); // 是否访问过该顶点
	visited[s] = true;
	vector<int> prev(vertexNum, -1); // prev[i]表示搜索结果路径中是哪个顶点到顶点i的
	recursiveDFS(s, t, visited, prev);
	print_res(prev, s, t);
}

void recursiveDFS(int s, int t, vector<bool> & visited, vector<int> & prev)
{
	// 递归终止条件
	if(found) return;
	if(s == t)
	{
		found = true;
		return;
	}
	// 子问题
	for(int j = 0; j < vertexNum; ++j)
	{
		if(adjMatrix[s][j] == 1 && !visited[j])
		{
			prev[j] = s;
			visited[j] = true;
			recursiveDFS(j, t, visited, prev);
		}
	}
}

(2)栈实现

// 不确定对不对
void Graph::DFS(int s, int t)
{
	if(s == t) return;
	vector<bool> visited(vertexNum, false);
	visited[s] = true;
	vector<int> prev(vertexNum, -1);
	stack<int> s;
	s.push(s);
	while(!s.empty())
	{
		int i = s.top(); //取栈顶元素
		s.pop();
		// 找它的下一个元素
		for(int j = 0; j <= vertexNum; ++j)
		{
			if(adjMatrix[i][j] == 1 && !visited[j])
			{
				visited[j] = true;
				if(j == t)
				{
					print_res(prev, s, t);
					return;
				}
				s.push(j);
			}
		}
	}
}

邻接表存储下的DFS

(1)递归实现

bool found = false; // 全局变量,或者类成员变量。作为递归终止条件,如果为true,表明找到了顶点t,不用再继续递归查找了 (不能直接用s==t来判断return吗)???

void Graph::DFS(int s, int t)
{
	found = false; // 递归终止条件:找到了顶点t
	vector<bool> visited(vertexNum, false); // 是否访问过该顶点
	visited[s] = true;
	vector<int> prev(vertexNum, -1); // prev[i]表示搜索结果路径中是哪个顶点到顶点i的
	recursiveDFS(s, t, visited, prev);
	print_res(prev, s, t);
}

void recursiveDFS(int s, int t, vector<bool> & visited, vector<int> & prev)
{
	// 递归终止条件
	if(found) return;
	if(s == t)
	{
		found = true;
		return;
	}
	// 子问题
	ListNode * p = adjList[s].firstedge;
	while(p)
	{
		int j = p->val;
		if(!visited[j])
		{
			prev[j] = s;
			visited[j] = true;
			recursiveDFS(j, t, visited, prev);
		}
		p = p->next;
	}
}

(2)栈实现

// 不确定对不对
void Graph::DFS(int s, int t)
{
	if(s == t) return;
	vector<bool> visited(vertexNum, false);
	visited[s] = true;
	vector<int> prev(vertexNum, -1);
	stack<int> s;
	s.push(s);
	while(!s.empty())
	{
		int i = s.top(); //取栈顶元素
		s.pop();
		// 找它的下一个元素
		ListNode * p = adjList[i];
		while(p)
		{
			int j = p->val;
			if(!visited[j])
			{
				visited[j] = true;
				if(j == t)
				{
					print_res(prev, s, t);
					return;
				}
				s.push(j);
			}
			p = p->next;
		}
	}
}

A*

IDA*

问题:求三度好友

方法一:广度优先
广度优先遍历到第3层即可,以邻接表存储的图为例:

void Graph::BFS(int s, int k)
{
	// g: 图
	// s: 起始顶点
	// k: k度好友
	if(k > vertexNum) return;
	
	// visited
	vector<bool> visited(vertexNum, false);
	visited[s] = true;
	// queue
	queue<int> q;
	q.push(s);
	// cur_k: 当前度数
	int cur_k = 0;
	
	// bfs begin
	while(!q.empty())
	{
		int i = q.front();
		q.pop();
		ListNode* p = adjList[i].firstedge;
		++cur_k;
		while(p)
		{
			int j = p->val;
			if(!visited[j])
			{
				prev[j] = i;
				q.push(j);
				visited[j] = true;
				if(cur_k == k) cout << j << ' ';
			}
			p = p->next;
		}
	}
}

方法二:深度优先

  • 对于单向路径可行:DFS递归时传多一个离初始节点的距离值,访问节点时,递归层数超过3的不再继续递归;
  • 对于多连通图不可行:在深度递归时可能会先往距离远的节点搜索再往距离近的节点搜索,这样出来的搜索结果就是错的。

附录

代码实现参考:

  • https://www.cnblogs.com/yellowgg/p/7875459.html
  • https://blog.csdn.net/mdjxy63/article/details/80397944
  • http://t.zoukankan.com/liugl7-p-11489184.html
举报

相关推荐

0 条评论