数据结构(三)——图

本文详细介绍了图数据结构,包括无向图和有向图的概念,以及深度优先搜索(DFS)和广度优先搜索(BFS)的区别与应用。此外,讨论了最小生成树的Prim和Kruskal算法,以及求解最短路径的Dijkstra算法。同时,文章提及了拓扑排序和强连通分量的Kosaraju算法,并对比了动态规划和贪心算法的联系与差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

无向图
  • 深度优先搜索查找(DFS):

    • 添加了一个数组edgeTo[]。这个数组可以找到每个与s连通的顶点回到s的路径。它会记住每个顶点到起点的路径,而不是记录当前顶点到起点的路径。为了做到这一点,在由边v-w第一次访问任意w时,将edgeTo[w]设为v来记住这条路径。换句话说,v-w是从s到我的路径上的最后一条已知的边。这样,搜索的结果是一棵以起点为根节点的树,edgeTo[]是一棵父链接表示的树。

    • 深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比

  • 广度优先搜索查找(BFS):

    • 深度优先搜索得到的路径不仅取决于图的结构,还取决于图的表示和递归调用的性质。广度优先搜索可以用来解决单点最短路径问题。深度优先搜索就好像是一个人在走迷宫,广度优先搜索则好像是一组人在一起朝各个方向走这个迷宫,每个人都有自己的绳子。当出现新的的叉路时,可以假设一个探索者可以分裂为更多的人来搜索它们,当两个探索者相遇时,会合二为一(并继续使用先到达者的绳子)。在程序中,在搜索一幅图时遇到有多条边需要遍历的情况时,我们会选择其中一条并将其他通道留到以后再继续搜索。在深度优先搜索中,我们我们用了一个可以下压的栈(这是由系统管理的,以支持递归搜索方法)。使用LIFO(后进后出)的规则来描述压栈和走迷宫先探索相邻的通道相似。从有待搜索的通道中选择最晚遇到过的那条。在广度优先搜索中,我们希望按照与起点的距离的顺序来遍历所有顶点,看起来这种顺序很容易实现:使用(FIFO,先进先出)队列来代替栈(LIFO,后进先出)即可。我们将从有待搜索的通道中选择最早遇到的那条。

    • 对于从s可达的任意顶点顶点v,广度优先搜索都能找到一条从s到v的最短路径(没有其他从s到v的路径所含的边比这条路径更少)。广度优先搜索所需的时间在最坏情况下和V+E成正比。

    • 这两个算法的不同之处仅在于从数据结构中获取下一个顶点的规则(对于广度优先搜索来说是最早加入的顶点,对于深度优先搜索来说是最晚加入的顶点)。

有向图
  • 拓扑排序:

    • 给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排到后面的元素(或者说明无法做到这一点)。如果一个有优先级限制的问题中存在有向环,那么这个问题肯定是无解的。

    • 有向图中基于深度优先搜索的顶点排序:

      	private Queue<Integer> pre; //所有顶点的前序排列
      	private Queue<Integer> post; //所有顶点的后序排列
      	private Stack<Integer> reversePost; //所有顶点的逆后序排列,即拓扑排序
      	public DepthFirstOrder(Digraph G)
      	{
      	pre         = new Queue<Integer>();
      	post        = new Queue<Integer>();
      	reversePost = new Stack<Integer>();
      	marked = new boolean[G.V()];
      	for(int v = 0;v < G.V();v++)
      	if(!marked[v])dfs(G,v);
      	}
      	private void dfs(Digraph G,int v)
      	{
      	pre.enqueue(v);
      	marked[v] = true;
      	for(int w : G.adj(v))
      	if(!Marked[w])
      	dfs(G,w);
      	post.enqueue(v);
      	reversePost.push(v);
      	}
      
    • 一幅有向无环图的拓扑顺序即为所有顶点的逆后序排列。

  • Kosaraju算法

    • 为了找到所有强连通分量,它会在反向图中进行深度优先搜索来将顶点排序(搜索顺序的逆后序),在给定有向图中用这个顺序再进行一次深度优先搜索。

    • 有向图中的连通性:
      如果两个顶点v和w是互相可达的,则称它们为强连通的。两个顶点是强连通当且仅当它们都在一个普通的有向环中。

    • 实现:

     private int[] id; //强连通分量的标识符
 			public kosarajuSCC(Digraph G)
 			{
 			marked = new boolean[G.V()];
 			id = new int[G.V()];
 			DepthFirstOrder order = new DepthFirstOrder(G.reverse);
 			for(int s : order.reversePost())
 			If(!marked[s]){dfs(G,s);count++;}
 			}
 			private void dfs(Digraph G,int v)
 			{
 			marked[v] = true;
 			id[v] = count;
 			for(int w : G.adj(v))if(!marked[w])dfs(G,w);
 			}
 			public boolean stronglyConnected(int v,int w)
 			{return id[v] == id[w];}
