【数据结构与算法】之图的基本概念 --- 第十六篇

博主秋招提前批已拿百度、字节跳动、拼多多、顺丰等公司的offer,可加微信:pcwl_Java 一起交流秋招面试经验,可获得博主的秋招简历和复习笔记。 

上一篇:堆的详解:https://blog.youkuaiyun.com/pcwl1206/article/details/84608381

目  录:

一、基本概念

1.1  图的定义

1.2  图中的术语

1.3  图的分类

二、图的存储方式

2.1  邻接矩阵

2.2  邻接表

三、图的应用

四、总结

五、图的Java代码实现

5.1、图的抽象数据类型描述

5.2  图的邻接矩阵描述

5.3  图的邻接表描述


一、基本概念

我们前面学习的数组、列表这类线性结构中的元素是“一对一”的关系,树中的元素是“一对多”的关系,而图中存储的元素则是“多对多”的关系。

1.1  图的定义

图(Graph)是由定点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V, E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

1.2  图中的术语

顶点:图中的元素叫作“顶点”(Vertex);

边:图中的一个顶点可以与任意其他顶点建立连接关系,我们把这种建立的关系叫作“边”(Edge);

:跟顶点相连接边的个数就叫做“度”(Degree);

入度:表示有多少条边指向这个顶点;

出度:表示有多少条边是以这个顶点为起点指向其他顶点的;

路径长度:路径上边的数目。

1.3  图的分类

按照有无方向,图可以分为有向图无向图

按照边的疏密程度分为稀疏图稠密图,这是个相对的概念。

带权图:每条边都有一个权重(Weight)。

连通图:在无向图G中,任意两个顶点是相通的就是连通图。


二、图的存储方式

图的结构比较复杂,任意两个顶点之间都可能存在关系,不能用简单的顺序存储结构来表示。我们经常用邻接矩阵、邻接表、十字链表以及邻接多重表去存储图中的元素。

2.1  邻接矩阵

邻接矩阵用一个二维数组存储数据。

特点:

1、0表示无边、1表示有变、有向图中值就是权重;

2、顶点的度是行内数组之和;各行之和是出度,各列之和是入度;

3、对于无向图来说,如果顶点 i 与顶点 j 之间右边,我们就将A[ i ][ j ]和A[ j ][ i ]都标记为1;

4、对于有向图来说,如果顶点 i 到顶点 j 之间,有一条箭头从 i 指向顶点 j 的边,那我们就将A[ i ][ j ]标记为1;同理,如果有一条箭头从 j 指向顶点 i 的边,那我们就将A[ j ][ i ]标记为1。

5、邻接矩阵的缺点:

如果我们存储的是稀疏图,即顶点很多,但是每个顶点上的边不多,那邻接矩阵的存储方式就比较浪费空间了,除此之外,在无向图中也会浪费掉一半的存储空间,因为无向图的邻接矩阵的上三角和下三角是完全一样的;

6、邻接矩阵的优点:

存储方式简单、直接。因为是基于数组的,所以在获取两个顶点的关系时,非常高效;另外方便计算,因为可以将很多图的运算转换成矩阵之间的运算。如求解最短路径问题使用的Floyd-Warshall算法,就是利用矩阵循环相乘若干次得到结果;

7、邻接矩阵是使用空间换时间的思想。

2.2  邻接表

针对上面邻接矩阵比较浪费内存空间的问题,下面来讲解另外一种可以存储图中元素的方式:邻接表。

邻接表有点像散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。

需要做出说明的是:下图中表示的是一个有向图的邻接表存储方式,每个顶点对应的链表里面,存储的是本节点作为起始节点所指向的顶点。但是对于无向图来说,每个顶点的链表中存储的是和这个顶点有边相连的顶点。

上面说到,邻接矩阵是空间换时间的思想。而邻接表正好相反,是时间换空间的思想。

如上图所示,如果我们要确定是否存在一条从顶点2到顶点4的边,那我们就必须遍历顶点2对应的那条链表,看链表中是否存在顶点4。而且链表的存储方式对缓存不够友好。所以,比起邻接矩阵的存储方式,在邻接表中查询两个顶点之间的关系就没有那么高效了。

