算法<初级> - 第四章 前缀树、图等相关问题(完结)

算法 第四章

<一> 前缀树/字典树(trie tree/prefix tree)

  • 类似于状态机的树结构,初始化为空根节点,输入字符串进行分支分裂

    • ⚪ - 输入"abc" - ⚪—ao—bo—co - 输入"abd" - ⚪—ao—bo—co(—do) ,即在o(b)节点处分支一个o(c)节点(o表示节点,"a"表示类似状态机的边)

    • 节点中可以存储需要的信息(比如输入了多少次相同串);查找前缀匹配信息利用前缀树

    • 相比哈希表查询而言,前缀树查询可扩展性更强,可以适用于其他条件更苛刻的查询匹配操作

  • 算法实现(Java)

public static class TrieNode {
		public int path;		// 边经过次数
		public int end;
		public TrieNode[] map;		// 表示边

		public TrieNode() {
			path = 0;
			end = 0;
			map = new TrieNode[26];		// 假设只会出现小写字母;若是中文则可以用hashmap来实现next指针
		}
	}

	public static class Trie {
		private TrieNode root;

		public Trie() {
			root = new TrieNode();
		}

		public void insert(String word) {
			if (word == null) {
				return;
			}
			char[] chs = word.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.map[index] == null) {
					node.map[index] = new TrieNode();
				}
				node = node.map[index];
				node.path++;
			}
			node.end++;
		}

		public void delete(String word) {		// 共享节点不删,计数器--
			if (search(word)) {
				char[] chs = word.toCharArray();
				TrieNode node = root;
				int index = 0;
				for (int i = 0; i < chs.length; i++) {
					index = chs[i] - 'a';
					if (node.map[index].path-- == 1) {
						node.map[index] = null;
						return;
					}
					node = node.map[index];
				}
				node.end--;
			}
		}

		public boolean search(String word) {
			if (word == null) {
				return false;
			}
			char[] chs = word.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.map[index] == null) {
					return false;
				}
				node = node.map[index];
			}
			return node.end != 0;
		}

		public int prefixNumber(String pre) {
			if (pre == null) {
				return 0;
			}
			char[] chs = pre.toCharArray();
			TrieNode node = root;
			int index = 0;
			for (int i = 0; i < chs.length; i++) {
				index = chs[i] - 'a';
				if (node.map[index] == null) {
					return 0;
				}
				node = node.map[index];
			}
			return node.path;
		}
	}
  • 例子:字符串数组arr1,arr2,求arr2中有哪些字符串是在arr1中出现的?

    • 先用arr1构造前缀树,然后逐一search数组中字符串即可,看end
  • 例子:arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?

    • 先用arr1构造前缀树,然后逐一search数组中字符串即可,看path

<二> 图的存储方式

  • 1)邻接表 2)邻接矩阵

    • 邻接表:每个点都有一条链表,链表节点表示有该点指向节点表示点的边。 - 带权表即把链表节点加上权重参数即可

    • 邻接矩阵:二维矩阵,行起点列终点,权重参数值,无权即+-1∞

  • 笔试中最常见的方式是用二维列表存储,其中一维列表:[边权,起点,终点]

类图 - 由二维列表生成类图

  • Node点结构:value,入度in,出度out,邻接节点Arraylist(出度节点),邻接边Arraylist(出度边)。

  • Edge边结构:权重,from,to。

  • Graph图结构:点Arraylist,边Arraylist

  • 转化成类图后方便图相关问题的解决 - 点集边集等,所有关于图的算法都可以用类图结构方便

  • 算法实现(Java)

// 点
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<>();
	}
}

//边
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;
	}
}

//图
public class Graph {
	public HashMap<Integer,Node> nodes;
	public HashSet<Edge> edges;

	public Graph() {
		nodes = new HashMap<>();
		edges = new HashSet<>();
	}
}

// 二维列表构造类图
public static Graph createGraph(Integer[][] matrix) {
		Graph graph = new Graph();
		for (int i = 0; i < matrix.length; i++) {
			Integer from = matrix[i][0];
			Integer to = matrix[i][1];
			Integer weight = 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;
	}

<三> 图的遍历

广度优先遍历(宽度优先遍历) - BFS

  • 一层一层的遍历

  • 利用队列实现 - 同时准备一个set防止已经遍历过的节点再进队列,相当于注册(如果只有栈的话,就将栈转为队列再实现 - 两次进栈再弹出就是队列)

  • 根节点判断set,入队列和set,(弹队列打印,所有出度节点判断入队列set)。直至队列变空

  • 算法实现(Java)

	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);
				}
			}
		}
	}