最小生成树
  • Prim(普里姆)算法
    • 它的每一步都会为一棵生长中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入树中(即由树中的顶点所定义的切分中的一条横切边)

    • 实现:

       private boolean[] marked; //最小生成树的顶点
   			private Queue<Edge> mst; //最小生成树的边
   			private MinPQ<Edge> pq; //横切边(包括失效的边)
   			public LazyPrimMST(EdgeWeightedGraph G)
   			{
   			pq = new MinPQ<Edge>();
   			marked = new boolean[G.V()];
   			mst = new Queue<Edge>();
   			visit(G,0); //假设G是连通的
   			while(!Pq.isEmpty())
   			{
   			Edge e = pq.delMin(); //从pq中得到权重最小的边
   			int v = e.either(),w = e.other(v);
   			if(marked[v]&&marked[w])continue; //跳过失效的边
   			mst.enqueue(e); //将边添加到树中
   			If(!marked[v])visit(G,v); //将顶点(v或w)添加到树中
   			if(!marked[w])visit(G,v); 
   			}
   			}
   			private void visit(EdgeWeightedGraph G,int v)
   			{ //标记顶点v并将所有连接v和未被标记顶点的边加入pq
   			marked[v] = true;
   			for(Edge e : G.adj(v))
   			If(!marked[e.other(v)])pq.insert(e);
   			}
  • Kruskal(克鲁斯卡尔)算法
    • 该算法的主要思想是按照边的权重顺序(从小到大)处理它们,将边加入最小生成树,加入的边不会与已经加入的边构成成环,直到树中含有V-1条边为止。
      这些边逐渐由一片森林合并为一棵树,也就是图的最小生成树。

    • Kruskal算法一般还是要比Prim算法要慢,因为在处理每条边时除了两种算法都要完成的优先队列操作之外,它还需要进行一次connect操作。

    • 实现:

        private Queue<Edge> mst;
				public KruskalMST(EdgeWeightedGraph G)
				{
				mst = new Queue<Edge>();
				minPQ<Edge> pq = new MinPQ<Edge>();
				for(Edge e : G.edges())pq.insert(e);
				UF uf = new UF(G.V());
				while(!pq.isEmpty() && mst.size() < G.V()-1)
				{
				Edge e = pq.delMin(); //
				int v = e.either(),w = e.other(v);
				if(uf.connected(v,w))continue; //
				uf.union(v,w); //
				mst.enqueue(e); //
				}
				}
  • Prim和Kruskal算法不能处理有向图问题,有向图问题是更加困难的最小树形问题
最短路径
  • 边的松弛:
    放松边v->w意味着检查从s到w的最短路径是否先从s到v,然后再由v到w。如果是,则根据这个情况更新数据结构的内容。其中由v到达w的最短路径是distTo[w]与e.weight()之和——如果这个值不小于distTo[w],称这条边失效了并将它忽略;如果这个值更小,就更新数据。点的松弛是类似的(对该点所连接的边进行操作)。

  • Dijkstra(迪克斯特拉)算法

    • Dijkstra算法能够解决边权重非负的加权有向图的单起点最短路径

    • 这个算法主要分为两种情况:要么边的to()得到的顶点还不在优先队列中,此时需要使用insert()方法将它加入到优先队列中;要么它已经在优先队列中且优先级(即到该点的总路径长度)需要降低,此时可以用change()方法实现

    • Prim算法每次添加的都是离树最近的非树顶点,Dijkstra算法每次添加的都是离起点最近的非树顶点。

  • 无环加权有向图中的最短路径算法

    • 许多应用中的加权有向图都是不含有有向环的。而该算法比Dijkstra算法更快、更简单。它能在线性时间(E+V)内解决单点最短路径问题; 能够解决负权重的边;能够解决相关的问题,例如找出最长的路径。

    • 首先,将distTo[s]初始化为0,其他distTo[]元素初始化为无穷大,然后一个一个地按照拓扑顺序放松所有顶点。

