带你用广度优先搜索实现地铁计费功能

1、地铁是如何实现收费的?

我们平时坐地铁去上班的时候,是否有想过,地铁是怎么计费的呢?因为地铁的线路都比较复杂,一般大城市的地铁网都是错综复杂的,比如广州,目前就有15条线路,假设一条线有20个站点,那么整个地铁网络就会有300个站点,因为地铁可以换乘,从一个起点到达一个终点,会有很多路线。我们都知道,地铁的收费是根据距离来决定的(我们这里先假设每两个站的距离都是相等的),也就是站点越多,收费就越多,通常来说,地铁从一个起点到另外一个点,都是按照最短路径来收费的(不可能按照最长吧?),如何找到这个最短路径呢?我们首先来看看以下简图:
在这里插入图片描述

1.1 数组实现方式

首先来看起点站和目的地是同一条地铁线的情况,比如从A到C站,这种计算距离是十分简单的,我们只要把整条一号线的站点组合成一个数组,就可以计算从其中某个站点到另一个站点的路程。如上图的从A到C,假设A站的id是1,C站的id是3,我们把数组的元素设置为id值,那么总费用S=每站收费*(3-1),如果一站的收费是2元,那么从A到C就是4元。这个过程十分简单!时间复杂度是O(1),十分高效。

1.2 无向图实现方式

起点和目的点在同一条地铁线的情况计算是十分简单的,只需要构造一个数组就能实现。但如果起点和目的地不是同一条线,那么就比较复杂了。比如从一号线的A站作为起点,去二号线的F站,可选择的走法比较多,途中就有以下两种走法:

路线一:A--->B--->C--->D--->E--->F

路线二:A--->B--->F

然而在现实中,两个站点的路线可能不止两条路线了,按照地铁的计费原则,理应是选择最短路径来进行计费的,也就是上面说的路线二。那么系统是怎么找到路线二,并确定它就是最短路径呢?其实,熟悉图论的童鞋早就会想到其实地铁线路,就是个无向图。而无向图中有个经典的算法是广度优先搜索,它可以找到无向图中两点之间的最短路径。

2、广度优先搜索找到两个站点最短路径

为了方便简单实现,我们这里用站点的ID作为图的各个顶点,0代表A,1代表B,2代表C,3代表D,4代表E,5代表F,这样上面的地铁线路图就简化成下面的无向图。于是,求A—>F的最短距离问题就变成求顶点0到顶点5的最短路径。

在这里插入图片描述

2.1 构建站点对象

我们先构造站点对象Station,这里我们主要用到的是站点的stationId,如下代码:

	/**
	 * 站点
	 */
	class Station{
		int stationId;//站点id
		String stationName;//站名
		
		public Station(int stationId,String stationName) {
			this.stationId = stationId;
			this.stationName = stationName;
		}

	}

2.2 用邻接表构造地铁无向图

我们使用邻接表来表示这个无向图,以下是构造出的站点邻接表。左边的数组代表所有的站点,右边的一个个链表元素代表数组中的站点邻接的站点,比如数组中id为2的站点,有两个邻接站点,分别是id为2和4。
在这里插入图片描述

我们用链表数组stationTable表示这张邻接表,由传入的站点列表stationList作为stationTable的数据源。光是站点还不够,站点是通过一组线路来连接起来的,这个可以通过传入两个站点调用addEdge方法,构造站点与站点的连线。

public class SubwayGraph {
	
	int stationCount;//站点总数量
	
	LinkedList<Integer>[] stationTable;//站点邻接表
	
	public SubwayGraph(List<Station> stationList) {
		int size = stationList.size();
		this.stationCount = size;
		this.stationTable = new LinkedList[size];
		
		for(Station s : stationList) {
			stationTable[s.stationId] = new LinkedList<>();
		}
		
	}
	
	public void addEdge(int stationId1, int stationId2) {
		stationTable[stationId1].add(stationId2);
		stationTable[stationId1].add(stationId2);
	}
	
