一、图的遍历
-
- 采用DFS的遍历去做,分为有向无环图和有环图
- 无环图则不需要visit数组进行辅助
void traverse(int[][] graph,int s){
//关于s的代码,是要干啥 查找所有可能的路径
for(int v:graph[s]){
traverse(graph,v);
}
//处理s,将s进行撤销
}
- 有环图 (需要visited布尔数组,来存储是否访问过)
void traverse(int[][] graph,int s){
if(visted[s]) return;
visted[s] = true;
for(int v:graph[s]){
traverse(graph,v);
}
}
- 有环图处理路径相关的 (增加一个OnPath布尔数组,存储当前进去的路径)
void traverse(int[][] graph,int s){
if(visted[s]) return;
visted[s] = true;
OnPath[s] = true;
for(int v:graph[s]){
traverse(graph,v);
}
OnPath[s] = false;
}
- 2.采用BFS去做,一般是最短路径的问题
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构
Set<Node> visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录扩散的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
/* 划重点:这里判断是否到达终点 */
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
二、图的建立
一般在题目中给的是二维数组,代表着节点的关系,需要将题目的输入转为图的结构,转为邻接表进行存储
- 邻接表的结构是一个链表类型的数组
List<Integer>[] list=new LinkerList[];
(有向图的建立)
List<Integer>[] buildGraph(int numCourse,int[][] prere){
//numcourse是二维数组中的课程(节点)数量
List<Integer>[] graph = new LinkedList[numCourse];
//给对应的每一个链表数组中的节点创建一个链表
for(int i = 0;i < numCourse; i++){
graph[i] = new LinkedList<>();
}
//将链表进行连接起来
for(int[] edge:prere){
//要学习edge[0],需要先学习edge[1]
int from = edge[1],to = edge[0];
graph[from].add(to);
// graph[to].add(from); (无向图的建立)
}
return graph;
}
注意:
-
树也可以看成图。实际上,树是一类特殊的图,树中一定不存在环。但图不一样,图中可能包含环。
-
当沿着图中的边搜索一个图时,一定要确保程序不会因为沿着环的边不断在环中搜索而陷入死循环。
-
避免死循环的办法是记录已经搜索过的节点,在访问一个节点之前先判断该节点之前是否已经访问过,如果之前访问过那么这次就略过不再重复访问。
-
遍历图的起点,一般若是联通的图,则从0开始,若不是,则需要遍历所有的节点进行开始
三、拓扑排序
- 将DFS在遍历的后序位置进行翻转,就是拓扑排序的结构,无环才可以进行拓扑排序
void traverse(TreeNode root) {
// 前序遍历代码位置
traverse(root.left)
// 中序遍历代码位置
traverse(root.right)
// 后序遍历代码位置
}
//只有当左右节点遍历结束才访问根节点,这里的问题也是如此
四、图的扩展-二分图
实际的应用:比如电影和演员,加入使用map集合去存储,可以快速通过演员是到电影,而反向的话则需要重新制作一个map表,这时我们可以创建一个二分图,实现多对一及多对多的关系
- 判断二分图
说白了就是遍历一遍图,一边遍历一边染色,看看能不能用两种颜色给所有节点染色,且相邻节点的颜色都不相同。
不涉及到路径的问题,只需要一个visted数组来防止一直循环
void traverse(int[][] graph(List<Integer>[] graph),int s){
if(visted[s]) return;
visted[s] = true;
for(int v:graph[s]){
tarverse(graph,v);
}
}
也可以将if进行换位置
void traverse(List<Integer>[] graph,int s){
visted[s] = true;
for(int v:graph[s]){
if(!visted[v]){
traverse(graph,v);
}
}
}
//遍历的时候保证没有没访问过
- 二分图的代码
/* 图遍历框架 */
void traverse(Graph graph, boolean[] visited, int v) {
visited[v] = true;
// 遍历节点 v 的所有相邻节点 neighbor
for (int neighbor : graph.neighbors(v)) {
if (!visited[neighbor]) {
// 相邻节点 neighbor 没有被访问过
// 那么应该给节点 neighbor 涂上和节点 v 不同的颜色
traverse(graph, visited, neighbor);
} else {
// 相邻节点 neighbor 已经被访问过
// 那么应该比较节点 neighbor 和节点 v 的颜色
// 若相同,则此图不是二分图
}
}
}
//对应的代码
class Solution {
boolean[] visted;
boolean[] color;
boolean erfen=true;
public boolean isBipartite(int[][] graph) {
int n = graph.length;
visted=new boolean[n];
color=new boolean[n]; //存放对应节点的值
for(int i=0;i<n;i++){
traverse(graph,i);
}
return erfen;
}
void traverse(int[][] graph,int s){
if(!erfen) return;
visted[s]=true;
for(int v:graph[s]){
//如果没有访问过,则染不一样的颜色
if(!visted[v]){
color[v]=!color[s]; //与之不同
traverse(graph,v);
}else{
//访问过,则判断颜色是否是相等的,相等则返回false
if(color[v] == color[s]){
erfen = false; //注意不是返回false
}
}
}
}
}
四、并查集 (高效处理图的连通问题)
社交网络中朋友圈的计算,我关注了一个人,咱俩认识,也关注一个人,合并指向同一个根。如果某两个节点被连通,则让其中的(任意)一个节点的根节点接到另一个节点的根节点上:
class UF {
/* 将 p 和 q 连接 */
public void union(int p, int q);
/* 判断 p 和 q 是否连通 */
public boolean connected(int p, int q);
/* 返回图中有多少个连通分量 */
public int count();
}
假设有十个不相连的图,则连通分量为为10,没两个相连,连通分量--
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
// 将两棵树合并为一棵
parent[rootP] = rootQ;
// parent[rootQ] = rootP 也一样
count--; // 两个分量合二为一
}
/* 返回某个节点 x 的根节点 */ find的函数的意思
private int find(int x) {
// 根节点的 parent[x] == x
while (parent[x] != x)
x = parent[x];
return x;
}
//find函数的修改
int find(int x){
if(parent[x]==x){
return x;
}
return find(parent[x]); //递归查找。返回上一层的根节点
}
/* 返回当前的连通分量个数 */
public int count() {
return count;
}
这样,如果节点 p 和 q 连通的话,它们一定拥有相同的根节点:
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
- 进行合并总的模板:
class UF {
// 连通分量个数
private int count;
// 存储每个节点的父节点
private int[] parent;
// n 为图中节点的个数
public UF(int n) {
this.count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
// 将节点 p 和节点 q 连通
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
parent[rootQ] = rootP;
// 两个连通分量合并成一个连通分量
count--;
}
// 判断节点 p 和节点 q 是否连通
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); //理解递归的含义
}
return parent[x];
}
// 返回图中的连通分量个数
public int count() {
return count;
}
}
1、用 parent 数组记录每个节点的父节点,相当于指向父节点的指针,所以 parent 数组内实际存储着一个森林(若干棵多叉树)。 难点
2、在 find 函数中进行路径压缩,保证任意树的高度保持在常数,使得各个 API 时间复杂度为 O(1)。使用了路径压缩之后,可以不使用 size 数组的平衡优化。
总的思路构建并查集:并编写union接口
-
首先创建一个parent数组(大的根的节点的指针是一直保持不变的,parent[x]=x指向自己),相当每一个对应的节点,方便后序进行合并,初始化连通分量为n
-
合并并查集的时候,首先的找到当前所在的节点的根节点,find函数,使用递归进行优化查找路径
-
合并并查集,分别找到对应的根,根相同,则直接返回,根不同,则将parent[rootQ] = rootP;,进行合并,连通分量--
-
可以通过uf.count()函数查看合并之后的连通分量
例题:返回无向图中连通分量的个数
并查集的难点:理解parent数组的作用,parent[2]
数组的定义 int[] parent;
int [ ] arr = new int [ ] { 64 , 12 , 43 ,34,88} ; 数组的赋值\
五、最小生成树
什么是最小生成树?
就是在一个在图中找一颗包含图中所有节点的树。专业点就是生成树是含有所有节点的无环连通子图。最小生成树就是在众多的子树中找到权重最小的生成树
注意:一般是在无向加权图中计算最小的生成树
例题1:以图来判断树,有n个节点,和一个二元组来表示节点的之间的关系,判断这些边是否能组成一个树?
//组成一个树的关键是:无环,即两个节点之间已经连通了,再加入其他的边就不会成树
代码如下:
boolean validTree(int n, int[][] edges){
Uf uf = new UF(n);
//遍历所有的边
for(int[] edge:edges){
int u=edge[0];
int v=edge[1];
//connected id boolean类型的函数
if(uf.connected(u,v)){
return false; //如果连通,则不是树
}
//将两个合并,可以是树的一部分
uf.union(u,v);
}
//需要保证最后只形成了一棵树,即只有一个连通分量
return uf.count()==1;
}
//总结:无向图找环,可以使用并查集,不断的进行组合,最后检查连通分量为1即可
例题2、最小生成树(Kruskal 261、1135、1584)
- 包含所有的节点
- 不包含环
- 权重最小
并查集可以实现前两个,第三个的需要使用贪心算法
算法思想:将权重从小到大进行排序,每次选择权重最小的时候,判断是否已经连通,若连通则舍弃,若不连通,则加入mst集合,是最小生成树的一部分
//最低的成本连通所有的城市
//其中connections为三元的 conn[0] conn[1] conn[2]为cost
int minimumCost(int n,int[][] conn){
//编号从1-n开始。所以初始化的大小为n+1
UF uf = new UF(n+1); //不理解??
//对所有边的权重从小到大排序
Arrays.sort(conn,(a,b)->(a[2]-b[2]));
int mst=0;
for(int[] edge:conn){
int u=edge[0];
int v=edge[1];
int weight=edge[2];
//若这条边产生环,不加入mst
if(uf.connected(u,v)){
continue;
}
//若这条边不会产生环
uf.union(u,v);
mst+=weight;
}
//三元运算符
//以为节点0没有被使用,所以节点0会占用一个连通分量
return uf.count()==2 ? mst:-1;
}
//扩展,上边的都涉及到一维的,比如conn[A,B,5],代表的是A和B之间的距离为5,假若这里给的是坐标 points[[2,2],[3,6],cost],如何去求?
//分别对应的是点的坐标
points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
//进行转为三元的edge[A,B,cost]
对应的是一个链表类型的数组
List<int[]> edges = new ArrayList<>();\
for (int i = 0; i < n; i++) {\ 代表着数组的行
for (int j = i + 1; j < n; j++) {\ 代表着数组
int xi = points[i][0], yi = points[i][1];\
int xj = points[j][0], yj = points[j][1];\
// 用坐标点在 points 中的索引表示坐标点\
edges.add(new int[] {\
i, j, Math.abs(xi - xj) + Math.abs(yi - yj)\
});\
}\
}
例题3、最小生成树(Prim 261、1135、1584)
Kruskal 算法是在一开始的时候就把所有的边排序,然后从权重最小的边开始挑选属于最小生成树的边,组建最小生成树。
Prim 算法是从一个起点的切分(一组横切边)开始执行类似 BFS 算法的逻辑,借助切分定理和优先级队列动态排序的特性,从这个起点「生长」出一棵最小生成树。
- //假设从起点进行一个一个元素的分析:
- edge[from, to, weight] 对应的邻接表private List<int[]>[] graph;
- 选中第一个节点,首先将所有的边加入优先队列,要和kruskal一样,加入数组,就要加入n个数组,太庞大了,选择优先队列,根据权重从小到大进行排序
- 怎么选择一个节点,并保存所有的边?选择0这个点,使用一个布尔数组进行记录,INMST【0】代表节点0已经在最小的生成树中,将cut方法提出,类似于遍历,遍历一个节点所有相邻的边,此时的判断相邻的点是否已经在生成树的集合中。若不在则加入队列,否则不加入进行跳过
- 判断当前的队列是否为空,类似于BFS,弹出队列,int to = edge[1],int weight=edge[2],判断节点to 是否已经在INMST的集合中,在continue,不在权重++,INMST[to]=true,然后接着执行cut方法,类似于遍历
- 判断最小生成树是否包含图中所有的节点,遍历其大小
总结:prim算法是基于图的邻接表进行实现的,因为加入优先队列进行排序的时候,通过邻接表的性质来得到相邻的边 ,此外,已经加入的边不加入优先队列
补充:优先队列的创建的代码
private PriorityQueue<int[]> pq; //根据权重进行排序
//进行初始化
this.pq=new PriorityQueue<>(a,b)->{
return a[2] - b[2];
}
//数组排序的规则 按照数组中的第三个元素进行排序,从小到大
Arrays.sort(conn,(a,b)->(a[2]-b[2]));
总结:kruscal和prim算法
- 思想都是利用贪心算法,每次找最小的
- kruscal是先将权重进行排好序,在进行一次挑选最小的
- prim算法是每次选择最小的
五,迪杰斯特拉算法 (给一个起点,计算到其他节点的最小的距离)
图中的节点一般就抽象成一个数字(索引),所以才有前面的parent[x] = x,图的具体实现一般是「邻接矩阵」或者「邻接表」。
基础必备:
- 使用邻接表进行表示一个图
// graph[s] 存储节点 s 指向的节点(出度)
List<Integer>[] graph;
- 对于图还需要获得他的相邻节点
List<Integer>[] graph; //graph[s]表示相邻的节点
// 输入节点 s,返回 s 的相邻节点
List<Integer> adj(int s) {
return graph[s];
}
- 对于加权的图,还需要直到两个节点的边的权重是多少?
// 返回节点 from 到节点 to 之间的边的权重
int weight(int from, int to);
这个 `weight` 方法可以根据实际情况而定,因为不同的算法题,题目给的
「权重」含义可能不一样,我们存储权重的方式也不一样
5.1 二叉树的层级遍历
void levelTraverse(TreeNode root){
if(root == null) return 0;
Queue<Treenode> q = new TreeNode<>();
q.offer(root);
int depth=1;
while(!q.isEmpty()){
int size = q.size();
for(int i=0;i<size;i++){
TreeNode cur=q.poll();
printf("节点%s在第%s层",cur,depth);
//将下层的节点放入队列
if(cur.left!=null){
q.offer(cur.left);
}
if(cur.right!=null){
q.offer(cur.right);
}
}
depth++;
}
}
while循环和for循环正是设计的巧妙之处,while向下遍历,for循环层层之间进行遍历
5.2 多叉树的遍历
void levelTraverse(TreeNode root){
if(root==null) return;
Queue<Integer> q=new LinkedList<>();
q.offer(root);
int depth=1;
while(!q.isEmpty()){
int size = q.size();
for(int i=0;i<size;i++){
TreeNode cur=q.poll();
for(TreeNode child:cur.children){
q.offer(child);
}
}
depth++;
}
}
5.3 BFS广度优先搜索
//不同的是需要加一个visit数组进行存储
List<list<Integer>> graph;
boolean[] visted = new boolean[n];
void BFS(Graph graph,int s){
Queue<Integer> q=new LinkedList<>();
q.offer(s);
visted[s];
int step=0;
while(!q.isEmpty()){
int size=q.size();
for(int i=0;i<size;i++){
int v = q.poll();
for(int z: graph(v)){
if(!visted[z]){
q.offer(z);
visted[v];
}
}
}
step++;
}
}
但是:对于加权图的问题,while中的for循环没有多大的意义。
为什么?有了刚才的铺垫,这个不难理解,刚才说 for 循环是干什么用的来着?
是为了让二叉树一层一层往下遍历,让 BFS 算法一步一步向外扩散,因为这个层数 depth,或者这个步数 step,在之前的场景中有用。
但现在我们想解决「加权图」中的最短路径问题,「步数」已经没有参考意义了,「路径的权重之和」才有意义,所以这个 for 循环可以被去掉。
类比:二叉树中的代码去掉for循环 没有for循环,不知道节点在那一层
如果你想同时维护 depth 变量,让每个节点 cur 知道自己在第几层,可以想其他办法,比如新建一个 State 类,记录每个节点所在的层数
class State {
// 记录 node 节点的深度
int depth;
TreeNode node;
State(TreeNode node, int depth) {
this.depth = depth;
this.node = node;
}
}
// 输入一棵二叉树的根节点,遍历这棵二叉树所有节点
void levelTraverse(TreeNode root) {
if (root == null) return 0;
Queue<State> q = new LinkedList<>();
q.offer(new State(root, 1));
// 遍历二叉树的每一个节点
while (!q.isEmpty()) {
State cur = q.poll();
TreeNode cur_node = cur.node;
int cur_depth = cur.depth;
printf("节点 %s 在第 %s 层", cur_node, cur_depth);
// 将子节点放入队列
if (cur_node.left != null) {
q.offer(new State(cur_node.left, cur_depth + 1));
}
if (cur_node.right != null) {
q.offer(new State(cur_node.right, cur_depth + 1));
}
}
}
5.4 迪杰斯特拉算法
prim更新的是未标记集合到已标记集合之间的距离
Dijkstra更新的是源点到未标记集合之间的距离