图的概念
树中的元素称为节点,图中的元素称为作顶点Vertex。
图中的一个顶点可以与任意其他顶点建立连接关系,这种建立的关系叫作边Edge。
跟顶点相连接的边的条数叫作顶点的度Degree。
边有方向的图叫作有向图,边没有方向的图就叫作无向图。
无向图中度表示一个顶点有多少条边,在有向图中,把度分为入度In-Degree和出度Out-Degree。
入度表示有多少条边指向这个顶点,出度表示有多少条边是以这个顶点为起点指向其他顶点。
每条边都有一个权重Weighted,这种图被称为带权图。
图的存储方法
邻接矩阵 Adjacency Matrix
邻接矩阵的底层依赖一个二维数组。
对于无向图来说,如果顶点 i 与顶点 j 之间有边,我们就将 A[i][j] 和 A[j][i] 标记为 1。
对于有向图来说,如果顶点 i 到顶点 j 之间,有一条箭头从顶点 i 指向顶点 j 的边,那我们就将 A[i][j]标记为 1。如果有一条箭头从顶点 j 指向顶点 i 的边,我们就将 A[j][i] 标记为 1。
对于带权图,数组中就存储相应的权重。
邻接矩阵的优缺点
顶点很多,每个顶点的边并不多的图被称为稀疏图 Sparse Matrix,稀疏图使用邻接矩阵的存储方法其绝大部分的存储空间都被浪费了。
邻接矩阵在获取两个顶点的关系时非常高效,可以将很多图的运算转换成矩阵之间的运算。
邻接表存储方法 Adjacency List
邻接表有点像散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点。
邻接矩阵存储起来比较浪费空间,使用比较节省时间。
邻接表存储起来比较节省空间,使用比较较消耗时间。
邻接表的改进
链表的存储方式对缓存不友好,在邻接表中查询两个顶点之间的关系不高效。
但可以将邻接表中的链表改成平衡二叉查找树、跳表、散列表或者红黑树,这样就可以更加快速地查找两个顶点之间是否存在边。
除此之外,还可以将链表改成有序动态数组通过二分查找的方法来快速定位两个顶点之间否是存在边。
如何存储微博、微信等社交网络中的好友关系?
针对微博用户关系,假设我们需要支持下面这样几个操作:
①判断用户 A 是否关注了用户 B
②判断用户 A 是否是用户 B 的粉丝
③用户 A 关注用户 B
④用户 A 取消关注用户 B
⑤根据用户名称的首字母排序,分页获取用户的粉丝列表
⑥根据用户名称的首字母排序,分页获取用户的关注列表
社交网络是一张稀疏图,使用邻接矩阵存储比较浪费存储空间,所以应该采用邻接表来存储。
邻接表可以查找某个用户关注了哪些用户,要想知道某个用户被哪些用户关注,需要一个逆邻接表。
邻接表中存储了用户的关注关系,逆邻接表中存储的是用户的被关注关系。
邻接表的链表中存储该顶点指向的顶点,逆邻接表中的链表中,存储的是指向该顶点的顶点。
在邻接表中查找用户关注了哪些用户。从逆邻接表中查找用户被哪些用户关注。
按照用户名称的首字母排序,分页来获取用户的粉丝列表或者关注列表这种情况适合适合使用跳表。
如果像微博那样有上亿的用户,数据规模太大可以通过哈希算法等数据分片方式,将邻接表存储在不同的机器上。
当要查询顶点与顶点关系的时候,利用同样的哈希算法,先定位顶点所在的机器,然后再在相应的机器上查找。
图的代码实现
public class Graph{ private int numVertex;//顶点数 private LinkedList<int>[] adj;//邻接表 public Graph(int numVertex){ //初始化顶点数,邻接表 this.numVertex = numVertex; adj = new LinkedList<int>[numVertex]; for (int i = 0; i < numVertex; i++) adj[i] = new LinkedList<int>(); } public void AddEdge(int s,int t){//向图加入一条边 adj[s].AddLast(t); adj[t].AddLast(s); } }
广度优先搜索(BFS)
先查找离起始顶点最近的,然后是次近的,依次往外搜索。
public void BFS(int s){ bool[] visited = new bool[numVertex]; //访问标志位数组 visited[s] = true;//将顶点设为已访问 Queue<int> queue = new Queue<int>(); queue.Enqueue(s);//将顶点s入队 while (queue.Count != 0){ //遍历队列直到队列为空 int temp = queue.Dequeue();//获取并打印出队顶点 Console.Write($"{temp},"); LinkedListNode<int> p = adj[temp].First;//出队顶点的链表头结点 while (p != null){//循环遍历链表 if (!visited[p.Value]){ visited[p.Value] = true;//改变标志位 queue.Enqueue(p.Value);//将其入队 } p = p.Next; } } }
深度优先搜索(DFS)
深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过程,非常适合用递归来实现。
public void DFS(int s){ bool[] visited = new bool[numVertex];//访问标志位数组 DFSAL(s,visited);//调用方法 } public void DFSAL(int i,bool[] visited){ visited[i] = true;//改变标志位 Console.Write($"{i},");//打印顶点 LinkedListNode<int> p = adj[i].First;//顶点链表的头结点 while (p != null){ //循环遍历链表 if (!visited[p.Value])//递归调用未被访问的顶点 DFSAL(p.Value, visited); p = p.Next; } }
测试两种搜索算法
//Main方法 Graph g = new Graph(8); g.AddEdge(0, 1); g.AddEdge(1, 2); g.AddEdge(1, 4); g.AddEdge(2, 3); g.AddEdge(2, 7); g.AddEdge(3, 4); g.AddEdge(4, 5); g.AddEdge(5, 6); g.AddEdge(6, 7); g.BFS(0); Console.WriteLine(); g.DFS(0); //c测试结果 0,1,2,4,3,7,5,6, 0,1,2,3,4,5,6,7,
内容小结
广度优先搜索和深度优先搜索是图上的两种最常用、最基本的搜索算法.简单粗暴没有优化,也被叫作暴力搜索算法,这两种搜索算法仅适用于图不大的搜索。
广度优先搜索是地毯式层层推进,从起始顶点开始依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。
深度优先搜索用的是回溯思想,非常适合用递归实现。深度优先搜索是借助栈来实现的。