深度优先遍历:DFS

  • 任何节点只有所有路径走完了才会回到父节点,深度遍历

  • 使用栈实现,同时准备一个set防止已经遍历过的节点再进队列(如果只有队列的话,就将队列转为栈再实现 - 两个队列相互倒,出队保留最后一个弹出就是栈)

  • 根节点判断set,入栈和set打印,(任意一出度节点判断入栈set打印 / 无出度节点未注册则弹栈)。直至栈变空

  • 算法实现(Java)

	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;
				}
			}
		}
	}

<四>图的常见算法

拓扑排序算法:

  • 适用范围:要求有向图,且有入度为0的点,没有环。节点依赖关系图,拓扑排序:后项依赖的条件在前项已完成。

  • eg. 学A前要学B,学B前要学C或者D,那么CBA/DBA/DCBA这样的排序顺序就是正确的拓扑排序,因为后项的前置项都已先完成。

    • 入度为0节点打印,其出度节点入度-1,其中入度为0节点打印,循环。
  • 算法实现(Java)

	// directed graph and no loop
	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);	  // 登记入度为0点
			}
		}
		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克里斯卡尔算法 / Prim普利姆算法 - 最小代价无环连通路,要求无向图 - 可以有多个连通图或者孤立节点

    • 不同的贪心策略:前者以边为关键来生成最小,后者以顶点为关键来生成最小生成树。

    • Kruskal:循环找全局最小边,加入进来的边就不再后续看了,找到时判断边左右两节点是否已经加入(即是否构成环) - 使用并查集+优先队列实现

    • Prim:任意选择一个点作为集合,选择集合相邻点最小权值边加入(这步还是由优先队列弹栈顶),邻接节点并入集合,循环。 - 只是一个点一个点的并入,所以不需要用并查集只用一个set就行。

  • 算法实现(Java)

// Kruskal - 并查集+优先队列
	public static class EdgeComparator implements Comparator<Edge> {

		@Override
		public int compare(Edge o1, Edge o2) {      // 优先队列的cmp
			return o1.weight - o2.weight;
		}
	}
	public static Set<Edge> kruskalMST(Graph graph) {		// K算法实现 - 使用并查集实现
		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;
	}


 // Prim - 优先队列:
 	public static class EdgeComparator implements Comparator<Edge> {

		@Override
		public int compare(Edge o1, Edge o2) {
			return o1.weight - o2.weight;
		}
	}
	public static Set<Edge> primMST(Graph graph) {
		PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
		HashSet<Node> set = new HashSet<>();
		Set<Edge> result = new HashSet<>();
		for (Node node : graph.nodes.values()) {        // 多个连通图for;否则一次就完成一个连通图
			if (!set.contains(node)) {
				set.add(node);
				for (Edge edge : node.edges) {
					priorityQueue.add(edge);
				}
				while (!priorityQueue.isEmpty()) {
					Edge edge = priorityQueue.poll();
					Node toNode = edge.to;
					if (!set.contains(toNode)) {
						set.add(toNode);
						result.add(edge);
						for (Edge nextEdge : node.edges) {
							priorityQueue.add(nextEdge);
						}
					}
				}
			}
		}
		return result;
	}

Dijkstra迪杰斯特拉算法:

  • 适用范围:没有权值为负数的边,求某点到其他各点的最短距离

  • 实现方法一:经典Dijkstra,遍历选择最近节点,遍历更新邻接节点距离,加入set注册最近节点,循环直至没有出度节点。
    + distanceMap存储到各点最短距离,getMinDistanceAndUnselectedNode函数选择出度节点中没有注册过且最短边

  • 实现方法二:使用堆结构优化Dijkstra算法,用小根堆去优化getMinDistanceAndUnselectedNode函数降低复杂度,循环直至没有出度节点。

  • 算法实现(Java)

