(迪杰斯特拉)Dijkstra算法详细讲解

(迪杰斯特拉)Dijkstra算法详细讲解_第1张图片

  1. 迪杰斯特拉算法的朴素写法

我们用这个图来模拟一下迪杰斯特拉算法,其实这个算法就是贪心加上广搜,理解了感觉非常简单

​ 贪心体现在哪呢? 广搜又体现在哪呢?

下面我们就来模拟一遍这个算

法的朴素写法,写完朴素写法之后我们试着对其优化。

此算法主要是解决,在一个图中,某一节点(start表示)到其余的节点的最短路径和距离。

算法维护着三个数组:

  • ​ dis[] : dis[i] 表示starti这个节点的最短距离

  • ​ visit[] :visit[i]= true 表示i这个节点已经被访问过之后就不需要访问了 ,这也是贪心算法体现之处

  • ​ pre[]: pre[i] = j 表示到 i节点的最短路径中i的前驱节点是j

    算法开始初始化:

  •   	初始化dis数组全为无穷大,表示目前到此节点不可达,为了以后更新所用,但是 dis[start] = 0,因为自己到自己永远是直接  可达的, 不需要寻找,  
    
  •   	初始化 visit数组全为false,表示都没有访问过
    
  •   	初始化 pre数组全为负无穷,表示 前驱节点目前不存在(不一定是负无穷,不存的数皆可)
    

算法一共分为两步:

  1. 从dis数组中找出最小路径的节点,设置此节点为已访问

  2. 对第一步找到的节点(用p表示),根据此节点邻接节点(用t表示)对dis数组进行更新,怎么更新呢?

  3. dis数组的含义是start节点到其他节点的最短路径,那么我们就可以这样更新:

    dis[p]+ map[p][i] < dis[i]时,dis[i]= dis[p]+ map[p][i] ,这个怎么理解呢,就是如果到t节点是从p节点再到t节点比我之前的那条路径到t节点还要短,那我么我们就要更新.更新完之后循环 第一步,直到所有节点都被访问过

​ 好,理解完概念之后,我们用左边的图模拟一下迪杰斯特拉算法的步骤:(假如我们要寻找1到其余三个节点的最短路)

​ 初始化 :
dis[]= [65525, 0, 65525, 65525, 65525]

visit[] =[false, false,false, false, false]

pre[] = [-1,-1,-1,-1,-1]

​ 算法循环开始

  1. 第一步 : 找最小值 可以看到 node= 1,设置visit[i]= true

​ 第二步: 以1节点进行扩展 扩展之后 dis = [65525, 0, 1, 65525, 4] pre = [-1, -1, 1, -1, 1]

  1. 第一步: 找最小值 可以看到 没有被访问过并且最小值为 node= 2 ,设置visit[2]= true

​ 第二步:更新操作,以二节点进行更新 扩展之后dis = [65525, 0, 1, 2, 4] pre = [-1, -1, 1, 2, 1]

  1. 第一步:找最小值 可以看到 没有被访问过并且最小值为 node= 3 ,设置visit[3]= true

​ 第二步:更新操作,以三节点进行更新扩展之后dis= [65525, 0, 1, 2, 3] pre = [-1, -1, 1,2, 3]

  1. 第一步:找最小值 可以看到 没有被访问过并且最小值为 node= 4 ,设置visit[4]= true

​ 第二步:更新操作 ,以四节点进行更新扩展之后dis= [65525, 0, 1, 2, 3] pre = [-1, -1, 1,2, 3]

第四步之后算法结束

如果你理解了上面步骤,那么代码应该是水到渠成了

// times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2
	// time表示边关系 N表示有多少个节点 K表示从哪个节点开始寻找最短路径
	public void networkDelayTime(int[][] times, int N, int K) {
		HashMap<Integer, List<int[]>> map = new HashMap<>();

		// 这是无向图
        // 邻接表存储
		for (int[] t : times) {
			if (!map.containsKey(t[0])) {
				map.put(t[0], new ArrayList<int[]>());
			}
			if (!map.containsKey(t[1])) {
				map.put(t[1], new ArrayList<int[]>());
			}
			map.get(t[0]).add(new int[] { t[1], t[2] });
			map.get(t[1]).add(new int[] { t[0], t[2] });
		}

		System.out.println(map);
		// 存储K到每个节点的最短值
		int[] dis = new int[N + 1];
		Arrays.fill(dis, Integer.MAX_VALUE);
		dis[K] = 0;

		// pre[i] = j 表示i的前驱节点是j
		int[] pre = new int[N + 1];
		Arrays.fill(pre, -1);

		boolean[] visit = new boolean[N + 1];
        
        
		while (true) {

			int min_node = -1;
			int min_len = Integer.MAX_VALUE;

			for (int i = 1; i < dis.length; i++) {

				if (!visit[i] && dis[i] < min_len) {
					min_len = dis[i];
					min_node = i;
				}

			}

			// 如果都遍历完了那就说明 图都遍历完成了
			if (min_node < 0) {
				break;
			}
			visit[min_node] = true;

			if (map.containsKey(min_node)) {
				for (int[] t : map.get(min_node)) {
					// 按照贪心的思想 访问过的一定是最短路径了 不需要更新
					// 因为从最短的边出去的节点一定是最短的,我们每一次找min_node都是找最短的边
					if (!visit[t[0]] && dis[t[0]] > dis[min_node] + t[1]) {
						dis[t[0]] = dis[min_node] + t[1];
						pre[t[0]] = min_node;
					}
				}
			}

		}
		System.out.println(Arrays.toString(dis));
		System.out.println(Arrays.toString(pre));

	}