因为邻接表比较像散列表。那我们可以想到在基于链表法解决散列冲突的散列表中,如果链过长,为了提高查找效率,可以将链表换成其他更加高效的数据结构,比如:平衡二叉查找树等。所以,我们也可以将邻接表和散列表一样进行优化。


三、图的应用

其实图的应用非常广泛,比如我们经常使用的社交平台微博和微信中是如何存储好友关系的呢?以及QQ好友里面的亲密度关系呢?以及地图、网络以及网络上的超链接等等。下面就以微博和微信这种社交网络中的好友关系举例说明。

经过前面对图的基本概念的了解,不难发现:微信是一种“无向图”,因为微信是互加好友的。但是微博可以单方面关注或者互关,所以微博是一种“有向图”。

针对微博用户关系,假设我们需要支持下面这几种操作:

1、判断用户A是否关注了用户B;

2、判断用户B是否关注了用户A;

3、用户A关注用户B;

4、用户A取消关注用户B;

5、根据用户名称的首字母排序,分页获取用户的粉丝列表;

6、根据用户名称的首字母排序,分页获取用户的关注列表;

很明显社交网络是一种稀疏图。比如,微信中十亿级的用户,但平均每个用户的好友数量也就几百个。所以不适合采用邻接矩阵的存储方式,否则会浪费大量的存储空间。这里,我们采用邻接表来存储。

这个时候,又会发现一个问题。邻接表中可以存储A的关注关系,但是没办法表示A的被关注关系(即A的粉丝)。我们需要使用逆邻接表来存储这种被关注关系。对应到图上就是:邻接表中,每个顶点的链表中,存储的是这个顶点指向的顶点;逆邻接表中,每个顶点的链表中,存储的是指向这个顶点的顶点。

因此,如果要查找某个用户关注了哪些用户,我们可以在邻接表中查找;如果要查找某个用户被哪些用户关注了,可以从逆邻接表中查找。

基础的邻接表不适合快速判断两个用户之间是否是关注和被关注关系,所以需要改进。而且在关注/粉丝列表中还需要按照用户名称的首字母排序,所以用跳表这个数据结构再合适不过了。因为跳表的插入、删除以及查找操作都十分高效,时间复杂度都为O(logn),空间复杂度稍高,是O(n)。最重要的是,跳表中存储的数据本来就是有序的,分页获取粉丝列表或者关注列表就非常的高效了。

对于小规模的数据,比如小型社交网络中的几十万个用户,我们可以将整个社交关系存储到内存中,所以上面的解决思路是没有问题的。但是如果像微博这样亿级用户,数据规模太大,就无法将所有的社交关系存储到内存中了。这个时候就可以采取我们常用的分布式策略了:通过哈希算法等数据分片方式,将邻接表存储在不同的的机器上。例如下图中,在机器1上存储顶点1、2、3的邻接表,在机器2上,存储顶点4、5的邻接表。逆邻接表的处理方式也是一样的。

当要查询顶点与顶点之间的关系的时候,我们就利用同样的哈希算法,先定位顶点所在的机器上,然后再在相应的机器上查找。

对于数据量级较大的社交关系,我们还可以转变下思路,比如,使用外部存储(比如:硬盘),因为外部存储要比内部存储宽裕很多。数据库就是我们经常用来持久化存储关系数据的。

如下图的表所示,为了高效的支持前面定义的操作,我们可以在表上建立多个索引。


四、总结

1、图的基本概念、术语、图的分类;

2、图的两种主要存储方式:邻接矩阵和邻接表。

邻接矩阵存储方式的缺点是比较浪费空间,但是有点是查询效率高,而且方便矩阵运算。

邻接表中每个顶点都对应一个链表,存储与其相连接的其他顶点。尽管邻接表的存储方式比较节省存储空间,但链表不方便查找,所以查询效率没有邻接矩阵高。针对这个问题,邻接表可以将链表换成更加高效的动态数据结构进行优化,比如:平衡二叉查找树、跳表、散列表等等。


五、图的Java代码实现

5.1、图的抽象数据类型描述

// 图的抽象数据类型描述
public interface MyGraph {

	void createGraph();    // 创建图
	
	int getVexNum();       // 返回图中的顶点数
	
	int getEdgeNum();      // 返回图中的边数
	
	Object getVex(int v);  // 给定位置v,返回其对应的顶点值
	
	int locateVex(Object vex);     // 给定顶点的值vex,返回其在图中的位置
	
