当前位置:

首页 > 编程开发 > Java这么实现无向图

Java这么实现无向图

基本概念图的定义一个图是由点集V={vi}和VV中元素的无序对的一个集合E={ek}所构成的二元组,记为G=(V,E),V中的元素vi叫做顶点,E中的元素ek叫做边。对于V中的两个点u,v,如果边(u,v)属于E,则称u,v两点相邻,u,v称为边(u,v)的端点。我们可以用m(G)=|E|表示图G中的边数,用n(G)=|V|表示图G中的顶点个数。无向图的定义对于E中的任意一条边(vi,vj),如果边(vi,vj)端点无序,则它是无向边,此时图G称为无向图。无向图是最简单的图模型,下图显示了同一幅无向图,顶

    基本概念

    图的定义

    一个图是由点集V={vi} 和 VV 中元素的无序对的一个集合E={ek} 所构成的二元组,记为G=(V,E),V中的元素vi叫做顶点,E中的元素 ek叫做边。

    对于V中的两个点 u,v,如果边(u,v) 属于E,则称 u,v两点相邻,u,v称为边(u,v)的端点。

    我们可以用m(G)=|E| 表示图G中的边数,用n(G)=|V|表示图G中的顶点个数。

    无向图的定义

    对于E中的任意一条边(vi,vj),如果边(vi,vj) 端点无序,则它是无向边,此时图G称为无向图。无向图是最简单的图模型,下图显示了同一幅无向图,顶点使用圆圈表示,边则是顶点之间的连线,没有箭头(图片来自于《算法第四版》):

    Java这么实现无向图

    无向图的 API

    对于一幅无向图,我们关心图的顶点数、边数、每个顶点的相邻顶点和边的添加操作,所以接口如下所示:

    package com.zhiyiyo.graph;
    
    /**
     * 无向图
     */
    public interface Graph {
        /**
         * 返回图中的顶点数
         */
        int V();
    
        /**
         * 返回图中的边数
         */
        int E();
    
        /**
         * 向图中添加一条边
         * @param v 顶点 v
         * @param w 顶点 w
         */
        void addEdge(int v, int w);
    
        /**
         * 返回所有相邻顶点
         * @param v 顶点 v
         * @return 所有相邻顶点
         */
        Iterable adj(int v);
    }

    无向图的实现方式

    邻接矩阵

    用矩阵表示图对研究图的性质及应用常常是比较方便的,对于各种图有各种矩阵表示方式,比如权矩阵和邻接矩阵,这里我们只关注邻接矩阵。它的定义为:

    对于图G=(V,E),|V|=n,构造一个矩阵 A=(aij)n×n,其中:

    Java这么实现无向图

    则称矩阵A为图G的邻接矩阵。

    由定义可知,我们可以使用一个二维的布尔数组 A 来实现邻接矩阵,当 A[i][j] = true 时说明顶点 i 和 j 相邻。

    对于 n个顶点的图 G,邻接矩阵需要消耗的空间为 n2个布尔值的大小,对于稀疏图来说会造成很大的浪费,当顶点数很大时所消耗的空间会是个天文数字。同时当图比较特殊,存在自环以及平行边时,邻接矩阵的表示方式是无能为力的。《算法》中给出了存在这两种情况的图:

    Java这么实现无向图

    边的数组

    对于无向图,我们可以实现一个类 Edge,里面只用两个实例变量用来存储两个顶点 u和 v,接着在一个数组里面保存所有 Edge 即可。这样做有一个很大的问题,就是在获取顶点 v的所有相邻顶点时必须遍历整个数组才能得到,时间复杂度是O(|E|),由于获取相邻顶点是很常用的操作,所以这种表示方式也不太行。

    邻接表数组

    如果我们把顶点表示为一个整数,取值范围为0∼|V|−1,那么就可以用一个长度为|V| 的数组的索引表示每一个顶点,然后将每一个数组元素设置为一个链表,上面挂载着索引所代表的的顶点相邻的其他顶点。图一所示的无向图可以用下图所示的邻接表数组表示出来:

    Java这么实现无向图

    使用邻接表实现无向图的代码如下所示,由于邻接表数组中的每个链表都会保存与顶点相邻的顶点,所以将边添加到图中时需要对数组中的两个链表进行添加节点的操作:

    package com.zhiyiyo.graph;
    
    import com.zhiyiyo.collection.stack.LinkStack;
    
    /**
     * 使用邻接表实现的无向图
     */
    public class LinkGraph implements Graph {
        private final int V;
        private int E;
        private LinkStack[] adj;
    
        public LinkGraph(int V) {
            this.V = V;
            adj = (LinkStack[]) new LinkStack[V];
            for (int i = 0; i < V; i++) {
                adj[i] = new LinkStack<>();
            }
        }
    
        @Override
        public int V() {
            return V;
        }
    
        @Override
        public int E() {
            return E;
        }
    
        @Override
        public void addEdge(int v, int w) {
            adj[v].push(w);
            adj[w].push(v);
            E++;
        }
    
        @Override
        public Iterable adj(int v) {
            return adj[v];
        }
    }

    这里用到的栈代码如下所示,栈的实现不是这篇博客的重点,所以这里不做过多解释:

    package com.zhiyiyo.collection.stack;
    
    import java.util.EmptyStackException;
    import java.util.Iterator;
    
    /**
     * 使用链表实现的堆栈
     */
    public class LinkStack {
        private int N;
        private Node first;
    
        public void push(T item) {
            first = new Node(item, first);
            N++;
        }
    
        public T pop() throws EmptyStackException {
            if (N == 0) {
                throw new EmptyStackException();
            }
    
            T item = first.item;
            first = first.next;
            N--;
            return item;
        }
    
        public int size() {
            return N;
        }
    
        public boolean isEmpty() {
            return N == 0;
        }
    
        public Iterator iterator() {
            return new ReverseIterator();
        }
    
        private class Node {
            T item;
            Node next;
    
            public Node() {
            }
    
            public Node(T item, Node next) {
                this.item = item;
                this.next = next;
            }
        }
    
    
        private class ReverseIterator implements Iterator {
            private Node node = first;
    
            @Override
            public boolean hasNext() {
                return node != null;
            }
    
            @Override
            public T next() {
                T item = node.item;
                node = node.next;
                return item;
            }
    
            @Override
            public void remove() {
            }
        }
    }

    无向图的遍历

    给定下面一幅图,现在要求找到每个顶点到顶点 0 的路径,该如何实现?或者简单点,给定顶点 0 和 4,要求判断从顶点 0 开始走,能否到达顶点 4,该如何实现?这就要用到两种图的遍历方式:深度优先搜索和广度优先搜索。

    Java这么实现无向图

    在介绍这两种遍历方式之前,先给出解决上述问题需要实现的 API:

    package com.zhiyiyo.graph;
    
    public interface Search {
        /**
         * 起点 s 和 顶点 v 之间是否连通
         * @param v 顶点 v
         * @return 是否连通
         */
        boolean connected(int v);
    
        /**
         * 返回与顶点 s 相连通的顶点个数(包括 s)
         */
        int count();
    
        /**
         * 是否存在从起点 s 到顶点 v 的路径
         * @param v 顶点 v
         * @return 是否存在路径
         */
        boolean hasPathTo(int v);
    
        /**
         * 从起点 s 到顶点 v 的路径,不存在则返回 null
         * @param v 顶点 v
         * @return 路径
         */
        Iterable pathTo(int v);
    }

    深度优先搜索

    深度优先搜索的思想类似树的先序遍历。我们从顶点 0 开始,将它的相邻顶点 2、1、5 加到栈中。接着弹出栈顶的顶点 2,将它相邻的顶点 0、1、3、4 添加到栈中,但是写到这你就会发现一个问题:顶点 0 和 1明明已经在栈中了,如果还把他们加到栈中,那这个栈岂不是永远不会变回空。所以还需要维护一个数组 boolean[] marked,当我们将一个顶点 i 添加到栈中时,就将 marked[i] 置为 true,这样下次要想将顶点 加入栈中时,就得先检查一个 marked[i] 是否为 true,如果为 true 就不用再添加了。重复栈顶节点的弹出和节点相邻节点的入栈操作,直到栈为空,我们就完成了顶点 0 可达的所有顶点的遍历。

    为了记录每个顶点到顶点 0 的路径,我们还需要一个数组 int[] edgeTo。每当我们访问到顶点 u 并将其一个相邻顶点 i 压入栈中时,就将 edgeTo[i] 设置为 u,说明要想从顶点i 到达顶点 0,需要先回退顶点 u,接着再从顶点 edgeTo[u] 处获取下一步要回退的顶点直至找到顶点 0。

    package com.zhiyiyo.graph;
    
    import com.zhiyiyo.collection.stack.LinkStack;
    import com.zhiyiyo.collection.stack.Stack;
    
    
    public class DepthFirstSearch implements Search {
        private boolean[] marked;
        private int[] edgeTo;
        private Graph graph;
        private int s;
        private int N;
    
        public DepthFirstSearch(Graph graph, int s) {
            this.graph = graph;
            this.s = s;
            marked = new boolean[graph.V()];
            edgeTo = new int[graph.V()];
            dfs();
        }
    
        /**
         * 递归实现的深度优先搜索
         *
         * @param v 顶点 v
         */
        private void dfs(int v) {
            marked[v] = true;
            N++;
            for (int i : graph.adj(v)) {
                if (!marked[i]) {
                    edgeTo[i] = v;
                    dfs(i);
                }
            }
        }
    
        /**
         * 堆栈实现的深度优先搜索
         */
        private void dfs() {
            Stack vertexes = new LinkStack<>();
            vertexes.push(s);
            marked[s] = true;
    
            while (!vertexes.isEmpty()) {
                Integer v = vertexes.pop();
                N++;
    
                // 将所有相邻顶点加到堆栈中
                for (Integer i : graph.adj(v)) {
                    if (!marked[i]) {
                        edgeTo[i] = v;
                        marked[i] = true;
                        vertexes.push(i);
                    }
                }
            }
        }
    
        @Override
        public boolean connected(int v) {
            return marked[v];
        }
    
        @Override
        public int count() {
            return N;
        }
    
        @Override
        public boolean hasPathTo(int v) {
            return connected(v);
        }
    
        @Override
        public Iterable pathTo(int v) {
            if (!hasPathTo(v)) return null;
            Stack path = new LinkStack<>();
    
            int vertex = v;
            while (vertex != s) {
                path.push(vertex);
                vertex = edgeTo[vertex];
            }
    
            path.push(s);
            return path;
        }
    }

    广度优先搜索

    广度优先搜索的思想类似树的层序遍历。与深度优先搜索不同,从顶点 0 出发,广度优先搜索会先处理完所有与顶点 0 相邻的顶点 2、1、5 后,才会接着处理顶点 2、1、5 的相邻顶点。这个搜索过程就是一圈一圈往外扩展、越走越远的过程,所以可以用来获取顶点 0 到其他节点的最短路径。只要将深度优先搜索中的堆换成队列,就能实现广度优先搜索:

    package com.zhiyiyo.graph;
    
    import com.zhiyiyo.collection.queue.LinkQueue;
    
    public class BreadthFirstSearch implements Search {
        private boolean[] marked;
        private int[] edgeTo;
        private Graph graph;
        private int s;
        private int N;
    
        public BreadthFirstSearch(Graph graph, int s) {
            this.graph = graph;
            this.s = s;
            marked = new boolean[graph.V()];
            edgeTo = new int[graph.V()];
            bfs();
        }
    
        private void bfs() {
            LinkQueue queue = new LinkQueue<>();
            marked[s] = true;
            queue.enqueue(s);
    
            while (!queue.isEmpty()) {
                int v = queue.dequeue();
                N++;
    
                for (Integer i : graph.adj(v)) {
                    if (!marked[i]) {
                        edgeTo[i] = v;
                        marked[i] = true;
                        queue.enqueue(i);
                    }
                }
            }
        }
    }

    队列的实现代码如下:

    package com.zhiyiyo.collection.queue;
    
    
    import java.util.EmptyStackException;
    
    
    public class LinkQueue {
        private int N;
        private Node first;
        private Node last;
    
        public void enqueue(T item) {
            Node node = new Node(item, null);
            if (++N == 1) {
                first = node;
            } else {
                last.next = node;
            }
            last = node;
        }
    
        public T dequeue() throws EmptyStackException {
            if (N == 0) {
                throw new EmptyStackException();
            }
    
            T item = first.item;
            first = first.next;
            if (--N == 0) {
                last = null;
            }
            return item;
        }
    
        public int size() {
            return N;
        }
    
        public boolean isEmpty() {
            return N == 0;
        }
    
        private class Node {
            T item;
            Node next;
    
            public Node() {
            }
    
            public Node(T item, Node next) {
                this.item = item;
                this.next = next;
            }
        }
    }
    本文内容来源于互联网,如有侵权请联系删除。
    作者最新文章
    编程开发
    相关文章 更多
    谷歌浏览器Mac版入口
    谷歌浏览器Mac版入口

    谷歌浏览器Mac版官方安装指南 谷歌浏览器Mac版官方安装入口是https://www.google.com/chrome/,需macOS 12+系统、500MB空间,下载.dmg后拖入应用程序安装,支持多设备同步、性能优化与隐私保护功能。 苹果电脑Chrome的安装入口究竟在哪里?这个问题最近可是

    Chrome浏览器JS脚本不运行怎么办
    Chrome浏览器JS脚本不运行怎么办

    Chrome中JavaScript未执行需依次检查:一、移除站点级禁用并添加允许域名;二、开启全局JavaScript开关;三、禁用干扰扩展;四、在开发者工具中启用JavaScript;五、重置内容设置为默认。 有时在Chrome里打开网页,会发现交互按钮点了没反应,数据加载不出来,页面仿佛“静止”

    IE浏览器怀旧版在线网址
    IE浏览器怀旧版在线网址

    IE浏览器怀旧版在线网址:一次精准的技术时光回溯 最近,不少老用户和怀旧爱好者在反复搜索一个问题:那个经典的Internet Explorer,如今还能在哪里原汁原味地体验到?答案指向一个特定的地址:https://ie.microsoft.com/legacy/。 这个网站远不止是一个简单的“皮肤

    火狐浏览器有哪些设置功能
    火狐浏览器有哪些设置功能

    火狐浏览器五大核心设置功能:解锁高效、安全与个性化体验 火狐浏览器功能强大,但如果不仔细挖掘,很多能大幅提升效率和安全性的设置可能就“藏着掖着”了。这就好比拥有一台高性能设备,却只用了基础模式。那么,如何把它调整到最顺手、最安全的状态?接下来,我们就聚焦于当前版本(截至2025年末)最关键的五大设置

    chrome搜索免验证入口
    chrome搜索免验证入口

    Chrome官方免验证入口为https://www.google.cn/chrome/,提供全平台安装包、免登录即用、本地化安全机制及引擎级性能优化。 到底该去哪里找正版、免费且无需繁琐验证的Chrome浏览器入口?这个问题困扰了不少网友。今天,我们就来直通核心,为大家详细拆解Chrome引擎的官方

    java heap space 选型思路:使用场景与区别整理
    java heap space 选型思路:使用场景与区别整理

    Java堆是JVM存储对象的核心内存区域,配置需结合场景:单体应用适中设置;大数据处理需大堆并关注GC停顿;微服务强调快速启动;高并发需精细划分堆区域。关键参数-Xms和-Xmx建议等值以稳定性能。垃圾回收器选择影响效率,如G1适用于大堆,ZGC可实现低停顿。内存错误时需监控堆状态。

    java heap space 使用中遇到的问题怎么解决
    java heap space 使用中遇到的问题怎么解决

    Java堆内存溢出错误通常因内存泄漏、数据处理需求过大或JVM参数配置不当引起。排查时可借助jmap、堆转储及MAT等工具定位问题。解决方案包括调整JVM内存参数(如-Xmx)、修复代码中的内存泄漏、优化大数据处理逻辑,并建立持续监控与预防机制,以保障应用稳定运行。

    java xml 选型思路:使用场景与区别整理
    java xml 选型思路:使用场景与区别整理

    XML在Java开发中用于配置、数据交换等场景。解析方式主要有DOM、SAX、StAX及第三方库。DOM适合操作小文件,SAX/StAX适合处理大文件流,JAXB用于对象与XML映射。选型需结合数据大小、内存、性能及团队熟悉度,现代框架常封装底层解析。

    using namespace 使用中遇到的问题怎么解决
    using namespace 使用中遇到的问题怎么解决

    命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,

    c语言函数递归 实操经验总结:这些技巧很实用
    c语言函数递归 实操经验总结:这些技巧很实用

    理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    macOS软件
    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    Mac软件 更多
    灵活计算器
    灵活计算器
    macOS/iOS/Android

    灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

    赤友清理大师
    赤友清理大师
    macOS

    赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    WINDOWS 更多
    Windows 10
    Windows 10
    Windows

    Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

    极度公式
    极度公式
    Windows/macOS/Linux

    极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

    密码键盘
    密码键盘
    Windows/macOS/iOS/Android

    密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。