IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 一看就懂的数据结构:图 -> 正文阅读

[数据结构与算法]一看就懂的数据结构:图

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

图的定义

之前我们讨论过树,树是一种非线性表,今天所讨论的图也是一种非线性表。不过和树相比,图更加的复杂。树中的元素称为节点,对应的,图中的元素称为顶点(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 的邻接 表。逆邻接表的处理方式也一样。当要查询顶点与顶点关系的时候,我们就利用同样的哈希 算法,先定位顶点所在的机器,然后再在相应的机器上查找

?

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

?

?

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-10-13 22:28:03  更:2021-10-13 22:28:18 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 6:45:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码