把节点通过任意方式连接起来,就叫图。二叉树也是图的一种。
图分为有向图(带箭头)和无向图(也叫双向图,不带箭头)
基本代码
class Vertex{
private String key; // 键值
Color color; // 颜色(状态)
int d; // 离根节点的最小距离
Vertex pi; // 父节点
}
由X节点相邻的节点,构成的链表,称为临接链表。
上图中,的邻接链表:
因此,写成类似代码的形式,就是
adjacencyList.put(1, 链表(2 -> 5))
adjacencyList.put(2, 链表(1-> 3 -> 5 ->4))
或者
adjacencyList.get(1) == 链表(2 -> 5) is true
adjacencyList.get(2) == 链表(1-> 3 -> 5 ->4) is true
链表的实现么,在前面的资料中已经讲过了。
在后面最小生成树算法中介绍。
理解遍历的顺序即可,考试让你写代码不太可能。重在理解遍历的顺序,很有可能让你写出一个图的遍历顺序。
代码看个热闹就行了,重在理解思想。
是指从根节点开始,一定要把根节点的所有相邻节点都访问完了,再选下一个节点作为根节点,重复上述操作。具体流程看代码注解。
public static void BFS(Map<Vertex, List<Vertex>> adjacencyList, Vertex s) {
/**
* 每个节点共有三种状态(颜色):
* 白色:未被访问,一开始节点都是白的
* 灰色:已被访问
* 黑色:已被访问,且其所有相邻节点也都被访问了
* 变黑操作 == 访问该节点的所有相邻节点
*/
// 将根节点访问并标灰,表示根节点已被访问
s.color = Color.GRAY;
System.out.print(s + " -> ");
// 根节点与根节点的距离当然是0辣
s.d = 0;
// 这个队列,存放灰色的节点,接下来要把它们依次变黑
Queue<Vertex> queue = new LinkedList<>();
// 根节点现在还是灰色的,接下来想把它变黑
queue.add(s);
while (!queue.isEmpty()){
// 弄出一个节点u,接下来想办法把它变黑
Vertex u = queue.poll();
if (adjacencyList.get(u) != null) {
// 遍历u节点的所有相邻节点
for (Vertex v : adjacencyList.get(u)) {
// 如果这个相邻节点是白色的,就表示还没访问过。回答一下PPT里老师的问题:为什么需要这个if?因为相邻节点还可能是灰色的,而灰色的节点已经访问过了,是不需要访问的,所以这个if是有必要的。
if (v.color.equals(Color.WHITE)) {
// 访问并灰化
System.out.print(v + " -> ");
v.color = Color.GRAY;
// 子节点离根节点的距离,比父节点离根节点的距离远1个单位
v.d = u.d + 1;
// U是v的父节点
v.pi = u;
// v是灰色的节点,存入队列
queue.add(v);
}
}
}
// u节点的所有相邻节点都变灰了,因此它可以变黑了
u.color = Color.BLACK;
}
}
用挖矿游戏举例子:
public static void DFS(Map<Vertex, List<Vertex>> adjacencyList, Vertex s) {
/**
* 每个节点共有两种状态(颜色):
* 白色:没挖过,一开始所有节点都是白的
* 黑色:挖过了
*/
// 这个栈存放有哪些点是可以往下挖的
Stack<Vertex> stack = new Stack<>();
// 根节点肯定可以往下挖
stack.add(s);
// 只要有节点可以往下挖,就继续干
while (!stack.isEmpty()) {
// 取出一个节点,准备开挖
Vertex u = stack.pop();
// 只有白色节点没挖过。黑色节点挖过了,不用再挖
if (u.color.equals(Color.WHITE)){
System.out.print(u+" -> ");
// 标记一下u节点现在挖过了(因为下一步是挖u的相邻节点,相邻节点都挖了,u节点自然也就挖了)
u.color = Color.BLACK;
if (adjacencyList.get(u) != null) {
// 把u节点的相邻节点都放进stack,待会儿挖
Iterator<Vertex> iterator = adjacencyList.get(u).iterator();
while (iterator.hasNext()) {
stack.push(iterator.next());
}
}
}
}
}
递归写法(老师PPT里的):
private int time;
public static void DFS(Map<Vertex, List<Vertex>> adjacencyList){
time = 0;
//回答老师PPT里的问题:为什么需要一个循环?难道不是从一个根节点开始,发散式的挖,就可以了吗?因为可能存在孤立点或孤立结构,它们与根节点可能没有连接。所以需要让所有点都做一次根节点。
for (Vertex u : adjacencyList.keySet()){
if (u.color.equals(Color.WHITE)){
DFS_VISIT(adjacencyList, u);
}
}
}
private DFS_VISIT(Map<Vertex, List<Vertex>> adjacencyList, Vertex u){
time += 1;
u.d = time;
u.color = Color.GRAY;
for (Vertex v : adjacencyList.get(u)){
if (v.color == Color.WHITE){
v.pi = u;
DFS_VISIT(adjacencyList, v);
}
}
u.color = Color.BLACK;
time += 1;
// u.f是挖完节点u的结束时刻
u.f = time;
}
不可能考。拓扑排序与深度优先搜索没有任何区别,考了深度优先搜索就不会考它了。
括号结构、强连通分量甚至都超出了计算机科学考研大纲,都是离散数学的内容,要是期末考了,我就,我就。。。。。。。我也没办法好吧
邻接矩阵,用于记录节点与节点之间的两两距离。若有n个节点,那么邻接矩阵就是n阶方阵。邻接矩阵中的元素Matrix[i, j]代表从节点i到节点j的距离。
class Edge {
Vertex v1;
Vertex v2;
int weight;
}
public static Set<Edge> Kruskal(Vertex[] vertices, Edge[] edges) {
// 存放MST的边
Set<Edge> A = new HashSet<>();
// 把自己放入到自己所在的集合
for (Vertex vertex : vertices){
vertex.set = new HashSet<>();
vertex.set.add(vertex);
}
// 将边按边长升序排序
Arrays.sort(edges, (e1,e2)->e1.weight - e2.weight);
// 从小到大遍历每条边
for(Edge e : edges) {
// 如果这条边的两个端点不在同一个集合中,那么就合并这两个端点所在的集合,同时这条边加入MST。
if (e.v1.set != e.v2.set) {
A.add(e);
union(e.v1,e.v2);
}
}
return A;
}
// 合并两个端点所在的集合
private static void union(Vertex v1, Vertex v2) {
for (Vertex v : v2.set) {
v.set = v1.set;
v1.set.add(v);
}
}
public static void Prim(Vertex[] vertices, Map<Vertex, List<Edge>> adj, Vertex r) {
// 一开始,所有节点的key值无穷大,也就是节点还未在图中
for (Vertex v : vertices){
v.key = Integer.MAX_VALUE;
v.p = null;
}
// 根节点key值为0
r.key = 0;
// 优先级队列: 按键值从小到大顺序存放节点,后面需要将节点按键值由小到大遍历
PriorityQueue<Vertex> Q = new PriorityQueue<>(vertices.length, (v1, v2)-> v1.key - v2.key);
for (Vertex v : vertices) {
Q.add(v);
}
while(!Q.isEmpty()){
// 弄一个键值最小的节点,开始考察它的所有邻边
Vertex u = Q.poll();
for (Edge e : adj.get(u)) {
// 取得邻边通往的节点
Vertex v = e.v1.equals(u) ? e.v2 : e.v1;
// 如果节点还在优先级队列中,说明节点的邻边还未遍历。并且当边长小于节点的键值时,取代其键值。
if (Q.contains(v) && e.weight < v.key){
v.p = u;
v.key = e.weight;
// 更新优先级队列(使之有序),优先级队列是二叉堆。这里相当于二叉堆的decreaseKey操作。
Q.add(Q.poll());
}
}
}
// 将最小生成树输出。(确定了所有节点的父节点,也就确定了整棵树)
for (Vertex v : vertices) {
System.out.println(v+".parent="+v.p);
}
}
所谓单源最短路径,就是从同一个起点出发,到达其他点的最短距离。
Dijkstra算法继承了Prim算法的思想,只不过把键值替换成了到根节点的距离。
public static void Dijkstra(Vertex[] vertices, Map<Vertex, List<Edge>> adj, Vertex r) {
for (Vertex v : vertices){
v.d = Integer.MAX_VALUE;
v.p = null;
}
r.d = 0;
PriorityQueue<Vertex> Q = new PriorityQueue<>(vertices.length, (v1, v2)-> v1.d - v2.d);
for (Vertex v : vertices) {
Q.add(v);
}
while(!Q.isEmpty()){
Vertex u = Q.poll();
for (Edge e : adj.get(u)) {
Vertex v = e.v1.equals(u) ? e.v2 : e.v1;
if (Q.contains(v) && e.weight + u.d < v.d){
v.p = u;
// v到根节点的距离 = u到根节点距离 + uv间距
v.d = e.weight + u.d;
Q.add(Q.poll());
}
}
}
for (Vertex v : vertices) {
System.out.println(v+".parent="+v.p);
}
}
害,终于结束了讨人厌的图论算法,到此为止,基本的数据结构部分已经结束(其实还有一种叫做串的数据结构,不在我们的考试范围内)。如果你已经裂开了,那么就到此为止吧,混个七八十分已经没问题了。下一部分是动态规划,考试作为压轴题,但是也不是那种很复杂的题。