数据结构与算法(四)——图的简单应用

一、图的定义

树是一种特殊的图,图比树更加复杂,因为图中的每个顶点都可以与其他顶点相连。

图又分有向图无向图。无向图的边没有箭头指向,是双向的;而有向图的边是有箭头指向的,是单向的。边上有权值的图称为带权图。

在这里插入图片描述
在这里插入图片描述

二、图的存储

图的存储方式一般有两种:邻接矩阵邻接表

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;
}

猜你喜欢

转载自blog.csdn.net/Longstar_L/article/details/108870489