「左程云-算法与数据结构笔记」| P9 详解前缀树和贪心算法(上)

635 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第七天,点击查看活动详情。

最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第六篇:P9|详解前缀树和贪心算法


一、图的表示方式

图的存储方式有两种: <1>邻接表 <2>邻接矩阵 下面的所有表示图的方法都是邻接表法,而对于邻接矩阵,下面会有函把邻接矩阵转化为邻接表。

1、邻接表

image.png

  • 邻接表把所有点与点之间的关系都记录在表中了,即有相邻的点就记录在表中
  • 例如上图中:3结点 与 1结点 和 2结点相邻,因此第三行记录了结点1和结点2

2、邻接矩阵

image.png

  • 这里使用布尔值0和1表示,有边和无边,1表示右连接,0表示无连接
  • 特殊的:此处还能表示边的长度,那样就可以数值表示长度,0表示无连接

3、图的表示方法

<1> Node

	public class Node {  
	   public int value;  
	   public int in;  
	   public int out;  
	   public ArrayList<Node> nexts;  
	   public ArrayList<Edge> edges;  
	  
	   public Node(int value) {  
	      this.value = value;  
	      in = 0;  
	      out = 0;  
	      nexts = new ArrayList<>();  
	      edges = new ArrayList<>();  
	   }  
	}
  1. value:结点的值
  2. in:指向该结点的线的个数
  3. out:该结点指出的线的个数
  4. nexts:该节点指向的结点
  5. edges:与该结点有关的线段

<2> Edge

	public class Edge {  
	   public int weight;  
	   public Node from;  
	   public Node to;  
	   public Edge(int weight, Node from, Node to) {  
	      this.weight = weight;  
	      this.from = from;  
	      this.to = to;  
	   }  
	}
  1. weight:权重,一般不使用,也可以表示为线的长度
  2. from:起始结点
  3. to:终点结点,与起始结点一起用于表明结点之间的指向

<3> Graph

	public class Graph {  
	   public HashMap<Integer,Node> nodes;  
	   public HashSet<Edge> edges;  
	   public Graph() {  
	      nodes = new HashMap<>();  
	      edges = new HashSet<>();  
	   }  
	}
  1. nodes:所有的结点
  2. edges:所有的边

<4> GraphGenerator

	public static Graph createGraph(Integer[][] matrix) {  
	   Graph graph = new Graph();  
	   for (int i = 0; i < matrix.length; i++) {  
	      Integer weight = matrix[i][0];  
	      Integer from = matrix[i][1];  
	      Integer to = matrix[i][2];  
	      if (!graph.nodes.containsKey(from)) {  
	         graph.nodes.put(from, new Node(from));  
	      }  
	      if (!graph.nodes.containsKey(to)) {  
	         graph.nodes.put(to, new Node(to));  
	      }  
	      Node fromNode = graph.nodes.get(from);  
	      Node toNode = graph.nodes.get(to);  
	      Edge newEdge = new Edge(weight, fromNode, toNode);  
	      fromNode.nexts.add(toNode);  
	      fromNode.out++;  
	      toNode.in++;  
	      fromNode.edges.add(newEdge);  
	      graph.edges.add(newEdge);  
	   }  
	   return graph;  
	}
  1. matrix:邻接矩阵,三个变量分别表示:权重、起始结点、终点结点
  2. 转换步骤:
    1. 首先判断 from 结点和 to 结点是否存在,如果不存在首先加到 Graph.nodes
    2. 然后拿到 from 结点和 to 结点,构造 Edge
    3. from 结点 out++to 结点 in++
    4. from 结点 nexts 加上 to 结点,edges 加上 edge
    5. Graph.edges 加上 edge

二、图的宽度优先遍历

1、实现思路

  • 从根节点开始遍历,首先拿到根节点相连的结点
  • 然后依次进入队列
  • 然后任意结点在出队列的时候,就要把与该结点相邻的结点放入队列
  • 但是可能会存在重复的结点入队的情况
  • 因此在入队的时候,我们同时使用 HashSet 去记录每个入队的结点,在入队之前做判断是否入队过,这样就能避免结点重复入队

