一看就懂的数据结构:图

图是一种比较复杂的数据结构,设计图的算法有很多,如图的搜索,最短路径,最小生成树等,这篇博客主要和大家讨论图的定义和如何去表示,相信看完本篇之后,也会对图有一个基本的了解。

图的定义

之前我们讨论过树,树是一种非线性表,今天所讨论的图也是一种非线性表。不过和树相比,图更加的复杂。树中的元素称为节点,对应的,图中的元素称为顶点(vertex)。图中的一个定点可以与任意其他顶点建立连接关系,我们把这种关系称为边(edge)

其实在我们生活中,就有很多符合图这种结构的例子。例如,我们的社交网络就是一个非常典型的图结构。就比如说我们用的微信,我们可以把每个用户看成一个顶点,如果两个用户互相加好友,就在对应的两个节点之间建立一条边。因此,微信的整个好友关系可以用一张图来进行表示。其中,每个用户有多少好友,对应的图中,就称为顶点的度(degree),也就是与顶点相连接的边的条数。

可以微博大家都知道,他的社交关系和微信又有些不同。微博允许单向关注,也就是说,用户A可能关注了用户B,但是用户B没有关注用户A,那么这种单向的社交关系,又该如何表示呢?

我们可以将图中的边,引入“方向”这个概念。如果用户A关注了用户B,那么就从图中画一条从A指向B的箭头,如果互相关注,那么我们就画两条边。我们把这种有方向的图称为有向图,相反,没有方向的就称为无向图。

 

无向图中顶点的“度”表示此顶点有多少条边与其相连。在有向图中,我们把度分为入度(in-degree)和出度(out-degree)。

顶点的入度,表示有多少条边指向这个顶点;顶点的出度,是指有多少条边是以这个顶点为起点指向其他的顶点。对应这个例子,就是微博中的粉丝量,入度表示有多少粉丝,出度就是你关注了哪些人。

现在再来看另一个社交软件QQ,QQ与微信相比,功能更加的复杂,大家可能关注到过亲密度这个功能。那么对应到图中,这个图就叫做带权图(weighted graph)。在带权图中,每条边都有一个权重(weight),我们用权重来表示QQ好友间的亲密度。

 

邻接矩阵的存储方法

知道了图的基本概念,我们就再来看看内存中如何存储图这种数据结构。

图最直观的一种存储方式为邻接矩阵(adjacency matrix)。学过线性代数的同学,应该都知道矩阵这个概念。邻接矩阵底层依赖二维数组。对于无向图来说,如果顶点i和顶点j之间有边,我们就将A[i][j]和A[j][i]标记为1;对于有向图,如果有一条从顶点i指向顶点j的边,我们就将A[i][j]标记为1,同理,如果有一条从顶点j指向顶点i的边,我们就将A[j][i]标记为1。对于带权图,数组中存储相应的权重。

 

现在来看看代码实现

public class Graph{
   private int v;
   private boolean matrix[][];
   
   public Graph(int v){
      this.v = v;
      matrix = new boolean[v][v]; 
      for(int i = 0;i < v;i++){
         for(int j = 0;j < v;j++){
            matrix[i][j] = false;
         }
      }
   }
   
   public void addEdge(int s,int t){
      //无向图
      matrix[s][t] = true;
      matrix[t][s] = true;
   }
}

 因为邻接矩阵底层依赖数组,所以,在邻接矩阵中,获取两个顶点之间的关系的操作相当。这种存储的另一个好处就是可以把图中的很多运算转换为矩阵运算,方便计算。

用邻接矩阵来表示一个图,虽然简单、直观,但是比较浪费存储空间。为什么这么说呢?;
对于无向图来说,如果 A[i][j] 等于 1,那 A[j][i] 也肯定等于 1。实际上,我们只需要存储 一个就可以了。也就是说,无向图的二维数组中,如果我们将其用对角线划分为上下两部 分,那我们只需要利用上面或者下面这样一半的空间就足够了,另外一半白白浪费掉了。
还有,如果我们存储的是 稀疏图 (Sparse Matrix),也就是说,顶点很多,但每个顶点的 边并不多,那邻接矩阵的存储方法就更加浪费空间了。比如微信有好几亿的用户,对应到图 上就是好几亿的顶点。但是每个用户的好友并不会很多,一般也就三五百个而已。如果我们 用邻接矩阵来存储,那绝大部分的存储空间都被浪费了。
但这也并不是说,邻接矩阵的存储方法就完全没有优点。首先,邻接矩阵的存储方式简单、 直接,因为基于数组,所以在获取两个顶点的关系时,就非常高效其次,用邻接矩阵存储 图的另外一个好处是方便计算。这是因为,用邻接矩阵的方式存储图,可以将很多图的运算 转换成矩阵之间的运算。比如求解最短路径问题时会提到一个 Floyd-Warshall 算法 ,就是 利用矩阵循环相乘若干次得到结果。

