图系列(二)图的遍历与拓扑排序

本文深入探讨了图的遍历方法,包括广度优先遍历和深度优先遍历,以及如何使用这两种方法进行拓扑排序。介绍了广度优先遍历使用队列实现,深度优先遍历则可以通过递归或栈来实现,同时还讨论了如何检测图中的环。对于拓扑排序,文章提供了深度优先遍历和广度优先遍历两种实现方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图的遍历与拓扑排序

好了,接下来是重中之重图的遍历。图遍历是很多其他算法的基础,比如Dijkstra算法。

图的遍历

广度优先遍历使用queue,深度优先遍历使用stack。简单来说,两种方式都是将元素从容器中取出,将子节点加入进去,只是“取”的方式不太一样而已。

广度优先遍历

广度优先遍历的关键是需要借助一个队列。代码如下:

class Solution {
    List<Integer>[] adjs;
    public void bfs(int n, int[][] edges) {
        // 初始化邻接链表
        // ...
        Queue<Integer> q = new LinkedList<>();
        q.add(0); // 假设起点从0开始, 通过广度优先遍历可以遍历所有元素。
        
        while(!q.isEmpty()) {
            int i = q.poll();
            for (Integer adj : adjs[i]) {
                q.add(adj);
            }
        }
    }
}

这里做了两个假设: 1. 起点从0开始,就是说0是树的根节点;2. 从0可以遍历到所有元素,并且只有一棵树。相应的,我们需要解除这两个假设:首先,我们可以从特定的点开始,例如入度为0的点;或者,使用一个数组,存储每个点的访问状态,如果元素还没有访问,下一轮继续访问。举个栗子:

class Solution {
    List<Integer>[] adjs;
    boolean[] visited;
    public void bfs(int n, int[][] edges) {
        // 初始化邻接链表
        // ...
        visited = new boolean[n];

        Queue<Integer> q = new LinkedList<>();
        for(int i = 0 ; i < n;i++) {
            if (!visited[i]) {
                q.add(i);
                while (!q.isEmpty()) {
                    int head = q.poll();
                    visited[head] = true;
                    for (Integer adj : adjs[head]) {
                        if (!visited[adj]) {
                            q.add(adj);
                        }
                    }
                }
            }
        }
    }
}

参见leetcode题目: 课程表

深度优先遍历

深度优先遍历,主要利用递归。当然,我们不能够一直避开环的问题。这里介绍《算法与数据结构》中典型的处理方式—— 涂色。没有访问过的元素涂成白色;已经访问过的元素涂成黑色;正在访问的,在同一条递归调用中的元素涂成灰色。代码如下:

class Solution {
    private List<Integer>[] adjs;
    private Color[] colors;
    public boolean containsLoop(int n, int[][] edges) {
        // 初始化
        adjs = new List[n];
        colors = new Color[n];
        for(int i = 0; i < n; i++) {
            adjs[i] = new LinkedList<>();
            colors[i] = Color.WHITE;
        }
        for(int i = 0 ; i<edges.length;i++) {
            int[] edge = edges[i];
            int from = edge[0];
            int to = edge[1];
            adjs[from].add(to);
        }

        // 开始访问
        for (int i = 0; i < n;i++) {
            if (colors[i] == Color.WHITE) {
                if(dfsVisit(i)) return true;
            }
        }
        return false;
    }

    // 判断是否存在环,存在立即返回true
    // 不存在返回false。
    private boolean dfsVisit(int i) {
        colors[i] = Color.GRAY;
        for (Integer adj: adjs[i]) {
            if (colors[adj] == Color.GRAY) return true; // 访问到同一递归调用中已经访问过的元素。
            if (colors[adj] == Color.WHITE) { // 访问还没有访问过的元素
                if (dfsVisit(adj)) return true;
            }
        }
        // 从递归调用中返回
        colors[i] = Color.BLACK;
        return false;
    }
    private enum Color {
        WHITE, GRAY, BLACK
    }
}

       其实,蛮简单的,多些几遍就会了。

       补充一种做法,昨天看到的,这种方法是利用栈stack辅助操作,这样就不需要用递归了。

class Solution {
    private List<Integer>[] adjs;
    private boolean[] visited;
    public void dfs(int n, int[][] edges) {
        // 初始化
        adjs = new List[n];
        visited = new boolean[n];
        for(int i = 0; i < n; i++) {
            adjs[i] = new LinkedList<>();
        }
        for(int i = 0 ; i<edges.length;i++) {
            int[] edge = edges[i];
            int from = edge[0];
            int to = edge[1];
            adjs[from].add(to);
        }


        for (int i = 0 ; i < n;i++) {
            if (!visited[i]) dfsVisited(i);
        }
    }

    private void dfsVisit(int i) {
        Stack<Integer> stack = new Stack<>();
        stack.push(i);
        while(!stack.isEmpty()) {
            Integer node = stack.pop();
            visited[node] = true;
            for (Integer adj: adjs[node]) {
                if (!visited[adj]) stack.push(adj);
            }
        }
    }
}