	int firstAdjVex(int v);        // 返回v的第一个邻接点
	
	int nextAdjVex(int v, int w);  // 返回v相对于w的下一个邻接点
}

5.2  图的邻接矩阵描述

// 图的类型
public enum GraphKind {

	UDG,   // 无向图(UnDirected  Graph)
	DG,    // 有向图(Directed  Graph)
	UDN,   // 无向网(UnDirected  NetWork),就是边有权重的无向图
	DN,    // 有向网(Directed  NetWork),就是边有权重的有向图
}
package com.zju.Graph;

import java.util.Scanner;

/**
 * @author 作者 pcwl
 * @date 创建时间:2018年12月3日 下午3:31:08
 * @version 1.0
 * @comment 图的邻接矩阵表示
 */
public class GraphDemo1 implements MyGraph {

	public final static int INFINITY = Integer.MAX_VALUE;
	private GraphKind kind; // 图的种类标志
	private int vexNum, edgeNum; // 图的当前顶点数和边数
	private Object[] vexs; // 顶点的集合
	private int[][] edges; // 边的集合

	public GraphDemo1() {
		this(null, 0, 0, null, null);
	}

	public GraphDemo1(GraphKind kind, int vexNum, int edgeNum, Object[] vexs, int[][] edges) {
		this.kind = kind;
		this.vexNum = vexNum;
		this.edgeNum = edgeNum;
		this.vexs = vexs;
		this.edges = edges;
	}

	// 图的创建算法 图的类型共有4种
	public void createGraph() {
		Scanner sc = new Scanner(System.in);
		System.out.println("请输入图的类型:(UDG、DG、UDN、DN)");
		GraphKind kind = GraphKind.valueOf(sc.next());
		switch (kind) {
		case UDG:
			createUDG();
			break;
		case DG:
			createDG();
			break;
		case UDN:
			createUDN();
			break;
		case DN:
			createDN();
			break;
		default:
			break;
		}
	}

	// 创建有向网
	private void createDN() {
		Scanner sc = new Scanner(System.in);
		System.out.println("请分别输出图的顶点数、图的边数:");
		vexNum = sc.nextInt();
		edgeNum = sc.nextInt();
		vexs = new Object[vexNum];
		System.out.println("请分别输出图的各个顶点:");
		for (int v = 0; v < vexNum; v++) {
			vexs[v] = sc.next();
		}
		edges = new int[vexNum][vexNum];
		// 初始化邻接矩阵
		for (int v = 0; v < vexNum; v++) {
			for (int u = 0; u < vexNum; u++) {
				edges[v][u] = INFINITY;
			}
		}
		System.out.println("请输入各个边的两个顶点及其权值");
		for (int k = 0; k < edgeNum; k++) {
			int v = locateVex(sc.next());
			int u = locateVex(sc.next());
			edges[v][u] = sc.nextInt();
		}
	}

	// 创建无向网
	private void createUDN() {
		Scanner sc = new Scanner(System.in);
		System.out.println("请分别输出图的顶点数、图的边数:");
		vexNum = sc.nextInt();
		edgeNum = sc.nextInt();
		vexs = new Object[vexNum];
		System.out.println("请分别输出图的各个顶点:");
		for (int v = 0; v < vexNum; v++) {
			vexs[v] = sc.next();
		}
		edges = new int[vexNum][vexNum];
		// 初始化邻接矩阵
		for (int v = 0; v < vexNum; v++) {
			for (int u = 0; u < vexNum; u++) {
				edges[v][u] = INFINITY;
			}
		}
		System.out.println("请输入各个边的两个顶点及其权值");
		for (int k = 0; k < edgeNum; k++) {
			int v = locateVex(sc.next());
			int u = locateVex(sc.next());
			edges[v][u] = edges[u][v] = sc.nextInt();
		}
	}

	// 创建有向图
	private void createDG() {
		// 略
	}

	
	// 创建无向图
	private void createUDG() {
		// 略
	}

	public int getVexNum() {
		return vexNum;
	}

	public int getEdgeNum() {
		return edgeNum;
	}

	// 根据顶点在数组中的位置返回顶点
	public Object getVex(int v) throws Exception {
		if (v < 0 && v >= vexNum)
			throw new Exception("第" + v + "个顶点不存在");
		return vexs[v];
	}