	public int distance(int station1, int station2) {
		int distance = new StationSearch(this, station1).distanceTo(station2);
		System.out.println(String.format("站点id为[%1$s]到站点id为[%2$s]的距离为[%3$s]",              station1, station2, distance));
		return distance;
	}

2.3 建立搜索类

为了把职责分开,所以把搜索另外建一个类StationSearch,专门处理站点距离的计算。

/**
	 *站点搜索
	 */
	static class StationSearch{
		boolean[] visited;//已经经过的站点
		
		Queue<Integer> preToVisit;//准备经过的站点
		
		int sourceStationId;//起点
		
		int prevStationId[];//数组下标为站点id,元素为当前下标站点的上一个站点
		
		public StationSearch(SubwayGraph g, int sourceStationId) {
			int bound = g.stationCount;
			this.visited = new boolean[bound];
			this.prevStationId = new int[bound];
			this.sourceStationId = sourceStationId;
			preToVisit = new LinkedList<>();
			bfs(g,sourceStationId);//广度优先搜索
		}
		
		/**
		 * 广度优先搜索
		 * @param g
		 * @param sourceStationId 起点
		 */
	    private void bfs(SubwayGraph g, int sourceStationId) {
	    	visited[sourceStationId] = true;//标识为已经过
	    	
	    	preToVisit.add(sourceStationId);
	    	
	    	while(!preToVisit.isEmpty()) {
	    		int toVisit = preToVisit.poll();
	    		
	    		for(int i : g.stationTable[toVisit]) {
	    			if(!visited[i]) {
	    				visited[i] = true;//标识为已访问
	    				prevStationId[i] = toVisit;//记录上一个站点
	    				preToVisit.add(i);//邻接站点进入队列
	    			}
	    		}
	    	}
	    }
	    
	    /**
	     * 是否可达站点
	     * @param targetStationId
	     * @return
	     */
	    public boolean hasPathTo(int targetStationId) {
	    	return visited[targetStationId];
	    }
	    
	    /**
	     * 到指定的站点距离
	     */
	    public int distanceTo(int targetStationId) {
	    	
	    	if(!hasPathTo(targetStationId)) return 0;
	    	
	    	int distance = 0;
	    	Stack<Integer> path = new Stack<>();
	    	for(int i = targetStationId;i != this.sourceStationId;i = prevStationId[i]) {
	    		distance += 1;
	    		path.push(i);
	    	}
	    	path.push(this.sourceStationId);
	    	
	    	System.out.print("经过站点:");
	    	int size = path.size();
	    	for(int p = 0;p<size;p++) {
	    		System.out.print(path.pop() + " ");
	    	}
	    	System.out.println();
	    	return distance;
	    }
		
	}

以上代码有几个成员变量:

  • visited;boolean数组,默认所有元素都是false。可以想象成列车已经经过的站点,把该下标等于该经过的站点的元素记为true。记录访问记录的原因是避免重复访问。
  • preToVisit;计划要经过(访问)的站点,是一个队列。可以想象成列车可能要去往的站点。
  • sourceStationId;起点站。开始计算的站点。
  • prevStationId;是个整型数组,当前下标是当前站点,下标对应的数组元素是当前站点的上一个站点。这个变量是为了记录轨迹用的。

搜索类中有个最核心的方法,就是dfs方法。这个方法实现的就是广度优先搜索。这个方法的大概逻辑分为以下几个步骤:

