比如说我想判断节点 1 是否和节点 3 相邻,我要去邻接表里 1 对应的邻居列表里查找 3 是否存在。但对于邻接矩阵就简单了,只要看看 matrix[1][3] 就知道了,效率高。
有向加权图的实现
如果是邻接表,我们不仅仅存储某个节点 x 的所有邻居节点,还存储 x 到每个邻居的权重。
如果是邻接矩阵,matrix[x][y] 不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重。
无向图怎么实现
如果连接无向图中的节点 x 和 y,把 matrix[x][y] 和 matrix[y][x] 都变成 true 不就行了;邻接表也是类似的操作。
图的遍历
图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点。所以,如果图包含环,遍历框架就要一个 visited 数组进行辅助;
boolean[] visited;
/* 图遍历框架 */
void traverse(Graph graph, int s) {
if (visited[s]) return;
// 经过节点 s
visited[s] = true;
for (int neighbor : graph.neighbors(s))
traverse(graph, neighbor);
// 离开节点 s
visited[s] = false;
}
这个 visited 数组的操作很像回溯算法做「做选择」和「撤销选择」,区别在于位置,回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对 visited 数组的操作在 for 循环外面。
1.二叉树层级遍历和 BFS 算法
// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {
if (root == null) return 0;
Queue q = new LinkedList<>();
q.offer(root);
int depth = 1;
// 从上到下遍历二叉树的每一层
while (!q.isEmpty()) {
int sz = q.size();
// 从左到右遍历每一层的每个节点
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
printf(“节点 %s 在第 %s 层”, cur, depth);
// 将下一层节点放入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
depth++;
}
}
while 循环控制一层一层往下走,for 循环利用 sz 变量控制从左到右遍历每一层二叉树节点。
注意我们代码框架中的 depth 变量,其实就记录了当前遍历到的层数。换句话说,每当我们遍历到一个节点 cur,都知道这个节点属于第几层。
2.基于二叉树的遍历框架,我们又可以扩展出多叉树的层序遍历框架:
// 输入一棵多叉树的根节点,层序遍历这棵多叉树
void levelTraverse(TreeNode root) {
if (root == null) return 0;
Queue q = new LinkedList<>();
q.offer(root);
int depth = 1;
// 从上到下遍历多叉树的每一层
while (!q.isEmpty()) {
int sz = q.size();
// 从左到右遍历每一层的每个节点
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
printf(“节点 %s 在第 %s 层”, cur, depth);
// 将下一层节点放入队列
for (TreeNode child : cur.children) {
q.offer(child);
}
}
depth++;
}
}
基于多叉树的遍历框架,我们又可以扩展出 BFS(广度优先搜索)的算法框架:
// 输入起点,进行 BFS 搜索
int BFS(Node start) {
Queue q; // 核心数据结构
Set visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录搜索的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散一步 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
printf(“从 %s 到 %s 的最短距离是 %s”, start, cur, step);
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
step++;
}
}
基于多叉树的遍历框架,我们又可以扩展出 BFS(广度优先搜索)的算法框架,所谓 BFS 算法,就是把算法问题抽象成一幅「无权图」,然后继续玩二叉树层级遍历那一套罢了。:
// 输入起点,进行 BFS 搜索
int BFS(Node start) {
Queue q; // 核心数据结构
Set visited; // 避免走回头路
q.offer(start); // 将起点加入队列
visited.add(start);
int step = 0; // 记录搜索的步数
while (q not empty) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散一步 */
for (int i = 0; i < sz; i++) {
Node cur = q.poll();
printf(“从 %s 到 %s 的最短距离是 %s”, start, cur, step);
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj()) {
if (x not in visited) {
q.offer(x);
visited.add(x);
}
}
}
step++;
}
}
这是对于无权图的应用,那么对于有权图我们就不能这么用了,因为有权图的最短路径问题不是依照步数来判断的了。所以我们要进一步简化框架,把while 循环里面的的for 去掉。
// 输入一棵二叉树的根节点,遍历这棵二叉树所有节点
void levelTraverse(TreeNode root) {
if (root == null) return 0;
Queue q = new LinkedList<>();
q.offer(root);
// 遍历二叉树的每一个节点
while (!q.isEmpty()) {
TreeNode cur = q.poll();
printf(“我不知道节点 %s 在第几层”, cur);
// 将子节点放入队列
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
如果你想同时维护 depth 变量,让每个节点 cur 知道自己在第几层,可以想其他办法,比如新建一个 State 类,记录每个节点所在的层数:
class State {
// 记录 node 节点的深度
int depth;
TreeNode node;
State(TreeNode node, int depth) {
this.depth = depth;
this.node = node;
}
}
// 输入一棵二叉树的根节点,遍历这棵二叉树所有节点
void levelTraverse(TreeNode root) {
if (root == null) return 0;
Queue q = new LinkedList<>();
q.offer(new State(root, 1));
// 遍历二叉树的每一个节点
while (!q.isEmpty()) {
State cur = q.poll();
TreeNode cur_node = cur.node;
int cur_depth = cur.depth;
printf(“节点 %s 在第 %s 层”, cur_node, cur_depth);
// 将子节点放入队列
if (cur_node.left != null) {
q.offer(new State(cur_node.left, cur_depth + 1));
}
if (cur_node.right != null) {
q.offer(new State(cur_node.right, cur_depth + 1));
}
}
}
Dijkstra 算法框架
我们也需要State 来辅助算法的执行
class State {
// 图节点的 id
int id;
// 从 start 节点到当前节点的距离
int distFromStart;
State(int id, int distFromStart) {
this.id = id;
this.distFromStart = distFromStart;
}
}
类似刚才二叉树的层序遍历,我们也需要用 State 类记录一些额外信息,也就是使用 distFromStart 变量记录从起点 start 到当前这个节点的距离。
加权图中的 Dijkstra 算法和无权图中的普通 BFS 算法不同,在 Dijkstra 算法中,你第一次经过某个节点时的路径权重,不见得就是最小的,所以对于同一个节点,我们可能会经过多次,而且每次的 distFromStart 可能都不一样,比如下图:
我会经过节点 5 三次,每次的 distFromStart 值都不一样,那我取 distFromStart 最小的那次,不就是从起点 start 到节点 5 的最短路径权重了么