邻接表的存储方法

针对邻接矩阵的存储比较浪费空间的问题,我们来看另一种图的存储方法:邻接表(adjacency list)。
邻接表有点像我们之前讨论的哈希表。他也是每一个节点对应一个链表。对于有向图,每个顶点对应的链表存储的是他指向的顶点。对于无向图,每个顶点的链表存储的是与这个顶点有边相连的顶点。

接下来用代码实现一下

public class Graph{
   private int v;
   private LinkedList<Integer>[] adj;//邻接表

   public Graph(int v){
     this.v = v;
     adj = new LinkedList<>[v];
     for(int i = 0;i < v;i++){
         adj[i] = new LinkedList<>[];
      }
   }

   public vid addEdge(int s,int t){
      //无向图的一条边存储两次
      adj[s].add[t];
      adj[t].add[s];    
   }
}

 之前我们还讨论过时间复杂度和空间复杂度互换的设计思想。邻接矩阵存储起来比较浪费空间,但是使用起来比较高效。相反,邻接表存储起来比较节省空间,但是使用起来就没有那么高效。

在之前讨论哈希表的时候,我们就提到过,基于链表法解决冲突的哈希表,对于链表过长,我们可以将链表转换为红黑树。

聊一聊别的

有了对于堆的基础知识,那我们来看看微博,微信等社交网络中的好友关系是如何存储的。

前面我们讨论到,微博,微信用的是两种不同的图,前者是有向图,后者是无向图,但是在解决思路上,确实差不多,下面先看看微博来进行举例。

针对微博的用户关系,我们假设需要支持下面几个操作。

# 判断用户 A 是否关注了用户 B;
# 判断用户 A 是否是用户 B 的粉丝;
# 用户 A 关注用户 B;
# 用户 A 取消关注用户 B;
# 根据用户名称的首字母排序,分页获取用户的粉丝列表;
# 根据用户名称的首字母排序,分页获取用户的关注列表。
关于如何存储一个图,我们讨论过两种方法:邻接矩阵和邻接表。因为社交网络是一张稀疏图,使用邻接矩阵存储比较浪费存储空间,所以,我们这里选用邻接表。
在邻接表中,查找某个用户关注了哪些用户很容易,但是,要想查找某个用户被哪些用户关注了,确是非常困难,所以,一张邻接表是不够的,我们还需要一张逆邻接表。
 

如上图,邻接表中存储了用户的关注关系,逆邻接表中存储用户的被关注关系。对应到图上,在邻接表中,每个顶点对应的链表存储这个顶点指向的顶点;在逆邻接表中,每个顶点对应的链表存储指向这个顶点的顶点。在邻接表中查找某个用户关注了哪些用户,在逆邻接表中查找某个用户被哪些用户关注。

基础的邻接表不适合判断两个用户之间是否关注与被关注的关系,因此我们将邻接表中的链表改改为支持快速查找的动态数据结构,但是选择哪种动态数据结构,又是一个问题?红黑树,跳表,有序动态数组还是哈希表呢?

因为我们需要按照用户名称的首字母排序,分页来获取用户的粉丝列表或者关注列表,用跳 表这种结构再合适不过了。这是因为,跳表插入、删除、查找都非常高效,时间复杂度是 O(logn),空间复杂度上稍高,是 O(n)。最重要的一点,跳表中存储的数据本来就是有序 的了,分页获取粉丝列表或关注列表,就非常高效。
如果对于小规模的数据,比如社交网络中只有几万、几十万个用户,我们可以将整个社交关 系存储在内存中,上面的解决思路是没有问题的。但是如果像微博那样有上亿的用户,数据 规模太大,我们就无法全部存储在内存中了。
这个时候该怎么办呢?我们可以通过哈希算法等数据分片方式,将邻接表存储在不同的机器上。你可以看下面这幅 图,我们在机器 1 上存储顶点 1,2,3 的邻接表,在机器 2 上,存储顶点 4,5 的邻接 表。逆邻接表的处理方式也一样。当要查询顶点与顶点关系的时候,我们就利用同样的哈希 算法,先定位顶点所在的机器,然后再在相应的机器上查找

 

除此之外,我们还有另外一种解决思路,就是利用外部存储(比如硬盘),因为外部存储的
存储空间要比内存会宽裕很多。数据库是我们经常用来持久化存储关系数据的,所以我这里
介绍一种数据库的存储方式。
我用下面这张表来存储这样一个图。为了高效地支持前面定义的操作,我们可以在表上建立
多个索引,比如第一列、第二列,给这两列都建立索引

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值