  1. 先把起点设置为已经过(访问),即visited[sourceStation]=true;
  2. 把起点放进待访问队列,即preToVisit.add(sourceStationId);
  3. preToVisit队列中的站点出列;
  4. 将出列的站点的邻接点遍历出来,如果遍历出的站点未访问过,则把这些节点记录为已访问,把它们放到队列preToVisit中,并且把它们的上一个站点(即从队列中poll出来的那个站点)记录下来;
  5. 重复3、4步骤,直到队列preToVisit为空。即优先把当前站点的邻接站点优先找出来,再把这些站点放进队列,再分别把这些站点的邻接站点优先找出来,以此类推,直到所有站点都访问到。

再来关注一下搜索类中distanceTo方法,这里prevStationId数组就派上用场了,因为这个数组是记录了当前站点访问的上一个站点,于是可以很容易的把从起点到当前站点的轨迹打出来。这里用了个for循环,指针变量设置为当前的站点,不断的自底向上的通过prevStationId[i]把所经过的站点遍历出来,直到指针变量到达了起始点才结束。因为整个遍历过程是从目的地到起点的倒序,所以用栈结构来存储这些遍历出的站点,以便从栈中弹出来的是起点到目的地的顺序。这里还用一个distance变量来计算整个过程经历过的“边数”,因为每一次遍历可以看作是找到一根边,一直到起点,就是所有边的总数,也就是起点到目的站点的距离。

	   int distance = 0;
	   Stack<Integer> path = new Stack<>();
	   for(int i = targetStationId;i != this.sourceStationId;i = prevStationId[i]) {
	      distance += 1;
	      path.push(i);
	   }
	   path.push(this.sourceStationId);

2.4 测试站点距离计算

核心代码写完了,可以测试一下。我们先根据文章开头那些站点建立几个站点对象,再用这些站点构造出地铁线路图,把所有的站点通过调用addEdge彼此连接起来。最后一步就是计算距离,并且打印出从起点到目的地的轨迹。

	public static void main(String[] args) {
		List<Station> stationList = new ArrayList<SubwayGraph.Station>();
		//建立站点
		stationList.add(new Station(0, "A"));
		stationList.add(new Station(1, "B"));
		stationList.add(new Station(2, "C"));
		stationList.add(new Station(3, "D"));
		stationList.add(new Station(4, "E"));
		stationList.add(new Station(5, "F"));
		
		//构造地铁线路图
		SubwayGraph g = new SubwayGraph(stationList);
		g.addEdge(0, 1);//A-B
		g.addEdge(1, 2);//B-C
		g.addEdge(1, 4);//B-E
		g.addEdge(2, 3);//C-D
		g.addEdge(3, 4);//D-E
		g.addEdge(4, 5);//E-F
		
		g.distance(0, 5);//计算站点0到5的距离
	}

以下为打印结果:

经过站点:0 1 4 5 
站点id为[0]到站点id为[5]的距离为:3

2.5 为什么广度优先搜索的路径是最短路径

为什么广度优先搜索的路径是最短路径呢?换句话说,为什么没有其他线路比当前计算出来的路线短?因为从广度优先搜索算法的实现来看,我们可以发现一个特点:队列preToVisit中的元素,都是按照它们和起点的距离顺序进队列的,距离起点站越近的总会最先出列。所以在目的站点targetStationId进队列到它出队列期间,不可能有别的更短路径到达targetStation。

3、广度优先搜索的性能

3.1 时间复杂度

这种广度优先搜索的时间复杂度是多少呢?我们假设最坏的情况,就是无向图中最后一步才找到目的站点,这时候意味着图中每个顶点,以及每个顶点的邻接点都访问了一次,也就是相当于每条边都访问了两次,如果无向图的边数是E,那么广度优先搜索最坏的情况所需要的时间是2E。而平均来看,广度优先的搜索时间复杂度是线性的O(E)。

3.2 空间复杂度

实现一个无向图,用了邻接表的存储结构。假设总的顶点数是N,邻接表除了要存储各个顶点,而且还要存储顶点的邻接点,另外还有额外的visitedpreToVisitprevStationId这几个结构,但空间规模都不会超过N,所以广度优先算法的空间复杂度是O(N)。

4、实现更好的地铁计费系统

其实上面的实现,不是最优的计费系统实现方式。正如我们刚开始假设的,如果起点和目的地是同一条路线,那么我们可以通过数组访问实现时间复杂度为常数的计算,而不用经历广度优先搜索。

在这里插入图片描述

5、本文源码地址

戳这里获取本文源代码:点我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章