	// 顶点定位 根据顶点名返回其在数组中的位置
	public int locateVex(Object vex) {
		for (int v = 0; v < vexNum; v++) {
			if (vexs[v].equals(vex))
				return v;
		}
		return -1;
	}

	// 查找第一个邻接点
	public int firstAdjVex(int v) throws Exception {
		if (v < 0 && v >= vexNum)
			throw new Exception("第" + v + "个顶点不存在");

		for (int j = 0; j < vexNum; j++) {
			if (edges[v][j] != 0 && edges[v][j] < INFINITY) {
				return j;
			}
		}
		return -1;
	}

	// 查找下一个邻接点,已知顶点v以及一个邻接点w,返回v相对于w的下一个邻接点
	public int nextAdjVex(int v, int w) throws Exception {
		if (v < 0 && v >= vexNum)
			throw new Exception("第" + v + "个顶点不存在");

		for (int j = w + 1; j < vexNum; j++) {
			if (edges[v][j] != 0 && edges[v][j] < INFINITY) {
				return j;
			}
		}
		return -1;
	}

	public void display() {
		for (int i = 0; i < vexNum; i++) {
			for (int j = 0; j < vexNum; j++) {
				if (edges[i][j] == INFINITY)
					System.out.print("+ ");
				else
					System.out.print(edges[i][j] + " ");
			}
			System.out.println();
		}
	}
}

5.3  图的邻接表描述

package Graph;
import java.util.Scanner;

// 邻接表顶点结点结构
class VNode {

    private Object data; // 顶点信息

    private ArcNode firstArc; // 指向第一条依附于该顶点的弧

    public VNode() {
        this(null, null);
    }

    public VNode(Object data) {
        this(data, null);
    }

    public VNode(Object data, ArcNode firstArc) {
        this.data = data;
        this.firstArc = firstArc;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public ArcNode getFirstArc() {
        return firstArc;
    }

    public void setFirstArc(ArcNode firstArc) {
        this.firstArc = firstArc;
    }
}

// 邻接表 边(弧)的结点类
class ArcNode {

    private int adjVex; // 该弧所指向的顶点位置

    private int value; // 边的权值

    private ArcNode nextArc; // 指向下一条弧

    public ArcNode() {
        this(-1, 0, null);
    }

    public ArcNode(int adjVex) {
        this(adjVex, 0, null);
    }

    public ArcNode(int adjVex, int value) {
        this(adjVex, value, null);
    }

    public ArcNode(int adjVex, int value, ArcNode nextArc) {
        this.adjVex = adjVex;
        this.value = value;
        this.nextArc = nextArc;
    }

    public int getAdjVex() {
        return adjVex;
    }

    public void setAdjVex(int adjVex) {
        this.adjVex = adjVex;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public ArcNode getNextArc() {
        return nextArc;
    }

    public void setNextArc(ArcNode nextArc) {
        this.nextArc = nextArc;
    }
}

// 图的邻接表的描述 
public class ALGraph implements IGraph{ 

    private GraphKind kind; // 图的种类标志

    private int vexNum, arcNum; // 图的当前顶点数和边数

    private VNode[] vexs; // 顶点  

    public ALGraph(){
        this(null, 0, 0, null);
    }   


    public GraphKind getKind() {
        return kind;
    }

    public VNode[] getVexs() {
        return vexs;
    }


    public ALGraph(GraphKind kind, int vexNum, int arcNum, VNode[] vexs) {
        this.kind = kind;
        this.vexNum = vexNum;
        this.arcNum = arcNum;
        this.vexs = vexs;
    }

    // 图的创建算法 图的类型共有4种 
    public void createGraph() {
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入图的类型:(UDG、DG、UDN、DN)");
        GraphKind kind = GraphKind.valueOf(sc.next());
        switch (kind) {
        case UDG:
            createUDG();
            break;
        case DG:
            createDG();
            break;
        case UDN:
            createUDN();
            break;
        case DN:
            createDN();
            break;
        default:
            break;
        }
    }