2、实现代码

	public static void bfs(Node node) {  
	   if (node == null) {  
	      return;  
	   }  
	   Queue<Node> queue = new LinkedList<>();  
	   HashSet<Node> map = new HashSet<>();  
	   queue.add(node);  
	   map.add(node);  
	   while (!queue.isEmpty()) {  
	      Node cur = queue.poll();  
	      System.out.println(cur.value);  
	      for (Node next : cur.nexts) {  
	         if (!map.contains(next)) {  
	            map.add(next);  
	            queue.add(next);  
	         }  
	      }  
	   }  
	}

三、图的深度优先遍历

1、实现思路

  • 在选定出发点后,随机找一个相邻的结点前进
  • 一直这样遍历,知道没有相邻的结点可以前进
  • 就后退一步,查看是否有相邻的结点(而且没有去过)可以前进
  • 如果还是没有,就继续倒回去
  • 重复这个过程,知道所有的结点都被遍历到

2、图解

image.png

3、实现代码

	public static void dfs(Node node) {  
	   if (node == null) {  
	      return;  
	   }  
	   Stack<Node> stack = new Stack<>();  
	   HashSet<Node> set = new HashSet<>();  
	   stack.add(node);  
	   set.add(node);  
	   System.out.println(node.value);  
	   while (!stack.isEmpty()) {  
	      Node cur = stack.pop();  
	      for (Node next : cur.nexts) {  
	         if (!set.contains(next)) {  
	            stack.push(cur);  
	            stack.push(next);  
	            set.add(next);  
	            System.out.println(next.value);  
	            break;  
	         }  
	      }  
	   }  
	}

四、拓扑排序算法


适用范围:要求是有向图、且有入度in为0的结点,而且没有环

1、实现思路

  • 首先找到第一个入度为0的点,然后擦掉与入度为0的这个结点有关的所有线
  • 此时就出现了第二个入度为0的点,同样的擦掉与第二个入度为0的点有关的所有线
  • 依次类推进行输出,就是拓扑排序

2、实现代码

	public static List<Node> sortedTopology(Graph graph) {  
	   HashMap<Node, Integer> inMap = new HashMap<>();  
	   Queue<Node> zeroInQueue = new LinkedList<>();  
	   for (Node node : graph.nodes.values()) {  
	      inMap.put(node, node.in);  
	      if (node.in == 0) {  
	         zeroInQueue.add(node);  
	      }  
	   }  
	   List<Node> result = new ArrayList<>();  
	   while (!zeroInQueue.isEmpty()) {  
	      Node cur = zeroInQueue.poll();  
	      result.add(cur);  
	      for (Node next : cur.nexts) {  
	         inMap.put(next, inMap.get(next) - 1);  
	         if (inMap.get(next) == 0) {  
	            zeroInQueue.add(next);  
	         }  
	      }  
	   }  
	   return result;  
	}

五、Kruskal算法


1、最小生成树

一个图中可能存在多条相连的边,假设图中有N个结点,最小生成树,就是在图中找(N-1)条边,把所有的结点连接起来,同时要保证边的权重和最小

2、实现思路

  • 把所有边按权重排序,并且认为所有的点都是孤立的集合
  • 然后连接权重最小的边,同时把两个点并为一个集合
  • 然后找到权重第二小的边,先判断起始点和终点是否都存在于集合中,如果存在代表会成环,就找权重第三小的边
  • 依次类推…………
  • 这里使用到了并查集,老师没有展开讲解,但下面的代码使用的是并查集

3、代码实现

	public static class EdgeComparator implements Comparator<Edge> {  
	  
	   @Override  
	   public int compare(Edge o1, Edge o2) {  
	      return o1.weight - o2.weight;  
	   }  
	  
	}  
	  
	public static Set<Edge> kruskalMST(Graph graph) {  
	   UnionFind unionFind = new UnionFind();  
	   unionFind.makeSets(graph.nodes.values());  
	   PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());  
	   for (Edge edge : graph.edges) {  
	      priorityQueue.add(edge);  
	   }  
	   Set<Edge> result = new HashSet<>();  
	   while (!priorityQueue.isEmpty()) {  
	      Edge edge = priorityQueue.poll();  
	      if (!unionFind.isSameSet(edge.from, edge.to)) {  
	         result.add(edge);  
	         unionFind.union(edge.from, edge.to);  
	      }  
	   }  
	   return result;  
	}