持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情
最近开始接触与学习数据结构与算法,一方面为了解决课内数据结构课解题思路不清晰的问题,另一方面也是听说左神的大名,故开始跟着左神一起学习数据结构与算法。同时以写博客的形式作为输出,也算是为了对所学的知识能掌握的更深吧
图的表达方式
- 邻接表
- 邻接矩阵
由于图的定义可以有许多种,因此可以指定一种最擅长的定义结构,当遇到不同定义的图时,可以将它转换成自己擅长的图结构再写对应的算法
图的定义
public class Graph{
public HashMap<Integer,Node> nodes;
public HashSet<Edge> edges;
public Grapg(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
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 calss 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;
}
}
}
图的遍历
图的广度优先遍历
广度优点遍历:从任意一个节点开始,先遍历源节点,然后遍历源节点的相邻节点,然后遍历源节点的相邻节点的相邻节点...
思路:
- 利用队列实现
- 从源节点开始依次按照宽度进队列,然后弹出
- 每弹出一个点,就把该节点所有没有进过队列的邻接点放入队列
- 重复2、3步骤,直到队列变空
思路图解:
每当出队一个元素时,就判断出队的元素是否还有与之相连的元素是还未遍历过的,如果有的话也加入队列中
代码实现:
public static void bfs(Node node){
if(node == null) return;
Queue<Node> queue = new LinkedList<>()
HashSet<Node> set = new HashSet<>();
queue.add(node);
set.add(node);
while(!queue.isEmpty()){
Node cur = queue.poll();
/*
进行操作,打印等
*/
for(Node next : cur.nexts){
if(!set.contains(next)){
queue.add(next);
set.add(next);
}
}
}
}
图的深度优先遍历
遍历思路:从某个选定节点出发,能前进就前进,若不能前进,则回退一步或者若干步再继续前进,直到所有与选定节点相同的点都被遍历为止
思路:使用栈记录下走过的路径,栈每弹出一个元素,就判断与该元素的所有相邻节点是否走过,如果有没走过的节点,则弹出的元素与该节点依次压入栈中,此时栈中记录的是新的一条路线。
不难发现,栈中的元素正是从A点开始走过的路径,通过不断的走相连且未遍历的点来达到深度优先遍历的目的,一直到与当前停留的点相连的所有点都是走过的点时,就出栈一个元素(也可以看成是在路径上倒退一步),查找与出栈元素相连且未遍历过的元素继续遍历(相当于走一条还未走过的新路线)
代码实现:
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);
/*
进行相应操作,打印等
*/
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);
/*
进行相应操作,打印等
*/
break;
}
}
}
}
拓扑排序算法
适用范围:要求有向图,且有入度为0的节点,且没有环
对于图内任意一个节点node,若是要对node进行操作,就必须先处理指向node的节点preNode,若是要对preNode进行操作,就必须先处理指向preNode的节点...拓扑排序算法就是找出一个符合这个遍历条件的遍历顺序
通常在编译顺序中就使用到了拓扑排序算法,若是要编译依赖a,就必须先编译依赖b和c,若是要编译b,就必须先编译依赖d和f...
思路:从入度为0的节点(源节点)开始,当处理完入度为0的节点时,就将该结点所指向的下一个节点们的入度-1,如果-1操作导致了这些下一个节点们的其中一些节点的入度为0,则处理那些入度变为0的节点,再次对这些入度变为0的节点的下一个节点们的入度做-1操作。
入度为0就说明了这个节点没有处理前驱节点的需要,此时可以对这个入度为0的节点进行操作
代码实现:
public static List<Node> sortedTopology(Graph graph){
//key:Node
//value:该结点剩余的入度
HahMap<Node,Integer> inMap = new HashMap<>();
Queue<Node> zeroInQueue = new LinkList<>();
//把所有节点和它的入度记录到inMap里
for(Node node : graph.nodes.values()){
inMap.put(node,node.in);
if(node.in == 0){
zeroInQueue.add(node);
}
}
//进行拓扑排序
List<Node> result = new ArrayList<>();//排序的结果放在这个List集合
while(!zeroInQueue.isEmpty()){
Node cur = zeroInQueue.poll();
result.add(cur);
for(Node node : cur.nexts){
inMap.put(node,inMap.get(node)-1);
if(inMap.get(node)==0){
zeroInQueue.add(node);
}
}
}
return result;
}
针对于无向图的算法(较为复杂 仅思路,做题巩固理解)
生成 最小生成树 问题
kruskal算法
思想:从边出发,从权值最小的边开始,不断增加权值最小的边,如果加了之后形成了环则跳过,不加上该边,直到边将所有的节点连接起来
如何判断是否成环? 以上图为例子:刚开始时,可以将ABCDE看作五个只有一个元素的集合:{A}、{B }、{C}、{D}、{E}。这五个集合分别于ABCDE对应。当加入第一条权值为2的边时,集合{A}就与集合{B}合并为一个集合{A、B},同时A和B所对应的新集合就是{A、B},当加入第二条权值为5的边时,集合{A、B}就与{C}合并为{A、B、C},ABC三个元素所对应的新集合就是{A、B、C}...最后5个元素都对应一个集合{A、B、C、D、E}。
值得注意的是:这个越来越大的集合其实也是包含了最小生成树目前的元素,集合的个数达到了图的节点个数时,就说明最小生成树完成。
当我们加入权值为100的边时,就会发现,这条边的两个节点B、D都在他们所对应的集合{A、B、C、D、E}里,那么就说明这条边的加入导致图中部分成环。(因为在集合里的元素都是可以直接或者间接到达的,当新加入的边的两个节点都在这个集合里,说明必然有若干条边可以形成一个环状,只有新加入的边的两个节点有一个是不在这个集合里的元素时才能避免成环)
代码实现思路:使用HashMap记录节点和集合的对应关系,每当加入一个最小权值的边,就判断该边的两个节点是否在两个节点所对应的集合中,不在就更新该边的两个节点所对应的两个集合,然后加入该边;在的话说明成环,找下一个最小权值的边
prim算法
思路图解:
在思路上与kruskal算法类似,需要在挑选边时考虑到是否有新的点加入。
Dijkstra算法——求单元最短路径
适用范围:边的权值没有负数
个人学习本模块后的一些碎碎念和感想
对于图的一些题目,对于码力的要求感觉要明显高于目前所学习的所有知识板块。想要学习好图这块的内容,做题巩固理解,提高将自己的想法转换成代码的能力是十分重要的(也不仅仅在图这里是这样),希望能够在接下来的学习和做题当中不断加深理解,提高码力吧,希望有什么问题或者表述不清楚的地方可以提出来,我会尽我所能回答和修改~ 也欢迎各位和我一起探讨算法相关知识和相关学习方法和路线~