图论


图的存储结构中有两种:邻接表矩阵。通常邻接表适用于边比较少的图中(边多,查找效率低),矩阵通常适用于比较稠密的图中(边少浪费空间)。

本文就邻接表存储图结构对图中比较重要的内容(图构建深度广度遍历dijstra算法prim算法以及kruskal算法进行介绍和代码实现java),水平有限,欢迎大家吐槽。主要内容参考此文 ,对原作者表示感谢。

图构建

  • 初始化方式
  1. 让用户输入(比较繁琐)
  2. 根据领点数组、边数组自动进行构建
    为了灵活,设置两种初始化方式:
public ListUdg() {}
public ListUdg(char[] inodes, EData[] eData) 
  • ListUdg内部成员:

程序中涉及到的数据结构如下:

图结构
    //表示一个节点
    private class Vnode {
        char data;
        Edge firstEdge;
    }
    //邻接边
    private class Edge {
        int index;
        int weight;
        Edge next;
    }
    //表示输入数据
     private static class EData {
        char start, end;
        int weight;
    }
    //成员变量:
    private Vnode[] Vnodes;
    private Map<Character, Integer> map; //线性查找到Char对应的数组下标
    private int edgeNumL; //记录图中总共的边数
  • 工具函数:
    private char readChar() 
    private int readInt()
  • 初始化:
    //以数组构建为例,用户输入也是一致的:
     public ListUdg(char[] inodes, EData[] eData) {
     //初始化内部成员变量
        int length = inodes.length;
        map = new HashMap<>();
        for (int i = 0; i < length; i++) {
            map.put(inodes[i], i);
        }
        Vnodes = new Vnode[length];
        for (int i = 0; i < length; i++) {
            Vnodes[i] = new Vnode();
            Vnodes[i].data = inodes[i];
            Vnodes[i].firstEdge = null;
        }
        edgeNumL = eData.length;
        //根据输入边,构建图结构
        for (int i = 0; i < eData.length; i++) {
            int index1 = map.get(eData[i].start);
            int index2 = map.get(eData[i].end);
            
            //注意,无向图,边的末端对应的下标
            Edge edge1 = new Edge(index2, eData[i].weight, null);
            Edge edge2 = new Edge(index1, eData[i].weight, null);
            
            if (Vnodes[index1].firstEdge == null) {
                Vnodes[index1].firstEdge = edge1;
            } else {
            //将连接的多条边,连接起来
                linkEdge(Vnodes[index1].firstEdge, edge1);
            }

            if (Vnodes[index2].firstEdge == null) {
                Vnodes[index2].firstEdge = edge2;
            } else {
                linkEdge(Vnodes[index2].firstEdge, edge2);
            }
        }
    }
    
    //插入到末端即可
    private void linkEdge(Edge list, Edge next) {
        while (list.next != null) {
            list = list.next;
        }
        list.next = next;
    }

    编写打印函数,验证建表是否成功(具体代码参见)
    初始化数据如下:
图初始化数据

遍历方式

  • 广度遍历
    : 数据结构使用队列,邻接表已经将所有的连接边串起来,迭代next即可,注意将已遍历的点加入队列。队列使用array,设置head,rear两个游标。
        public void bfs() {
        int length = Vnodes.length;
        int[] queue = new int[length];
        int head = 0;
        int rear = 0;
        boolean[] visited = new boolean[length];
        for (int i = 0; i < visited.length; i++) {
            visited[i] = false;
        }
        System.out.println("BFS:");
        for (int i = 0; i < length; i++) {
            if (!visited[i]) {
                visited[i] = true;
                System.out.println("visited:" + Vnodes[i].data);
                //访问之后,插入队列
                queue[rear++] = i;
            }

            while (head != rear) {
                int index = queue[head++];
                Edge edge = Vnodes[i].firstEdge;
                while (edge != null) {
                    int tIndex = edge.index;
                    if (!visited[tIndex]) {
                        visited[tIndex] = true;
                        System.out.println("visited:" + Vnodes[tIndex].data);
                        //访问之后,插入队列
                        queue[rear++] = tIndex;
                    }
                    edge = edge.next;
                }
            }
        }
        System.out.println();
    }
  • 深度遍历
    : 使用递归,同样设置visited数组表示访问情况
