链表、树与图的关系:
链表是特殊化的树,树是特殊化的图。
一、图的存储
图的存储有三种方式: 邻接矩阵、出边数组 和 邻接表。
上图中三种表示方式的介绍如下:
1.邻接矩阵:
矩阵中的值为1, 代表 存在 行坐标 到 列坐标 的边。
如果有权重值的话,可以将1设置成相应的权重值。
这种表示方法的缺点是边数相对顶点较少的图,存在对存储空间的浪费。
2.出边数组:
数组[1,2] [1,3] 分别代表这存在 结点1 到 结点2 的边,结点1 到 结点3 的边。一个图就使用多个数组表示完成了。
如果有权重值的话,可以使用一个Node类来表示,Node中有两个属性,一个是表示结点,另一个值表示权重值。(比如:Node{ int nextnode, int weight} ),使用Node对象的数组来表示带权重的图。
出边数组是最方便使用的表示方式,也是最常用的。
3.邻接表:
使用数组和链表相结合的存储方式,构造一个数据类型,存放结点的 值 和 下一个结点的地址,这种表示方法也是常用的图的表示方法。数据类型如下:
struct Node
{
int tou;
Node* next;
};
Node* head[MAX_N];
head数组存放,所有的邻接表的头结点地址。对于每个结点的连接点,不断地在头结点后面进行连接。
二、图的遍历
深度优先遍历 划分连通块
广度优先遍历 拓扑排序
图的深度优先遍历如图:
使用一个bool类型的visited数组,对结点是否访问做标记,防止重复访问。
示意图是一种可能的遍历方式,在走到第5步的时候,会从结点B 回溯到 结点E ,再回溯到结点G。因为有一个visited数组来标记是否访问,访问过的就不再重复访问。
图的广度优先遍历如图:
广度优先遍历就是层序遍历,需要额外使用一个 队列 进行遍历的操作。
三、代码示例:
以下两个题目,分别是无向图找环 和 有向图找环 的类型。
1. DFS 深度优先遍历(无向图可以看做 两个双向边的有向图)
Leetcode 684题
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。
示例1:
输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
1
/ \
2 - 3
示例2:
输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
| |
4 - 3
代码如下:
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
//出现过的最大的点就是n
for(vector<int>& e:edges)
{
int u=e[0],v = e[1];
n = max(n,u);
n = max(n,v);
}
//模板:出边数组初始化
edge = vector<vector<int>>(n+1,vector<int>());
visit = vector<bool>(n+1,false);
hasCycle = false;
//加边
for(vector<int>& e:edges)
{
int u = e[0],v = e[1];
//无向图看作双向边的有向图
addEdge(u,v);
addEdge(v,u);
dfs(u,0);
if(hasCycle)
return e;
}
return {};
}
private:
//图的深度优先遍历
//visit数组,避免重复访问
//fa是第一次走到x的点,比如示例1中的1 --> 2
void dfs(int x,int fa)
{
//第一步:标记已访问
visit[x] = true;
//第二步:遍历所有出边
for(int y:edge[x])
{
if(y==fa) continue; //返回父亲,不是环;避免s->a,a->s的情况
if(!visit[y]) dfs(y,x);
else hasCycle = true;
}
visit[x]=false;
}
//先完成x,再完成y,就形成了x->y的有向边
void addEdge(int x, int y )
{
edge[x].push_back(y);
}
int n;
vector<vector<int>> edge;
vector<bool> visit;
bool hasCycle;
};
2.BFS广度优先遍历
Leetcode 207题
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
代码如下:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
n = numCourses;
//出边数组初始化
edge = vector<vector<int>>(n,vector<int>());
inDeg = vector<int>(n,0);
for(vector<int>& pre : prerequisites)
{
int ai = pre[0];
int bi = pre[1];
//加边模板
//因为学bi前要先学ai,就形成了ai到bi的边
addEdge(bi,ai);
}
//拓扑排序
//学过的课程数等于n,则说明能完成所有课程的学习
return topsort() == n;
}
private:
//假如x的连接的边是y,则出边数组的[x,y]
void addEdge(int x, int y)
{
edge[x].push_back(y);
//x-->y,则y的入度是1
inDeg[y]++;
}
//有向图找环 模板 (拓扑排序)
//返回学的课程数
//拓扑排序
int topsort()
{
int learned = 0;
//拓扑排序基于BFS,需要队列
queue<int> q;
//从所有零入度点出发
for(int i=0 ; i<n ;i++)
if(inDeg[i] == 0) q.push(i);
//执行BFS
while(!q.empty())
{
int x = q.front();//取队头,这门课学过了
q.pop();
learned++;
//考察x的所有出边(去掉约束关系)
for(int y : edge[x])
{
inDeg[y]--; //去掉约束关系
if(inDeg[y] == 0)
{
q.push(y);
}
}
}
//cout<<"learned:"<<learned<<endl;
return learned;
}
int n;
vector<vector<int>> edge;
vector<int> inDeg; //入度
};
四、DFS与BFS对比
DFS更适合搜索树形状态空间:
递归本身就会产生树的结构
可以用一个全局变量维护状态中较为复杂的信息(例如:子集方案,排列方案)
不需要队列,节省空间
BFS适合求“最小代价”,“最小步数”的题目
BFS是按层次序搜索,第K步搜完才会搜K+1步,在任意时刻队列中至多只有两层