// 方法一:
	public static HashMap<Node, Integer> dijkstra1(Node head) {
		HashMap<Node, Integer> distanceMap = new HashMap<>();	// 
		distanceMap.put(head, 0);
		HashSet<Node> selectedNodes = new HashSet<>();

		Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes); // 选择最近节点
		while (minNode != null) {
			int distance = distanceMap.get(minNode);
			for (Edge edge : minNode.edges) {		// 准备更新邻接节点距离
				Node toNode = edge.to;
				if (!distanceMap.containsKey(toNode)) {	 // 如果没有加入则加入
					distanceMap.put(toNode, distance + edge.weight);
				}
				distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));	//如果加入过了则更新
			}
			selectedNodes.add(minNode);	// 最近节点注册
			minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);  // 选择最近节点
		}
		return distanceMap;
	}

	public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> touchedNodes) {
		Node minNode = null;
		int minDistance = Integer.MAX_VALUE;
		for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
			Node node = entry.getKey();
			int distance = entry.getValue();
			if (!touchedNodes.contains(node) && distance < minDistance) {	// 选择出度节点中没有注册过且最短边
				minNode = node;
				minDistance = distance;
			}
		}
		return minNode;
	}
    
// 方法二:
	public static class NodeRecord {
		public Node node;
		public int distance;

		public NodeRecord(Node node, int distance) {
			this.node = node;
			this.distance = distance;
		}
	}

	public static class NodeHeap {
		private Node[] nodes;
		private HashMap<Node, Integer> heapIndexMap;
		private HashMap<Node, Integer> distanceMap;
		private int size;

		public NodeHeap(int size) {     
			nodes = new Node[size];     // 小根堆
			heapIndexMap = new HashMap<>();	 // 小根堆索引
			distanceMap = new HashMap<>();	// 存放最短距离
			this.size = 0;      // 堆大小
		}

		public boolean isEmpty() {
			return size == 0;
		}

		public void addOrUpdateOrIgnore(Node node, int distance) {
			if (inHeap(node)) {												//如果node在堆中
				distanceMap.put(node, Math.min(distanceMap.get(node), distance));	// 更新最短距离
				insertHeapify(node, heapIndexMap.get(node));
			}
			if (!isEntered(node)) {		// heapIndexMap没有加入过
				nodes[size] = node;	
				heapIndexMap.put(node, size);
				distanceMap.put(node, distance);
				insertHeapify(node, size++);
			}
		}

		public NodeRecord pop() {	// 弹出最小距离nodes[0]
			NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
			swap(0, size - 1);
			heapIndexMap.put(nodes[size - 1], -1);
			distanceMap.remove(nodes[size - 1]);
			nodes[size - 1] = null;
			heapify(0, --size);
			return nodeRecord;
		}

		private void insertHeapify(Node node, int index) {   //小根堆加入元素
			while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {
				swap(index, (index - 1) / 2);
				index = (index - 1) / 2;
			}
		}

		private void heapify(int index, int size) {     // 小根堆构造
			int left = index * 2 + 1;
			while (left < size) {
				int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])
						? left + 1 : left;
				smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;
				if (smallest == index) {
					break;
				}
				swap(smallest, index);
				index = smallest;
				left = index * 2 + 1;
			}
		}

		private boolean isEntered(Node node) {
			return heapIndexMap.containsKey(node);
		}

		private boolean inHeap(Node node) {
			return isEntered(node) && heapIndexMap.get(node) != -1;
		}

		private void swap(int index1, int index2) {
			heapIndexMap.put(nodes[index1], index2);
			heapIndexMap.put(nodes[index2], index1);
			Node tmp = nodes[index1];
			nodes[index1] = nodes[index2];
			nodes[index2] = tmp;
		}
	}

	public static HashMap<Node, Integer> dijkstra2(Node head, int size) {	
		NodeHeap nodeHeap = new NodeHeap(size);	// size总节点数
		nodeHeap.addOrUpdateOrIgnore(head, 0);
		HashMap<Node, Integer> result = new HashMap<>();
		while (!nodeHeap.isEmpty()) {
			NodeRecord record = nodeHeap.pop();	// 用小根堆去优化getMinDistanceAndUnselectedNode函数
			Node cur = record.node;
			int distance = record.distance;
			for (Edge edge : cur.edges) {
				nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
			}
			result.put(cur, distance);	// 存储至各点最短距离
		}
		return result;
	}

posted @ 2020-01-18 17:10  黄龙士  阅读(199)  评论(0编辑  收藏  举报