    // 创建有向网
    private void createDN() {
        Scanner sc = new Scanner(System.in);
        System.out.println("请分别输出图的顶点数、图的边数:");
        vexNum = sc.nextInt();
        arcNum = sc.nextInt();
        vexs = new VNode[vexNum];
        System.out.println("请分别输出图的各个顶点:");
        for(int v = 0; v < vexNum; v++){
            vexs[v] = new VNode(sc.next());
        } 
        System.out.println("请输入各个边的两个顶点及其权值");
        for(int k = 0; k < arcNum; k++){
            int v = locateVex(sc.next()); //弧尾 指出的点
            int u = locateVex(sc.next()); //弧头 被指入的点
            int value = sc.nextInt();
            addArc(v, u, value);
        }
    }


    // 创建无向网 
    private void createUDN() { 
        Scanner sc = new Scanner(System.in);
        System.out.println("请分别输出图的顶点数、图的边数:");
        vexNum = sc.nextInt();
        arcNum = sc.nextInt();
        vexs = new VNode[vexNum];
        System.out.println("请分别输出图的各个顶点:");
        for(int v = 0; v < vexNum; v++){
            vexs[v] = new VNode(sc.next());
        } 
        System.out.println("请输入各个边的两个顶点及其权值");
        for(int k = 0; k < arcNum; k++){
            int v = locateVex(sc.next()); //弧尾 指出的点
            int u = locateVex(sc.next()); //弧头 被指入的点
            int value = sc.nextInt();
            addArc(v, u, value);
            addArc(u, v, value);
        }
    }


    // 在位置v、u顶点之间,添加一条弧,其权值为value  
    private void addArc(int v, int u, int value) {
        ArcNode arc = new ArcNode(u,value);
        arc.setNextArc(vexs[v].getFirstArc());
        vexs[v].setFirstArc(arc); 
    }


    // 创建有向图 
    private void createDG() {
        // 略 
    }


    // 创建无向图 
    private void createUDG() {
        // 略
    }

    public int getVexNum() {
        return vexNum;
    }

    public int getArcNum() { 
        return arcNum;
    }

    // 根据顶点在数组中的位置返回顶点  
    public Object getVex(int v) throws Exception {        
        if(v < 0 && v >= vexNum)
            throw new Exception("第" + v + "个顶点不存在");
        return vexs[v].getData();
    }

    // 顶点定位 根据顶点名返回其在数组中的位置  
    public int locateVex(Object vex) {  
        for(int v = 0; v < vexNum; v++){
            if(vexs[v].getData().equals(vex))
                return v;
        }
        return -1;
    } 

    // 查找第一个邻接点  若没有邻接点则返回-1 
    public int firstAdjVex(int v) throws Exception { 
        if(v < 0 && v >= vexNum)
            throw new Exception("第" + v + "个顶点不存在");

        VNode vex = vexs[v]; 
        if(vex.getFirstArc() != null)
            return vex.getFirstArc().getAdjVex();
        else 
            return -1; 
    }

    // @description 查找v的相对于w位置的邻接点的下一个邻接点,如 v-a-b-w-c(a,b,w,c都是v的邻接点),那么返回的就是c 
    public int nextAdjVex(int v, int w) throws Exception { 
        if(v < 0 && v >= vexNum)
            throw new Exception("第" + v + "个顶点不存在");

        VNode vex = vexs[v];
        ArcNode arcvw = null;
        for(ArcNode arc = vex.getFirstArc(); arc != null; arc = arc.getNextArc()){
            if(arc.getAdjVex() == w){
                arcvw = arc;
                break;
            }
        }
        if(arcvw != null && arcvw.getNextArc() != null)
            return arcvw.getNextArc().getAdjVex();
        else
            return -1;
    }   

    public void display() throws Exception{
        for(int i = 0; i < vexNum; i++){
            VNode vex = vexs[i];
            System.out.print(vex.getData());
            for(ArcNode arc = vex.getFirstArc(); arc != null; arc = arc.getNextArc()){
                System.out.print("->" + getVex(arc.getAdjVex()) +  arc.getValue());
            }
            System.out.println();
        }
    }
}

 


上一篇:堆的详解:https://blog.youkuaiyun.com/pcwl1206/article/details/84608381

推荐及参考:

1、图的基本概念:https://www.cnblogs.com/polly333/p/4760275.html

2、《数据结构与算法之美》之图篇:https://time.geekbang.org/column/article/70537

3、图的创建 (邻接矩阵+邻接表):https://blog.youkuaiyun.com/liuquan0071/article/details/50435570

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值