1、图的概念
图是一种灵活的数据结构,一般作为一种模型用来定义对象之间的关系或联系。对象由顶点(V)表示,而对象之间的关系或者关联则通过图的边(E)来表示。
图可以分为有向图和无向图,一般用G=(V,E)来表示图。经常用邻接矩阵或者邻接表来描述一副图。
在图的基本算法中,最初需要接触的就是图的遍历算法,根据访问节点的顺序,可分为广度优先搜索(BFS)和深度优先搜索(DFS)。
2、广度优先搜索
可以参考以下几道题:
广度优先搜索在进一步遍历图中顶点之前(这一句话的意识是:在遍历另外一个顶点的相邻节点时,需要把当前顶点的相邻节点全部遍历完),先访问当前顶点的所有邻接结点。
下面是广度优先搜索(BFS)的步骤:
- 首先选择一个顶点作为起始结点,将起始节点放入队列中,并将其标记为已经被访问过;
- 此时,队列存入了起始节点,所以不为空。从队头取出一个顶点,遍历当前顶点的所有相邻节点,将它们放入队列中,并标记为被访问过;
- 所有邻接点都已入队后,即不存在未标记的邻接点时,从队头取一个顶点,并使其成为当前顶点。重复执行第一步、第二步。
- 直至因队列为空而不能执行第二步时,搜索结束。
需要注意的是:1、搜索的前提条件是,队列不为空;2、广度优先搜索使用的是队列;
下面是广度优先搜索的框架:
二叉树的最小深度、打开转盘锁、最小高度树、单词接龙1、单词接龙2、课程表1、课程表2。
// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
Queue<Node> q; // 核心数据结构,广度优先搜索使用的是队列
Set<Node> 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();
/* 划重点:这里判断是否到达终点*/
if (cur is target)
return step;
/* 将 cur 的相邻节点加入队列 */
for (Node x : cur.adj())
//如果当前顶点的相邻节点没有被访问过
if (x not in visited) {
//加入队列,并标记
q.offer(x);
visited.add(x);
}
}
/* 划重点:更新步数在这里 */
step++;
}
}
注意:队列q就不说了,BFS 的核心数据结构;cur.adj()泛指cur相邻的节点,比如说二维数组中,cur上下左右四面的位置就是相邻节点;visited的主要作用是防止走回头路,大部分时候都是必须的,但是像一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要visited。
2.1、二叉树的最小深度
怎么套到 BFS 的框架里呢?首先明确一下起点start和终点target是什么,怎么判断到达了终点?
显然起点就是root根节点,终点就是最靠近根节点的那个「叶子节点」,叶子节点就是两个子节点都是null的节点:
需要注意的是:一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要visited。
if (cur.left == null && cur.right == null)
// 到达叶子节点
代码为:
int minDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> q = new LinkedList<>();
q.offer(root);
// root 本身就是一层,depth 初始化为 1
int depth = 1;
while (!q.isEmpty()) {
int sz = q.size();
/* 将当前队列中的所有节点向四周扩散 */
for (int i = 0; i < sz; i++) {
TreeNode cur = q.poll();
/* 判断是否到达终点 */
if (cur.left == null && cur.right == null)
return depth;
/* 将 cur 的相邻节点加入队列 */
if (cur.left != null)
q.offer(cur.left);
if (cur.right != null)
q.offer(cur.right);
}
/* 这里增加步数 */
depth++;
}
return depth;
}
2.2、打开转盘锁
解题思路:仔细想想,这就可以抽象成一幅图,每个节点有 8 个相邻的节点,又让你求最短距离,这不就是典型的 BFS 嘛,框架就可以派上用场了。
class Solution {
//BFS就是解决一幅图中从起点到终点的最近距离
public int openLock(String[] deadends, String target) {
//记录死亡数字
HashSet<String> set=new HashSet<>();
for(String e:deadends){
set.add(e);
}
//记录已经访问过的数字
HashSet<String> set1=new HashSet<>();
int min=0;
Queue<String> q=new LinkedList<>();
q.add("0000");
set1.add("0000");
while(!q.isEmpty()){
int len=q.size();
//这里需要注意,在循环中必须使用len,不能直接使用q.size()
//因为下面的循环中,会向这个队列中添加元素
for(int i=0;i<len;i++){
String tem=q.poll();
if(set.contains(tem)){
continue;
}
if(tem.equals(target)){
return min;
}
//把当前队列中的顶点的所有相邻节点全部添加到队列中
//因为有四个数字,所以需要四次循环,每个数字有两个相邻的节点
for(int j=0;j<4;j++){
//当前数字向上旋转后的结果
String s1=upString(tem,j);
if(!set1.contains(s1)){
q.add(s1);
set1.add(s1);
}
//当前数字向下旋转后的结果
String s2=downString(tem,j);
if(!set1.contains(s2)){
q.add(s2);
set1.add(s2);
}
}
}
min++;
}
return -1;
}
public String upString(String s,int k){
char[] arr=s.toCharArray();
if(arr[k]=='9'){
arr[k]='0';
}else{
arr[k]++;
}
return new String(arr);
}
public String downString(String s,int k){
char[] arr=s.toCharArray();
if(arr[k]=='0'){
arr[k]='9';
}else{
arr[k]--;
}
return new String(arr);
}
}
2.2、为什么 BFS 可以找到最短距离,DFS 不行吗?
首先,你看 BFS 的逻辑,depth每增加一次,队列中的所有节点都向前迈一步,这保证了第一次到达终点的时候,走的步数是最少的。
DFS 不能找最短路径吗?其实也是可以的,但是时间复杂度相对高很多。
你想啊,DFS 实际上是靠递归的堆栈记录走过的路径,你要找到最短路径,肯定得把二叉树中所有树杈都探索完才能对比出最短的路径有多长对不对?
而 BFS 借助队列做到一次一步「齐头并进」,是可以在不遍历完整棵树的条件下找到最短距离的。
形象点说,DFS 是线,BFS 是面;DFS 是单打独斗,BFS 是集体行动。这个应该比较容易理解吧。
2.3、为什么BFS 那么好,为啥 DFS 还要存在?
BFS 可以找到最短距离,但是空间复杂度高,而 DFS 的空间复杂度较低。
还是拿刚才我们处理二叉树问题的例子,假设给你的这个二叉树是满二叉树,节点总数为N,对于 DFS 算法来说,空间复杂度无非就是递归堆栈,最坏情况下顶多就是树的高度,也就是O(logN)。
但是你想想 BFS 算法,队列中每次都会储存着二叉树一层的节点,这样的话最坏情况下空间复杂度应该是树的最底层节点的数量,也就是N/2,用 Big O 表示的话也就是O(N)。
由此观之,BFS 还是有代价的,一般来说在找最短路径的时候使用 BFS,其他时候还是 DFS 使用得多一些(主要是递归代码好写)。
3、深度优先搜索
深度优先搜索在搜索过程中访问某个顶点后,需要递归地访问此顶点的所有未访问过的相邻顶点。
DFS(深度优先搜索算法)主要是通过栈的方式来实现的(实际就是一个回溯算法,可以采用回溯的方式实现),主要规则是:
- 选择一个初始顶点,并将其标记为已访问;
- 从该初始节点的相邻顶点中选择一个,继续这个过程(即再寻找邻接结点的邻接结点),一直深入下去,直到一个顶点没有邻接结点了,每访问一个就要标记为已访问;
- 回溯到这个涂黑顶点的上一层顶点,再找这个上一层顶点的其余邻接结点,继续如上操作,如果所有邻接结点往下都访问过了,就把自己标记,再回溯到更上一层。
- 上一层继续做如上操作,直到所有顶点都访问过。
3.1、岛屿的数量
这道题就是一个典型的深度优先遍历和广度优先遍历的应用。
我们可以将二维网格看成一个无向图,竖直或水平相邻的 1之间有边相连。为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1,则以其为起始节点开始进行深度优先搜索或者广度优先搜索。在深度优先搜索或者广度优先搜索的过程中,每个搜索到的 1都会被重新标记为 0。最终岛屿的数量就是我们进行深度优先搜索的次数。
深度优先搜索的实现:
class Solution {
//深度优先遍历
private int[][] nums=new int[][]{{1,0},{0,1},{-1,0},{0,-1}};
public int numIslands(char[][] grid) {
if(grid.length==0){
return 0;
}
int row=grid.length;
int col=grid[0].length;
//岛屿的个数
int count=0;
//记录被遍历过的位置
boolean[][] arr=new boolean[row][col];
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(grid[i][j]=='1'&&arr[i][j]==false){
count++;
dfs(arr,grid,i,j,row,col);
}
}
}
return count;
}
public void dfs(boolean[][] arr,char[][] grid,int x,int y,int row,int col){
arr[x][y]=true;
for(int[] num:nums){
int curRow=num[0]+x;
int curCol=num[1]+y;
if(judgeRange(curRow,curCol,row,col)&&arr[curRow][curCol]==false&&grid[curRow][curCol]=='1'){
dfs(arr,grid,curRow,curCol,row,col);
}
}
}
public boolean judgeRange(int x,int y,int row,int col){
if(x>=0&&x<row&&y>=0&&y<col){
return true;
}
return false;
}
}
广度优先遍历的实现:
class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
grid[r][c] = '0';
Queue<Integer> neighbors = new LinkedList<>();
neighbors.add(r * nc + c);
while (!neighbors.isEmpty()) {
int id = neighbors.remove();
int row = id / nc;
int col = id % nc;
if (row - 1 >= 0 && grid[row-1][col] == '1') {
neighbors.add((row-1) * nc + col);
grid[row-1][col] = '0';
}
if (row + 1 < nr && grid[row+1][col] == '1') {
neighbors.add((row+1) * nc + col);
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
neighbors.add(row * nc + col-1);
grid[row][col-1] = '0';
}
if (col + 1 < nc && grid[row][col+1] == '1') {
neighbors.add(row * nc + col+1);
grid[row][col+1] = '0';
}
}
}
}
}
return num_islands;
}
}