到了现在,你是否体会到了广搜和贪心的思想了, 广搜就是每次搜索大一圈,直到遍历完图, 贪心就是为了更好地广搜, 使得每次只用搜索最小的那个节点

  1. 迪杰斯特拉的优先队列写法

    ​ 不知道细心的你是否注意到了,我们在每次循环都需要去找一遍dir里面的最小值, 那么我们有没有什么方法直接就可以拿到呢, 不用循环去找?

    ​ 答案是有的, 有一种数据结构是优先队列, 可以按照队列里面的优先级进行排序, 最小的永远在队列头, 我们就可以使用这种数据结构, 每次循环只需要取出队列头就行了, 当队列空, 自然也就遍历结束了

     我们直接上代码
    
    // 使用优先队列进行优化
    	public void networkDelayTime_better(int[][] times, int N, int K) {
    
    		HashMap<Integer, List<int[]>> map = new HashMap<>();
    
    		// 这是无向图
            // 邻接表存储
    		for (int[] t : times) {
    			if (!map.containsKey(t[0])) {
    				map.put(t[0], new ArrayList<int[]>());
    			}
    			if (!map.containsKey(t[1])) {
    				map.put(t[1], new ArrayList<int[]>());
    			}
    			map.get(t[0]).add(new int[] { t[1], t[2] });
    			map.get(t[1]).add(new int[] { t[0], t[2] });
    		}
    
    		int[] dis = new int[N + 1];
    		Arrays.fill(dis, Integer.MAX_VALUE);
    
    		
    		dis[K] = 0;// 自己到自己
    
    		int[] pre = new int[N + 1];
    		Arrays.fill(pre, -1);
    
    		boolean[] visit = new boolean[N + 1];
    
    		// 优先队列
    		PriorityQueue<Integer> q = new PriorityQueue<>(new Comparator<Integer>() {
    
    			@Override
    			public int compare(Integer o1, Integer o2) {
    				return dis[o1] - dis[o2];
    			}
    
    		});
    
    		// 先把自己压进去
    		q.offer(K);
    
    		int num;
    		while (!q.isEmpty()) {
    			num = q.poll();
    
    			if (visit[num]) {
    				continue;
    			}
    			
    			visit[num] = true;
    			if (map.containsKey(num)) { // 如果存在这个节点
    				for (int[] t : map.get(num)) { // 遍历这个节点的相邻节点
    					if (!visit[t[0]] && dis[t[0]] > dis[num] + t[1]) { 
                            // 如果路径更少
    						// 则更新
    						dis[t[0]] = dis[num] + t[1];
    						pre[t[0]] = num;
    						q.offer(t[0]);
    					}
    
    				}
    			}
    
    		}
    		System.out.println(Arrays.toString(dis));
    		System.out.println(Arrays.toString(pre));
    
    	}
    
  2. 最短路径的深度优先写法(当做理解)

    ​ 我们定义一个辅助函数

    ​ // start表示从起始节点, sum表示起始节点到每个节点的路径, 用作更新用

    ​ // map表示邻接表

    ​ private void dfs(int start, int sum, HashMap> map)

    ​ // 此函数作用是 寻找到start节点到没个节点的做短路径 ,存储到一个变量里

    ​ 直接看代码理解

    public void networkDelayTime_dfs(int[][] times, int N, int K) {
    
    		HashMap<Integer, List<int[]>> map = new HashMap<>();
    
    		// 这是无向图
    		for (int[] t : times) {
    			if (!map.containsKey(t[0])) {
    				map.put(t[0], new ArrayList<int[]>());
    			}
    			if (!map.containsKey(t[1])) {
    				map.put(t[1], new ArrayList<int[]>());
    			}
    			map.get(t[0]).add(new int[] { t[1], t[2] });
    			map.get(t[1]).add(new int[] { t[0], t[2] });
    		}
    
    		dis = new int[N + 1];
    		Arrays.fill(dis, Integer.MAX_VALUE);
    		dis[K] = 0;
    		
    
    		visit = new boolean[N + 1];
    		visit[K] = true;
    		dfs(K, 0, map);
    
    		System.out.println(Arrays.toString(dis));
    
    	}
    
    	private void dfs(int k, int i, HashMap<Integer, List<int[]>> map) {
    		// 剪枝操作, 当遍历到某一个节点时, 找到的路径 已经比我存储在dis数组里面的还要大, 那我们不需要遍历后面了,直接砍掉
    		if (dis[k] < i) {
    			return;
    		}
    		// 到了这里 i肯定小于或等于dis[k] 直接更新即可
    		dis[k] = i;
    
            // 对于k的每一个邻接点 
    		for (int[] t : map.get(k)) {
    			// 如果没有访问过
    			if (!visit[t[0]]) {	
    				// 设置为访问状态
    				visit[t[0]] = true;
                    // 后面的交给递归
    				dfs(t[0], i + t[1], map);
                    // 递归结束之后 需要回溯 
    				visit[t[0]] = false;
    			}
    
    		}
    
    	}
    

    看到这里,你是否掌握了图的最短路径的求解?

你可能感兴趣的:(算法与数据结构)