这样做的好处是,可以在一个方法体里面完成对每个元素的遍历。比如,如果每个点有val属性,你就可以方便地通过深度遍历进行加和操作。

拓扑排序

我还是不要偷懒,赶紧把这个部分写掉,省的哪天我自己都忘了。参见leetcode题目: 课程表 II 。

深度优先遍历的做法

相当于,处于“树”最低端的节点,排在最后面。(因为无环的图就是一系列的树。)

import java.util.LinkedList;
import java.util.List;

class Solution {
    List<Integer>[] adjs;
    Color[] colors;
    int[] ans;
    int size;
    public int[] topoSort(int n, int[][] edges) {
        adjs = new List[n];
        colors = new Color[n];
        ans = new int[n];
        size = n;
        for (int i = 0 ; i < n; i++) {
            adjs[i] = new LinkedList<>();
            colors[i] = Color.WHITE;
        }

        for (int i = 0 ; i < edges.length; i++) {
            int[] edge = edges[i];
            int pre = edge[1];
            int course = edge[0];
            adjs[pre].add(course);
        }

        for (int i = 0 ; i < n;i++) {
            if (colors[i] == Color.WHITE) {
                if (dfsVisit(i)) return new int[0];
            }
        }
        return ans;
    }

    // 判断是否存在环,如果存在返回true。
    // 当存在环时,是无法进行拓扑排序的。
    private boolean dfsVisit(int i) {
        colors[i] = Color.GRAY;
        for (int adj: adjs[i]) {
            if (colors[adj] == Color.WHITE) {
                if (dfsVisit(adj)) return true;
            }
            if (colors[adj] == Color.GRAY) return true; // 存在环
        }
        colors[i] = Color.BLACK;
        ans[--size] = i; // 这一步很关键,最先返回的是叶子节点。
        return false;
    }

    private enum Color {WHITE, GRAY, BLACK}
}

广度优先遍历的做法

这种做法的意思是,找到入度为0的点,肯定是起点,将它放在开头。就像一系列的课程,你把课程一门一门地修掉。每修掉一门课之后,更新点的入度,然后继续从入度为0的点开始。

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

class Solution {
    List<Integer>[] adjs;
    int[] indegrees;
    public int[] topoSort(int n, int[][] edges) {
        adjs = new List[n];
        indegrees = new int[n];
        for (int i = 0 ; i < n; i++) {
            adjs[i] = new LinkedList<>();
        }
        // 入度统计和邻接链表初始化。
        for (int i = 0 ; i < edges.length; i++) {
            int[] edge = edges[i];
            int pre = edge[1];
            int course = edge[0];
            adjs[pre].add(course);
            indegrees[course] += 1;
        }

        // 广度优先遍历
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0 ; i < n;i++) {
            if (indegrees[i] == 0) q.add(i);
        }
        int[] ans = new int[n];
        int size = 0;
        while(!q.isEmpty()) {
            int i = q.poll(); // 最先访问入度为0的点。
            ans[size++] = i;
            for (int adj: adjs[i]) {
                indegrees[adj] -= 1; // 更新入度
                if(indegrees[adj] == 0) q.add(adj); // 添加入度为0的点
            }
        }
        return size == n? ans:new int[0];
    }
}

