一、图的定义
树是一种特殊的图,图比树更加复杂,因为图中的每个顶点都可以与其他顶点相连。
图又分有向图和无向图。无向图的边没有箭头指向,是双向的;而有向图的边是有箭头指向的,是单向的。边上有权值的图称为带权图。
二、图的存储
图的存储方式一般有两种:邻接矩阵和邻接表。
1、邻接矩阵
邻接矩阵使用二维数组存储,假设图中的顶点数为n,则这是一个 n * n 的二维数组。对于非带权图来说,如果顶点 i 到顶点 j 存在指向的边,则二维数组 [i] [j] 位置赋值为1;如果是带权值的图,则二维数组 [i] [j] 位置赋值为相应的权值。
如果图的边数比较少,或者说这是一个稀疏图,那么用邻接矩阵存储的时候,显然大量的位置都是0,这很浪费空间。
2、邻接表
邻接表解决了稀疏图用邻接矩阵存储时大量浪费空间的问题。邻接表由顶点表和链表组成,与散列表非常相似,每个顶点对应的链表里面,存储的是该顶点指向的顶点。链表的存储方式对缓存并不友好,而且如果想要查询某个顶点的所有出度节点,就需要遍历整个链表,所以使用邻接表查询比邻接矩阵更耗时。
// 无向图
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 void addEdge(int s, int t) {
adj[s].add(t);
adj[t].add(s);
}
}
借鉴散列表的处理办法,用红黑树、跳表等动态数据结构来替代链表,可以提升邻接表的查询效率。
三、图的简单应用
微博的人际关系如何用图存储?
在微博这种社交媒体中,存在关注与被关注的用户关系,更像是一张有向图。由于微博用户众多,且属于稀疏图,因此采用邻接表来存储更合理。当A关注了B,则存在A指向B的边。
用图操作微博用户关系:
- 判断用户 A 是否关注了用户 B;=============判断顶点A有没有指向顶点B的边
- 判断用户 B 是否被用户 A 关注 ;=============判断顶点B有没有一条从顶点A指向的边
- 用户 A 关注用户 B;=============新增顶点A有指向顶点B的边
- 用户 A 取消关注用户 B;=============删除顶点A指向顶点B的边
- 根据用户名称的首字母排序,分页获取用户 A 的粉丝列表;=============遍历逆邻接表中顶点A的链表
- 根据用户名称的首字母排序,分页获取用户 A 的关注列表。=============遍历邻接表中顶点A的链表
其中,邻接表用于存储用户的关注关系,即关注列表;逆邻接表用于存储用户的被关注关系,即粉丝列表。
逆邻接表可以通过遍历邻接表获得。
对于分页获取某用户的关注列表,并且要按照姓名首字母排序,如果邻接表的边表仅用链表实现,将会比较耗时。可以将链表升级为跳表,因为跳表的底层链表是有序的,并且适合范围查询,插入、删除也很高效,时间复杂度为O(logn),空间复杂度为O(n)。
此外,对于大规模数据,无法全部存储在内存中,一般可以将其持久化到数据库中。
比如一列存储用户id,一列存储粉丝id,在两列上都建立非唯一索引,那么就可以方便地查询某用户的粉丝列表和关注列表。如果这一查询操作很频繁,那么还可以考虑为这两列建立联合索引,这样可以省去回表过程。但同时还需要考虑联合索引的排序问题。如果查询用户粉丝列表更频繁,就采用(user_id, follower_id)的联合索引,因为建立这个索引之后,能使用的索引是user_id和(user_id, follower_id),单独的follower_id不能使用索引,而如果对follower_id还有额外的需求,那么再考虑为follower_id单独建索引;如果查询关注列表更频繁,可以采用(follower_id, user_id)的联合索引。
四、图的遍历
1、深度优先搜索
代码实现:
public void dfs(int s, int t){
boolean found = false;
boolean[] visited = new boolean[v];
//初始化被访问数组
for(int i = 0; i < v; ++i){
visited[i] = false;
}
//定义一个路径数组
int[] pre = new int[v];
for(int i = 0; i < v; ++i){
pre[i] = -1;
}
recurDfs(s,t,found, visited,pre);
print(pre,s,t);
}
public void recurDfs(int w, int t, boolean found, boolean[] visited, int[] pre){
//每次先判断是否已经找到
if(found == true) return;
//先访问当前的w
visited[w] = true;
//判断w是否已经到达t
if(w == t){
//如果已经到达,则直接返回
found = true;
return;
}
//否则,依次递归访问w的出度
for(int i = 0; i < adj[w].size(); ++i){
//取出出度
int q = adj[w].get(i);
if(!visited[q]){
//递归访问
pre[q] = w;
recurDfs(q,t,found,visited,pre);
}
}
}
2、广度优先搜索
代码实现:
public void bfs(int s, int t){
boolean[] visited = new boolean[v];
//初始化被访问数组
for(int i = 0; i < v; ++i){
visited[i] = false;
}
//定义一个路径数组
int[] pre = new int[v];
for(int i = 0; i < v; ++i){
pre[i] = -1;
}
//定义一个队列
Queue<Integer> queue = new Queue<>();
//首先访问源节点
visited[s] = true;
//将源节点入队
queue.add(s);
//当队列不空时
while(queue.size()>0){
//出队一个节点
int w = queue.poll();
//访问以该节点为顶点的链表
for(int i = 0; i < adj[w].size(); ++i){
int q = adj[w].get(i);
//如果这个节点没有被访问过
if(!visited[q]){
pre[q] = w;
if(q == t){
print(pre, s, t);
//结束
return;
}
//访问它
visited[q] = true;
//将其加入到队列中
queue.add(q);
}
}
}
}
public void print(int[] pre, int s, int t){
//递归打印,递归结束的条件是t==s且pre[t]==-1
if(pre[t] != -1 && t != s){
print(pre,s,pre[t]);
}
System.out.print(t + " ");
}
3、图的搜索应用
在社交网络中,经常会推荐“可能认识的人”。这其实可以通过对图进行广度优先搜索来实现,因为广度优先搜索属于层次遍历。
比如,想要找出社交网络中某个用户的三度好友关系,只需要找到与该用户之间的边数小于等于3的用户即可。
代码实现:
public void bfs(int s){
boolean[] visited = new boolean[v];
//初始化被访问数组
for(int i = 0; i < v; ++i){
visited[i] = false;
}
//定义一个路径数组
int[] pre = new int[v];
for(int i = 0; i < v; ++i){
pre[i] = -1;
}
//定义一个队列
Queue<Integer> queue = new Queue<>();
//首先访问源节点
visited[s] = true;
//将源节点入队
queue.add(s);
//当队列不空时
while(queue.size()>0){
//出队一个节点
int w = queue.poll();
//访问以该节点为顶点的链表
for(int i = 0; i < adj[w].size(); ++i){
int q = adj[w].get(i);
//如果这个节点没有被访问过
if(!visited[q]){
pre[q] = w;
//判断当前节点和源节点之间的边数,如果小于等于3,则打印
if(distance(pre,s,q) <= 3){
System.out.print(q + " ");
}
//访问它
visited[q] = true;
//将其加入到队列中
queue.add(q);
}
}
}
}
//判断当前节点与源节点之间的边数
public int distance(int[] pre, int s ,int w){
int i = w;
int count = 0;
while(pre[i] != -1){
++count;
i = pre[i];
}
return count;
}