other

概念性的东西
  • 关于线性与顺序

    • 线性是线性,顺序是顺序,线性是逻辑结构,顺序是存储结构,两者不是一个概念,线性是指一个元素后继只有唯一的一个元素或节点,非线性是一个元素后面可以有多个后继或前继节点,顺序是指存储结构连续,例如数组是顺序的,链表不是顺序的,但他们都是线性的。当然顺序也可以是非线性的,例如顺序结构存储非线性结构的二叉树!!!
  • 二叉链表存储结构

    • 以二叉链表作为树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点。所以根节点的右指针指向根节点的右兄弟,根节点没有兄弟节点,因此为空
  • 树是图的特例,不能为空;空树是二叉树的特例,即二叉树可以为空。

键索引计数法
		int N = a.length;
		String[] aux = new String[N];
		int[] count = new int[R+1];
		//计算出现频率
		for(int i = 0;i < N;i++)
		count(a[i].key()+1]++;
		//将频率转换为索引
		for(int r = 0;r < R;r++)
		count[r+1] += count[r];
		//将元素分类
		for(int i = 0;i < N;i++)
		aux[count[a[i].key()]++] = a[i];
		//回写
		for(int i = 0;i < N;i++)
		a[i] = aux[i];
  • 键索引计数法是一种对于小整数键排序非常有效却常常被忽略的排序算法

  • 低、高位优先的字符串排序便是基于键索引计数法的

KMP算法
		void createNext(const char* str, int* next, int length)
		{
		    int i = 1, k = 0;
		    next[0] = 0;//为0则表示不存在相同的最长前缀和最长后缀

		    while(i < length)
		    {
		        while(k>0 && str[i] != str[k])
		            k = next[k-1];//如果前面k>0,而当前不匹配,则回溯
		        if(str[i] == str[k])
		            k++;//匹配到相同的,k+1
		        next[i] = k;//真正给next赋值的地方
		        i++;
		    }
		}
		int Kmp(const char *src, int slen, const char *str, int len)
		{
		    int *next = new int[len];
		    createNext(str, next, len);

		    int i = 0;
		    int k = 0;
		    while(i < slen && k < len)
		    {
		        if(src[i] == str[k])
		            i++,k++;//匹配则两个同时向后移动
		        else if(k>0)
		            k = next[k-1];//匹配失败且k>0,则回溯
		        else
		            i++;//若匹配失败且k=0,则往下寻找匹配字符
		    }

		    delete[] next;
		    if(k == len)//寻找到需要的子串
		        return i - len;
		    else
		        return -1;
		}
动态规划和贪心算法的联系与区别

动态规划和贪心算法都是一种递推算法 ,均由局部最优解来推导全局最优解

不同点:
贪心算法:
1.贪心算法中,作出的每步贪心决策都无法改变,因为贪心策略是由上一步的最优解推导下一步的最优解,而上一步之前的最优解则不作保留。
2.由(1)中的介绍,可以知道贪心法正确的条件是:每一步的最优解一定包含上一步的最优解。

	该算法存在问题:   
			1.   不能保证求得的最后解是最佳的;   
			2.   不能用来求最大或最小解问题;   
			3.   只能求满足某些约束条件的可行解的范围。实现该算法的过程:   

动态规划算法:
1.全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有最优解
2.动态规划的关键是状态转移方程,即如何由以求出的局部最优解来推导全局最优解
3.边界条件:即最简单的,可以直接得出的局部最优解

	通过动态规划算法解决多段图最短路径规划问题的思想是,我们要计算每一个子段图到下一个子段图的所有可能的连接情况,并保留每种可能选择中最短的那个连接。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值