遍历#include #include #define max 100 //定义节点最大个数 int tag[100]; typedef char datatype; /*----------------定义边信息--------------*/ typedef struct node { int adress; // 记录节点位子 struct node *next; //指向下一条边的指针 } edgenode; /*-------------节点元素---定义类型--------------*/ typedef struct vnode { datatype element; //节点元素 edgenode *firstedge; //节点所指向的第一条边 int id; } vexternode; /*----------------定义邻接表类型--------------*/ typedef struct map { vexternode maplist[max]; //存放头结点的顺序表 int n,e; //的顶点数边数 } linkmap; int v[100]={0}; //深度优先遍历中标记已访问的信息 int dv[100]={0}; //广度优先遍历中标记已访问的信息 /*----------------定义建立--------------*/ linkmap *create(linkmap *maps) { int chr[100][2],chh;//chr建立元组(没权值) char c[100]; // 存放节点元素 int i,j,m,k; edgenode *p,*pre; maps=(linkmap *)malloc(sizeof(linkmap)); printf("***********************************"); printf("\n"); printf("请输入节点个数:"); printf("输入节点个数:"); scanf("%d",&maps->n); printf("请输入边的个数:"); scanf("%d",&maps->e); scanf("%c",&chh); //空格 printf("请输入节点元素:"); for(i=0;in;i++) { scanf("%c",&c[i]);//输入节点元素 scanf("%c",&chh);//空格 maps->maplist[i].element=c[i];//把节点元素存放到邻接表中 maps->maplist[i].firstedge=NULL; } printf("请输入元组(节点节点之间的关系)\n"); for(i=0;ie;i++) for(j=0;j<2;j++) scanf("%d",&chr[i][j]); m=0; for(i=0;in;i++) { for(k=0;me&&chr[m][0]==i;m++,k++) { p=(edgenode *)malloc(sizeof(edgenode)); p->adress=chr[m][1]; //边p保存节点位子 if(k==0) maps->maplist[i].firstedge=p; else pre->next=p; pre=p; } p->next=NULL; } return maps; } /*----------------深度优先-------------*/ void dfs(linkmap *maps,int i)//i用来指定深度优先遍历的起始值 { edgenode *pp; printf("%c",maps->maplist[i].element); v[i]=1; pp=maps->maplist[i].firstedge; while(pp) { if(!v[pp->adress]) dfs(maps,pp->adress); pp=pp->next; } } void dfsmap(linkmap *maps) { int i=0; for(i=0;in;i++) v[i]=0; for(i=0;in;i++) if(!v[i]) { dfs(maps,i); } } /*----------------广度优先-------------*/ void bfs(linkmap *map,int i) { edgenode *p; int queue[100],front,real,k; front=-1; real=-1; printf("%c",map->maplist[i].element); dv[i]=1; queue[++real]=i; while(frontmaplist[k].firstedge; while(p) { if(!dv[p->adress]) { printf("%c",map->maplist[p->adress].element); queue[++real]=p->adress; dv[p->adress]=1; } p=p->next; } } } void bfsmap(linkmap *maps) { int i=0; for(i=0;in;i++) dv[i]=0; for(i=0;in;i++) if(!dv[i]) bfs(maps,i); } /*----------------计算入度数-------------*/ void id(linkmap *maps) { int i=0; edgenode *p=maps->maplist[i].firstedge; for(i;in;i++) maps->maplist[i].id=0; for(i=0;in;i++) { p=maps->maplist[i].firstedge; while(p) { maps->maplist[p->adress].id++; p=p->next; } } } /*----------------输出各节点的入度数-------------*/ void print(linkmap *maps) { int i=0; for(i;in;i++) printf("%d",maps->maplist[i].id); } /*----------------输出拓扑排序-------------*/ int topsort(linkmap *map) { int k=0,i,j,v,tag[100];//tag用来标记是否已访问到 int queue[100];//用队列存储 int front=0,real=0; edgenode *p; for(i=0;in;i++) { tag[i]=0;//初始化标记 } for(i=0;in;i++) { if(map->maplist[i].id==0&&tag[i]==0) { queue[++real]=i;//让每一个未被访问到的且入度为0的节点进栈 tag[i]=1;//当节点进栈时,标记此节点被访问过 } } while(frontmaplist[v].element);//输出刚出栈的元素 k++;//用来统计拓扑排序输出的个数 p=map->maplist[v].firstedge; //p指向此节点的下一条边 while(p) { j=p->adress;//j记下下一条边所对应节点的位子 if(map->maplist[j].id==0&&tag[j]==0)//下一条边节点入度减一,并判断之后入度是否为零且未被访问过 { queue[++real]=j;//让每一个未被访问到的且入度为0的节点进栈 tag[j]=1;//进栈…… } p=p->next;//p指向下一条关联于该节点的边 } } return k; //k用来计算输出的个数,并判定了是否有环 } /*--------的非递归遍历-------*/ void fdg(linkmap *maps,int i) { edgenode *p,*q; linkmap *m; int stack[100]; int top=0; stack[top]=i; printf("%c ",maps->maplist[i].element); tag[i]=1; p=maps->maplist[i].firstedge; while(top>=0) { while(p) { if(tag[p->adress]!=1) printf("%c ",maps->maplist[p->adress].element); stack[++top]=p->adress; tag[p->adress]=1; q=p; p=maps->maplist[p->adress].firstedge; if(p&&tag[p->adress]==1) p=p->next; } do{ p=q; if(top==0) { p->adress=stack[top]; top--; } else p->adress=stack[--top]; p=maps->maplist[p->adress].firstedge; if(top==-1) break; while(p!=NULL) { if(tag[p->adress]==1) p=p->next; else break; }; }while(!p); } } void fdgsmap(linkmap *maps) { int i=0; for(i=0;in;i++) tag[i]=0; for(i=0;in;i++) if(!tag[i]) fdg(maps,i); } void main() { edgenode *p1; linkmap *maps; int i=0,c,num; maps=create(maps); id(maps); printf("深度优先遍历结果为:"); dfsmap(maps); printf("\n广度优先遍历结果为:"); bfsmap(maps); printf("拓扑排序结果为:"); num=topsort(maps); if(num==maps->n) printf("此拓扑排序树无环\n"); else printf("此拓扑排序树有环\n"); printf(" \n非递归深度优先遍历结果为:"); fdgsmap(maps); printf("\n"); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值