持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情 下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。
图的存储方式
- 邻接表
- 邻接矩阵
- 如何表达图,生成图
图:由点集和边集组成
邻接表法可以直接查出点所有邻居,即点集
邻接矩阵法可以直接查出所有边,即边集
邻接表法和邻接矩阵法都可以表示有向图和无向图
某些特殊的图也可以使用数组表示(每一个点都有往上的点且没有环)
可以用自己喜欢表达图的方式把所有算法实现一遍作为模板,在题目出现新的图的表达方式,只要写一个图的转化到自己的模板上,将题目出现的图结构变成自己喜欢表达的图结构,即遇到表达图的特征比较特殊的题,转化成可以调模板函数的结构
宽度优先遍历
思路分析 得到图的首节点,通过set元素不可重复的特性将节点入队Queue中,Queue出队的顺序就是宽度优先遍历的结果 代码实现
public static void bfs(Node head){
if (head==null){
return;
}
Queue<Node> queue=new LinkedList<>();
HashSet<Node> set = new HashSet<>();
queue.add(head);
set.add(head);
while(!queue.isEmpty()){
Node node=queue.poll();
System.out.println(node.value);
for(Node next:node.nexts){
if (!set.contains(node)){
set.add(node);
queue.add(node);
}
}
}
}
使用set是为了节点不重复加进queue,防止程序进入死循环 通过输出得出遍历顺序
深度优先遍历
深度优先遍历的思路是一旦发现后续的路没走过,根据返回的节点继续走深度,直到把这条路都走完
代码实现
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;
}
}
}
}
注意看,当发现set里面未包含指定的节点时,先将此节点包含他的邻居节点一起送回Queue中,break跳出当前循环并从之前未发现的节点处遍历
拓扑排序 (适合有向图)
在指定有向图中,如何确定依次遍历的顺序,最终所有的路径都遍历
以该图为例
先找到入度为0的点,取出并去掉他的影响(即将从他出发的线一一去除),以此不断循环,最终得到拓扑排序的结果
代码实现
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算法(适用无向图)
从边的角度出发生成最小生成树,依次选择最小的边,将所有边排好序 ,从最小的边开始考虑,只需要考虑加上这条边之后不会形成环
如何保证不会生成环
在最开始,将不同点分进不同的集合中,如图
在连接AC时,查看A所在的集合和C所在的集合是否相同,
- 不同则不会形成环,之后把AC两个集合合在一起
- 相同则会形成环,不能要这条边 以此类推,按边的长度由小到大依次比较,已经在同一个集合中的点再出现连线就会形成环,直到所有的点都在同一个集合中,此时最小生成树生成成功 代码实现
public static class MySets{
public HashMap<Node, List<Node>> setMap;
public MySets(List<Node> nodes){
for(Node cur:nodes){
List<Node> set = new ArrayList<Node>();
set.add(cur);
//此时每个点的集合中只有自己本身
setMap.put(cur, set);
}
}
public boolean isSameSet(Node from ,Node to){
List<Node> fromSet = setMap.get(from);
List<Node> toSet = setMap.get(to);
return fromSet==toSet;
}
public void union(Node from,Node to){
List<Node> fromSet = setMap.get(from);
List<Node> toSet = setMap.get(to);
for (Node toNode:toSet) {
fromSet.add(toNode);
//将toSet中节点的集合改为fromSet
setMap.put(toNode, fromSet);
}
}
}
也可以采用并查集的结构
public static class UnionFind {
private HashMap<Node, Node> fatherMap;
private HashMap<Node, Integer> rankMap;
public UnionFind() {
fatherMap = new HashMap<Node, Node>();
rankMap = new HashMap<Node, Integer>();
}
private Node findFather(Node n) {
Node father = fatherMap.get(n);
if (father != n) {
father = findFather(father);
}
fatherMap.put(n, father);
return father;
}
public void makeSets(Collection<Node> nodes) {
fatherMap.clear();
rankMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
rankMap.put(node, 1);
}
}
public boolean isSameSet(Node a, Node b) {
return findFather(a) == findFather(b);
}
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aFather = findFather(a);
Node bFather = findFather(b);
if (aFather != bFather) {
int aFrank = rankMap.get(aFather);
int bFrank = rankMap.get(bFather);
if (aFrank <= bFrank) {
fatherMap.put(aFather, bFather);
rankMap.put(bFather, aFrank + bFrank);
} else {
fatherMap.put(bFather, aFather);
rankMap.put(aFather, aFrank + bFrank);
}
}
}
}
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;
}
k算法是从线的角度实现最小生成树
Prim算法(要求无向图)
可以从任意一个点出发,当线被标志时才被解锁,
当边被解锁时,划勾 ,如在A时,AB,AC,AD都被解锁,每次添加新的点都会将该点的边加进priorityQueue中,在下一次在被解锁的边中且没有划下滑线的边中寻找权最小的边,若左右两个点都在set中,则需要选择其他点,假如到D时,此时AD都已在set集合中,需要选择BC,再从B到E
每次选的边只会把一个新的点加进来,检查一条边的时候判断左右两点是否在集合中,不需要这么复杂的集合,只需要放进一个HashMap中
代码
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循环解决森林问题 即所有的图都没有交集
for (Node node : graph.nodes.values()) {
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 : toNode.edges) {
priorityQueue.add(nextEdge);
}
}
}
}
}
return result;
}
可能会重复边,即便重复了边,其点已经在set中,会被跳过,可以加一个存放边的set进行判断,可以减少部分的时间复杂度
Dijkstra算法 --单元最短路径算法
适用于没出现累加和为负数的环,陷入死循环
规定出发点,到某一点的最短路径
如图,以A为出发点,生成到各点的最短距离
解题流程
A到各点的距离初始化时除了本身为0之外其他都为正无穷,当发现从A出发到各点距离有比之前距离更小时,替换,遍历完一遍后,A到A本身的距离确定,锁死
在剩下的记录中找到最短的长度为B
依次遍历B的所有边,从原路径A到B再到B边上的点,查看是否有更短路径
当遍历完所有记录都锁死,循环遍历结束
为什么不能出现累加和为负数的环
如果出现负数,不断循环可以将权值变小,不断变小陷入死循环
被锁死的记录也会出现不对的情况
代码实现
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 (Map.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;
}
缺陷是 可能没有判断是否存在累加和为负数的环Dijkstra算法的优化可以从堆入手,堆可以吐出长度最小的边,但是不能修改堆里的数据,遍历完一个节点的边后可能要重新维护堆