public void dfs() {
        int length = Vnodes.length;
        boolean[] visited = new boolean[length];
        for (int i = 0; i < length; i++) {
            visited[i] = false;
        }
        System.out.println("DFS:");
        for (int i = 0; i < length; i++) {
            if(!visited[i])
                dfs(i, visited);
        }
        System.out.println();
    }

    private void dfs(int index, boolean[] visited) {
        visited[index] = true;
        System.out.println("visited:" + Vnodes[index].data);
        Edge edge = Vnodes[index].firstEdge;
        while (edge != null) {
//            快速迭代完成
            if (!visited[edge.index]) {
                dfs(edge.index, visited);
            }
            edge = edge.next;
        }
    }

图中比较重要的几种算法:

dijkstra算法

单源最短路径,给定起点,输出到所有其他的点最短的路径。

算法思路

设置两个集合S和U,S中保存的是已经遍历过的点,U中是未遍历完成的(通过visited数组区分);初始化S中只有起点,找到两集合之间最短距离,并将边的另一端加入集合S中,更新s到其他顶点的距离(两种情况,未达->可达,距离变短,(s,v)的距离可能大于(s,k)+(k,v)的距离。

    public void dijkstra(int vtex,int[] prev,int[] dist){
    //初始化,visited数组和距离数组
        int length = dist.length;
        boolean[] visited = new boolean[length];
        for (int i = 0; i < length; i++) {
            visited[i] = false;
            prev[i] = 0;
            //获取相连边距离
            dist[i] = getWeight(vtex,i);
        }
        visited[vtex] = true;
        dist[vtex] = 0;
        
//        每次取出新的节点
        int k = 0;
        for (int i = 0; i < length - 1; i++) {
//            取出最小距离的点
            int min = INF;
            for (int j = 0; j < length; j++) {
                if(!visited[j] && dist[j] < min){
                    min = dist[j];
                    k = j;
                }
            }
            visited[k] = true;

//            对新加入的点进行距离更新
            for (int j = 0; j < length; j++) {
                int tmp = getWeight(k,j);
                //防止运算溢出
                tmp = (tmp == INF)?INF:(min+tmp);
                if(!visited[j] && tmp < dist[j]){
                //距离变短,更新前驱节点和距离
                    prev[j] = k;
                    dist[j] = tmp;
                }
            }
        }
        System.out.println("dist:");
        for (int i = 0; i < dist.length; i++) {
            System.out.print(dist[i]+"  ");
        }
        System.out.println();
        System.out.println("prev:");
        for (int i = 0; i < prev.length; i++) {
            System.out.print(prev[i]+"  ");
        }
        System.out.println();
        //根据前驱节点回溯,打印路径
        for (int i = 0; i < length ; i++) {
            if(i != vtex){
                System.out.printf("%s -> %s distance:%d\n\t",Vnodes[vtex].data,Vnodes[i].data,dist[i]);
//                output the path from the right --> left
                System.out.print(Vnodes[i].data+"-->");
                int pre = prev[i];
                while(pre != vtex){
                    System.out.printf("%s-->",Vnodes[pre].data);
                    pre =  prev[pre];
                }
                System.out.printf("%s\n",Vnodes[vtex].data);
            }
        }
    }    
    
    //相连边距离
    private int getWeight(int start,int end){
        Edge edge = Vnodes[start].firstEdge;
        while(edge != null){
            if(edge.index == end){
                return edge.weight;
            }
            edge = edge.next;
        }
        return INF;
    }

prim算法

先介绍prim,prim算法在思想方法上和dijkstra很类似。

算法思路

同样设置两个集合,点集合U(存放最小生成树中点),边集合(最小生成树边); 从所有uЄU,vЄ(V-U)(V-U表示去除U的所有顶点)的边中选取权值最小的边(u,v),将顶点v加入集合U中,将边(u,v)加入集合T中,如此不断重复,直到U=V为止,最小生成树构造完毕。

    public void prim(int start){
    //初始化点集合和边集合
        int length = Vnodes.length;
        boolean[] visited = new boolean[length];
        //为了方便打印路径,设置EData数组表示距离
        EData[] eData = new EData[length];
        for (int i = 0; i < length; i++) {
            int tmp = getWeight(start,i);
            eData[i] = new EData(Vnodes[start].data,Vnodes[i].data,tmp);
        }
        visited[start] = true;
        //得到V-U中最短边的一端顶点
        int u = getMin(visited,eData);
        int sum = 0;
        while(u != -1){
        //加入集合U
            visited[u] = true;
            sum += eData[u].weight;
            //进行调整
            for (int i = 0; i < length; i++) {
                int tmp = getWeight(u,i);
                //集合V-U边的更新
                if(!visited[i] && tmp < eData[i].weight){
                    eData[i].weight = tmp;
                    eData[i].start = Vnodes[u].data;
                    eData[i].end = Vnodes[i].data;
                }
            }
            u = getMin(visited,eData);
        }
        //输出路径信息
        System.out.println("prim distance:");
        for (int i = 0; i < length; i++) {
            if(i != start){
                System.out.printf("%s-->%s %d\n",eData[i].start,eData[i].end,eData[i].weight);
            }
        }
        System.out.println("total :"+sum);
    }    
    //获取最短边
    private int getMin(boolean[] visited,EData[] eData){
        int index = -1;
        int min = INF;
        for (int i = 0; i < eData.length; i++) {
            if(!visited[i] && eData[i].weight < min){
                min = eData[i].weight;
                index = i;
            }
        }
        return index;
    }

kruskal算法

对所有的边进行从小到大排序,n个顶点中选择n-1条边,并保证这n-1条边不构成回路。

算法关键之处在于如何保证不构成回路,通过类似并查集(union-find)思路,将所有联通的路径端点的ends设置为同一个值,对于要加入的边,如果边两端的点vends相同说明已经有路径可达,没有必要添加此边。

    //获取边集合,也可以保存用户输入或者初始化的边集合直接使用
    private EData[] getEdata(){
        EData[] edata = new EData[edgeNumL];
        int index = 0;
        for (int i = 0; i < Vnodes.length; i++) {
            Edge edge = Vnodes[i].firstEdge;
            while(edge!=null){
            //去除无向图中的重复边,为边集排序准备
                if(edge.index > i){
                    edata[index++]= new EData(Vnodes[i].data,Vnodes[edge.index].data,edge.weight);
                }
                edge = edge.next;
            }
        }
        return edata;
    }
    //对边集合进行排序,可以使用快排、堆排,这里为了方便直接冒泡排序
    private void sortEdges(EData[] eData){
        for (int i = 0; i < edgeNumL; i++) {
            for (int j = i+1; j < edgeNumL; j++) {
                if(eData[i].weight > eData[j].weight){
                    EData tmp = eData[i];
                    eData[i] = eData[j];
                    eData[j] = tmp;
                }
            }
        }
    }
    //算法实现
    public void kruskal(){
    //获取边集
        EData[] edata = getEdata();
    //边集排序
        sortEdges(edata);
    //递归追溯数组
        int[] ends = new int[edgeNumL];
    //最短边集    
        EData[] ret = new EData[edgeNumL];
        int index = 0;

        for (int i = 0; i < edgeNumL; i++) {
            EData tmp = edata[i];
            int start = map.get(tmp.start);
            int end = map.get(tmp.end);

            int m = getEnd(ends,start);
            int n = getEnd(ends,end);
            //如果对应的ends值不同,需要添加此边
            if(m!=n){
                ends[m] = n;
                ret[index++] = tmp;
            }
        }
        //打印结果
        int sum = 0;
        System.out.println("krusal distance:");
        for (int i = 0; i < index; i++) {
            System.out.printf("%s -> %s:%d\n",ret[i].start,ret[i].end,ret[i].weight);
            sum+= ret[i].weight;
        }
        System.out.println("total :"+sum);
    }
    //
    private int getEnd(int[] ends,int index){
        while(ends[index] != 0){
            index = ends[index];
        }
        return index;
    }

完整代码

本文只是基本实现,针对各个不同算法都有一定的改进版本,后面有时间再添加补充。

参考资料:

  1. http://wangkuiwu.github.io/2013/01/01/datastruct-index/
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 图的概念 图是一种非线性的数据结构,一个图中有两类东西,一种是结点,一种是边.我们用V这个集合来表示节点(vert...
    fredal阅读 2,312评论 2 14
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,740评论 0 19
  • 目录 1.基本图算法参见基本的图算法参见深度优先搜索和广度优先搜索专题 2.最小生成树——无向图参见最小生成树 3...
    王侦阅读 1,497评论 0 1
  • 1736年,瑞士数学家Euler(欧拉)在他的一篇论文中讨论了格尼斯七桥问题,由此诞生了一个全新的数学分支——图论...
    不困于情阅读 4,371评论 0 9
  • 有他是在一个偶然夫妻日常生活里,家里有一个规定就是:每天要坚持劳动,分享你做了什么,收获了什么…… 有一种“相见恨...
    江村塘影阅读 988评论 0 1