【LeetCode 】试题总结:广度优先搜索(BFS)
说明:本文仅做为本人总结算法竞赛试题的笔记,参照许多了题解,如有侵权请联系。
一、数据结构:二叉树中的 BFS
(一)、二叉树的堂兄弟节点
在二叉树中,根节点位于深度 0 处,每个深度为 k 的节点的子节点位于深度 k+1 处。
如果二叉树的两个节点深度相同,但 父节点不同 ,则它们是一对堂兄弟节点。
我们给出了具有唯一值的二叉树的根节点 root ,以及树中两个不同节点的值 x 和 y 。
只有与值 x 和 y 对应的节点是堂兄弟节点时,才返回 true 。否则,返回 false。
示例 1:
输入:root = [1,2,3,4], x = 4, y = 3
输出:false
示例 2:
输入:root = [1,2,3,null,4,null,5], x = 5, y = 4
输出:true
输入:root = [1,2,3,null,4], x = 2, y = 3
输出:false
提示:
二叉树的节点数介于 2 到 100 之间。
每个节点的值都是唯一的、范围为 1 到 100 的整数。
试题链接
https://leetcode-cn.com/problems/cousins-in-binary-tree/
解题思路
这道题虽然标记为bfs,但看到使用bfs的好像并不多啊。个人认为对于这题来说bfs还更加好理解一点。
树里面的bfs就是层序遍历。
那么层序遍历的模板如下:
Queue<TreeNode>q=new LinkedList<>();
q.add(root);
while(!q.isEmpty()){
int size=q.size();
//使用size是可以将这一层的结点进行操作,而不用进入下一层
while(size-->0){
TreeNode temp=q.poll();
if(temp.left!=null){
//进行相关操作
q.add(temp.left);
}
if(temp.right!=null){
//进行相关操作
q.add(temp.right);
}
}
}
那么对于这道题来说,则需要对模板进行一些添加。
首先是要在树里面找x和y对应的结点,那么可以增加以下对象:
//x对应的结点
TreeNode xNode=null;
//y对应的结点
TreeNode yNode=null;
然后还要判断两者的父节点是否一样,那么还要再添加父节点:
//x对应的结点的父节点
TreeNode xFather=null;
//y对应结点的父节点
TreeNode yFather=null;
那么再层序遍历的过程中就可以找相应的结点:
if(temp.left!=null){
q.add(temp.left);
//找结点
if(temp.left.val==x){xNode=temp.left;xFather=temp;}
if(temp.left.val==y){yNode=temp.left;yFather=temp;}
}
if(temp.right!=null){
q.add(temp.right);
//找节点
if(temp.right.val==x){xNode=temp.right;xFather=temp;}
if(temp.right.val==y){yNode=temp.right;yFather=temp;}
}
接下来对于找到结点的情况进行讨论:
1.如果在遍历过程中两个结点都没找到,那么继续找(什么也不做):
if(xNode==null&&yNode==null){}
2.如果两个结点都找到了,那么判断是否满足要求(父节点不同):
else if(xNode!=null&&yNode!=null){
return xFather!=yFather;
}
3.如果只找到一个结点,但是这层还没遍历完,那么就继续找;如果这层遍历完了就只找到了一个,说明辈分肯定不同,直接返回false:
else if(size==0)
return false;
即:
if(xNode==null&&yNode==null){}
else if(xNode!=null&&yNode!=null){
return xFather==yFather;
}
else if(size==0)
return false;
那么完整代码如下:
代码
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isCousins(TreeNode root, int x, int y) {
//这两个值任意一个都不会出现在根节点
if(root==null||root.val==x||root.val==y)
return false;
Queue<TreeNode>q=new LinkedList<>();
q.add(root);
//对应x值的结点
TreeNode xNode=null;
//对应y值的结点
TreeNode yNode=null;
//对应x值的父亲的结点
TreeNode xFather=null;
//对应y值的父亲的结点
TreeNode yFather=null;
//开始bfs
while(!q.isEmpty()){
int size=q.size();
while(size-->0){
TreeNode temp=q.poll();
if(temp.left!=null){
q.add(temp.left);
//找节点
if(temp.left.val==x){xNode=temp.left;xFather=temp;}
if(temp.left.val==y){yNode=temp.left;yFather=temp;}
}
if(temp.right!=null){
q.add(temp.right);
//找节点
if(temp.right.val==x){xNode=temp.right;xFather=temp;}
if(temp.right.val==y){yNode=temp.right;yFather=temp;}
}
//两个节点都没找到,什么也不做
if(xNode==null&&yNode==null){}
//两个节点都找到了,那么判断它们是不是堂兄弟节点
else if(xNode!=null&&yNode!=null){
//如果父亲结点不相等,说明是堂兄弟结点
return xFather!=yFather;
}else if(size==0){
return false;
}//这层遍历完了,但是有一个节点找到了,另外一个没找到
}
}
return false;
}
}
(二)、二叉树的层序遍历 II
方法一:广度优先搜索
树的层次遍历可以使用广度优先搜索实现。从根节点开始搜索,每次遍历同一层的全部节点,使用一个列表存储该层的节点值。
如果要求从上到下输出每一层的节点值,做法是很直观的,在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的尾部。这道题要求从下到上输出每一层的节点值,只要对上述操作稍作修改即可:在遍历完一层节点之后,将存储该层节点值的列表添加到结果列表的头部。
为了降低在结果列表的头部添加一层节点值的列表的时间复杂度,结果列表可以使用链表的结构,在链表头部添加一层节点值的列表的时间复杂度是 O(1)。在 Java 中,由于我们需要返回的 List 是一个接口,这里可以使用链表实现;而 C++ 或 Python 中,我们需要返回一个 vector 或 list,它不方便在头部插入元素(会增加时间开销),所以我们可以先用尾部插入的方法得到从上到下的层次遍历列表,然后再进行反转。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> levelOrder = new LinkedList<List<Integer>>();
if (root == null) {
return levelOrder;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<Integer>();
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
TreeNode left = node.left, right = node.right;
if (left != null) {
queue.offer(left);
}
if (right != null) {
queue.offer(right);
}
}
levelOrder.add(0, level);
}
return levelOrder;
}
}
复杂度分析
时间复杂度:O(n),其中 nnn 是二叉树中的节点个数。每个节点访问一次,结果列表使用链表的结构时,在结果列表头部添加一层节点值的列表的时间复杂度是 O(1),因此总时间复杂度是 O(n)。
空间复杂度:O(n),其中 nnn 是二叉树中的节点个数。空间复杂度取决于队列开销,队列中的节点个数不会超过 nnn。
(三)、二叉树的锯齿形层序遍历
要求我们按层数的奇偶来决定每一层的输出顺序。规定二叉树的根节点为第 0 层,如果当前层数是偶数,从左至右输出当前层的节点值,否则,从右至左输出当前层的节点值。
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> ans = new LinkedList<List<Integer>>();
if (root == null) {
return ans
}
Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();
nodeQueue.offer(root);
boolean isOrderLeft = true;
while (!nodeQueue.isEmpty()) {
Deque<Integer> levelList = new LinkedList<Integer>();
int size = nodeQueue.size();
for (int i = 0; i < size; ++i) {
TreeNode curNode = nodeQueue.poll();
if (isOrderLeft) {
levelList.offerLast(curNode.val);
} else {
levelList.offerFirst(curNode.val);
}
if (curNode.left != null) {
nodeQueue.offer(curNode.left);
}
if (curNode.right != null) {
nodeQueue.offer(curNode.right);
}
}
ans.add(new LinkedList<Integer>(levelList));
isOrderLeft = !isOrderLeft;
}
return ans;
}
}
复杂度分析
时间复杂度:O(N),其中 NNN 为二叉树的节点数。每个节点会且仅会被遍历一次。
空间复杂度:O(N)。我们需要维护存储节点的队列和存储节点值的双端队列,空间复杂度为 O(N)。
二、数据结构:图中的 BFS
(一)、单词接龙
参考链接:推荐阅读原题题解,本博文仅做本人自己参考总结
作者:liweiwei1419
链接:https://leetcode-cn.com/problems/word-ladder/solution/yan-du-you-xian-bian-li-shuang-xiang-yan-du-you-2/
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/word-ladder/solution/dan-ci-jie-
方法一:广度优先搜索
1. 思路一:建立图
本题要求的是最短转换序列的长度,看到最短首先想到的就是广度优先搜索。想到广度优先搜索自然而然的就能想到图,但是本题并没有直截了当的给出图的模型,因此我们需要把它抽象成图的模型。
我们可以把每个单词都抽象为一个点,如果两个单词可以只改变一个字母进行转换,那么说明他们之间有一条双向边。因此我们只需要把满足转换条件的点相连,就形成了一张图。
基于该图,我们以 beginWord 为图的起点,以 endWord 为终点进行广度优先搜索,寻找 beginWord 到 endWord 的最短路径。
基于上面的思路我们考虑如何编程实现。
首先为了方便表示,我们先给每一个单词标号,即给每个单词分配一个 id。创建一个由单词 word 到 id 对应的映射 wordId,并将 beginWord 与 wordList 中所有的单词都加入这个映射中。之后我们检查 endWord 是否在该映射内,若不存在,则输入无解。我们可以使用哈希表实现上面的映射关系。
然后我们需要建图,依据朴素的思路,我们可以枚举每一对单词的组合,判断它们是否恰好相差一个字符,以判断这两个单词对应的节点是否能够相连。但是这样效率太低,我们可以优化建图。
具体地,我们可以创建虚拟节点。对于单词 hit,我们创建三个虚拟节点 it、ht、hi*,并让 hit 向这三个虚拟节点分别连一条边即可。如果一个单词能够转化为 hit,那么该单词必然会连接到这三个虚拟节点之一。对于每一个单词,我们枚举它连接到的虚拟节点,把该单词对应的 id 与这些虚拟节点对应的 id 相连即可。
最后我们将起点加入队列开始广度优先搜索,当搜索到终点时,我们就找到了最短路径的长度。注意因为添加了虚拟节点,所以我们得到的距离为实际最短路径长度的两倍。同时我们并未计算起点对答案的贡献,所以我们应当返回距离的一半再加一的结果。
代码一
class Solution {
Map<String, Integer> wordId = new HashMap<String, Integer>();
List<List<Integer>> edge = new ArrayList<List<Integer>>();
int nodeNum = 0;
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
for (String word : wordList) {
addEdge(word);
}
addEdge(beginWord);
if (!wordId.containsKey(endWord)) {
return 0;
}
int[] dis = new int[nodeNum];
Arrays.fill(dis, Integer.MAX_VALUE);
int beginId = wordId.get(beginWord), endId = wordId.get(endWord);
dis[beginId] = 0;
Queue<Integer> que = new LinkedList<Integer>();
que.offer(beginId);
while (!que.isEmpty()) {
int x = que.poll();
if (x == endId) {
return dis[endId] / 2 + 1;
}
for (int it : edge.get(x)) {
if (dis[it] == Integer.MAX_VALUE) {
dis[it] = dis[x] + 1;
que.offer(it);
}
}
}
return 0;
}
public void addEdge(String word) {
addWord(word);
int id1 = wordId.get(word);
char[] array = word.toCharArray();
int length = array.length;
for (int i = 0; i < length; ++i) {
char tmp = array[i];
array[i] = '*';
String newWord = new String(array);
addWord(newWord);
int id2 = wordId.get(newWord);
edge.get(id1).add(id2);
edge.get(id2).add(id1);
array[i] = tmp;
}
}
public void addWord(String word) {
if (!wordId.containsKey(word)) {
wordId.put(word, nodeNum++);
edge.add(new ArrayList<Integer>());
}
}
}
复杂度分析
时间复杂度: O ( N × C 2 ) O(N \times C^2) O(N×C2)。其中 NNN 为 wordList 的长度, C C C 为列表中单词的长度。
建图过程中,对于每一个单词,我们需要枚举它连接到的所有虚拟节点,时间复杂度为 O©,将这些单词加入到哈希表中,时间复杂度为 O ( N × C ) O(N \times C) O(N×C),因此总时间复杂度为 O ( N × C ) O(N \times C) O(N×C)。
广度优先搜索的时间复杂度最坏情况下是 O ( N × C ) O(N \times C) O(N×C)。每一个单词需要拓展出 O ( C ) O(C) O(C)个虚拟节点,因此节点数 O ( N × C ) O(N \times C) O(N×C)。
空间复杂度: O ( N × C 2 ) O(N \times C^2) O(N×C2)。其中 N 为 wordList 的长度, C C C 为列表中单词的长度。哈希表中包含 O ( N × C ) O(N \times C) O(N×C)个节点,每个节点占用空间 O ( C ) O(C) O(C),因此总的空间复杂度为 O ( N × C 2 ) O(N \times C^2) O(N×C2)。
2. 思路二:使用朴素思想,不建立图
如果一开始就构建图,每一个单词都需要和除它以外的另外的单词进行比较,复杂度是 O(NwordLen)O(N \rm{wordLen})O(NwordLen),这里 NNN 是单词列表的长度;
为此,我们在遍历一开始,把所有的单词列表放进一个哈希表中,然后在遍历的时候构建图,每一次得到在单词列表里可以转换的单词,复杂度是 O(26×wordLen)O(26 \times \rm{wordLen})O(26×wordLen),借助哈希表,找到邻居与 NNN 无关;
使用 BFS 进行遍历,需要的辅助数据结构是:
队列;
visited 集合。说明:可以直接在 wordSet (由 wordList 放进集合中得到)里做删除。但更好的做法是新开一个哈希表,遍历过的字符串放进哈希表里。这种做法具有普遍意义。绝大多数在线测评系统和应用场景都不会在意空间开销。
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
public class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
Set<String> wordSet = new HashSet<>(wordList);
if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
return 0;
}
wordSet.remove(beginWord);
// 第 2 步:图的广度优先遍历,必须使用队列和表示是否访问过的 visited 哈希表
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
Set<String> visited = new HashSet<>();
visited.add(beginWord);
// 第 3 步:开始广度优先遍历,包含起点,因此初始化的时候步数为 1
int step = 1;
while (!queue.isEmpty()) {
int currentSize = queue.size();
for (int i = 0; i < currentSize; i++) {
// 依次遍历当前队列中的单词
String currentWord = queue.poll();
// 如果 currentWord 能够修改 1 个字符与 endWord 相同,则返回 step + 1
if (changeWordEveryOneLetter(currentWord, endWord, queue, visited, wordSet)) {
return step + 1;
}
}
step++;
}
return 0;
}
/**
* 尝试对 currentWord 修改每一个字符,看看是不是能与 endWord 匹配
*
* @param currentWord
* @param endWord
* @param queue
* @param visited
* @param wordSet
* @return
*/
private boolean changeWordEveryOneLetter(String currentWord, String endWord,
Queue<String> queue, Set<String> visited, Set<String> wordSet) {
char[] charArray = currentWord.toCharArray();
for (int i = 0; i < endWord.length(); i++) {
// 先保存,然后恢复
char originChar = charArray[i];
for (char k = 'a'; k <= 'z'; k++) {
if (k == originChar) {
continue;
}
charArray[i] = k;
String nextWord = String.valueOf(charArray);
if (wordSet.contains(nextWord)) {
if (nextWord.equals(endWord)) {
return true;
}
if (!visited.contains(nextWord)) {
queue.add(nextWord);
// 注意:添加到队列以后,必须马上标记为已经访问
visited.add(nextWord);
}
}
}
// 恢复
charArray[i] = originChar;
}
return false;
}
}
方法二:双向广度优先搜索
1.思路一
根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。考虑一个简单的二叉树,每一层都是满二叉树的扩展,节点的数量会以 2 为底数呈指数增长。
如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始,另一边从 endWord 开始。我们每次从两边各扩展一层节点,当发现某一时刻两边都访问过同一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而提高代码运行效率。
代码一
class Solution {
Map<String, Integer> wordId = new HashMap<String, Integer>();
List<List<Integer>> edge = new ArrayList<List<Integer>>();
int nodeNum = 0;
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
for (String word : wordList) {
addEdge(word);
}
addEdge(beginWord);
if (!wordId.containsKey(endWord)) {
return 0;
}
int[] disBegin = new int[nodeNum];
Arrays.fill(disBegin, Integer.MAX_VALUE);
int beginId = wordId.get(beginWord);
disBegin[beginId] = 0;
Queue<Integer> queBegin = new LinkedList<Integer>();
queBegin.offer(beginId);
int[] disEnd = new int[nodeNum];
Arrays.fill(disEnd, Integer.MAX_VALUE);
int endId = wordId.get(endWord);
disEnd[endId] = 0;
Queue<Integer> queEnd = new LinkedList<Integer>();
queEnd.offer(endId);
while (!queBegin.isEmpty() && !queEnd.isEmpty()) {
int queBeginSize = queBegin.size();
for (int i = 0; i < queBeginSize; ++i) {
int nodeBegin = queBegin.poll();
if (disEnd[nodeBegin] != Integer.MAX_VALUE) {
return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;
}
for (int it : edge.get(nodeBegin)) {
if (disBegin[it] == Integer.MAX_VALUE) {
disBegin[it] = disBegin[nodeBegin] + 1;
queBegin.offer(it);
}
}
}
int queEndSize = queEnd.size();
for (int i = 0; i < queEndSize; ++i) {
int nodeEnd = queEnd.poll();
if (disBegin[nodeEnd] != Integer.MAX_VALUE) {
return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;
}
for (int it : edge.get(nodeEnd)) {
if (disEnd[it] == Integer.MAX_VALUE) {
disEnd[it] = disEnd[nodeEnd] + 1;
queEnd.offer(it);
}
}
}
}
return 0;
}
public void addEdge(String word) {
addWord(word);
int id1 = wordId.get(word);
char[] array = word.toCharArray();
int length = array.length;
for (int i = 0; i < length; ++i) {
char tmp = array[i];
array[i] = '*';
String newWord = new String(array);
addWord(newWord);
int id2 = wordId.get(newWord);
edge.get(id1).add(id2);
edge.get(id2).add(id1);
array[i] = tmp;
}
}
public void addWord(String word) {
if (!wordId.containsKey(word)) {
wordId.put(word, nodeNum++);
edge.add(new ArrayList<Integer>());
}
}
}
复杂度分析
时间复杂度:O(N×C2)O(N \times C^2)O(N×C2)。其中 N 为 wordList 的长度,C 为列表中单词的长度。
建图过程中,对于每一个单词,我们需要枚举它连接到的所有虚拟节点,时间复杂度为 O(C),将这些单词加入到哈希表中,时间复杂度为 O(N×C)O(N \times C)O(N×C),因此总时间复杂度为 O(N×C)O(N \times C)O(N×C)。
双向广度优先搜索的时间复杂度最坏情况下是 O(N×C)O(N \times C)O(N×C)。每一个单词需要拓展出 O(C)个虚拟节点,因此节点数 O(N×C)O(N \times C)O(N×C)。
空间复杂度:O(N×C2)O(N \times C^2)O(N×C2)。其中 NNN 为 wordList 的长度,CCC 为列表中单词的长度。哈希表中包含 O(N×C)O(N \times C)O(N×C) 个节点,每个节点占用空间 O(C)O(C)O(C),因此总的空间复杂度为 O(N×C2)O(N \times C^2)O(N×C2)。
https://leetcode-cn.com/problems/word-ladder/solution/dan-ci-jie-long-by-leetcode-solution/
2.思路二
已知目标顶点的情况下,可以分别从起点和目标顶点(终点)执行广度优先遍历,直到遍历的部分有交集。这种方式搜索的单词数量会更小一些;
更合理的做法是,每次从单词数量小的集合开始扩散;
这里 beginVisited 和 endVisited 交替使用,等价于单向 BFS 里使用队列,每次扩散都要加到总的 visited 里。
代码二
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 第 1 步:先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
Set<String> wordSet = new HashSet<>(wordList);
if (wordSet.size() == 0 || !wordSet.contains(endWord)) {
return 0;
}
// 第 2 步:已经访问过的 word 添加到 visited 哈希表里
Set<String> visited = new HashSet<>();
// 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用
Set<String> beginVisited = new HashSet<>();
beginVisited.add(beginWord);
Set<String> endVisited = new HashSet<>();
endVisited.add(endWord);
// 第 3 步:执行双向 BFS,左右交替扩散的步数之和为所求
int step = 1;
while (!beginVisited.isEmpty() && !endVisited.isEmpty()) {
// 优先选择小的哈希表进行扩散,考虑到的情况更少
if (beginVisited.size() > endVisited.size()) {
Set<String> temp = beginVisited;
beginVisited = endVisited;
endVisited = temp;
}
// 逻辑到这里,保证 beginVisited 是相对较小的集合,nextLevelVisited 在扩散完成以后,会成为新的 beginVisited
Set<String> nextLevelVisited = new HashSet<>();
for (String word : beginVisited) {
if (changeWordEveryOneLetter(word, endVisited, visited, wordSet, nextLevelVisited)) {
return step + 1;
}
}
// 原来的 beginVisited 废弃,从 nextLevelVisited 开始新的双向 BFS
beginVisited = nextLevelVisited;
step++;
}
return 0;
}
/**
* 尝试对 word 修改每一个字符,看看是不是能落在 endVisited 中,扩展得到的新的 word 添加到 nextLevelVisited 里
*
* @param word
* @param endVisited
* @param visited
* @param wordSet
* @param nextLevelVisited
* @return
*/
private boolean changeWordEveryOneLetter(String word, Set<String> endVisited,
Set<String> visited,
Set<String> wordSet,
Set<String> nextLevelVisited) {
char[] charArray = word.toCharArray();
for (int i = 0; i < word.length(); i++) {
char originChar = charArray[i];
for (char c = 'a'; c <= 'z'; c++) {
if (originChar == c) {
continue;
}
charArray[i] = c;
String nextWord = String.valueOf(charArray);
if (wordSet.contains(nextWord)) {
if (endVisited.contains(nextWord)) {
return true;
}
if (!visited.contains(nextWord)) {
nextLevelVisited.add(nextWord);
visited.add(nextWord);
}
}
}
// 恢复,下次再用
charArray[i] = originChar;
}
return false;
}
}
三、数据结构:网格中的 BFS
(一)、岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 300
- grid[i][j] 的值为 ‘0’ 或 ‘1’
试题链接
https://leetcode-cn.com/problems/number-of-islands/
解题思路
我们可以将二维网格看成一个无向图,竖直或水平相邻的 1 之间有边相连。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 1 ,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 1 都会被重新标记为 0 。
BFS 解法:
class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0){
return 0;
}
int rowNum = grid.length;
int colNum = grid[0].length;
int num_islands = 0;
for (int r = 0; r < rowNum; r++){
for (int c = 0; c < colNum; ++c){
if (grid[r][c] == '1'){
num_islands = num_islands + 1;
grid[r][c] = '0';
Queue<Integer> queue = new LinkedList();
//在网格类中使用整数排序代替元素位置;row = tem / colNum; col = tem % colNum;
queue.add(r * colNum + c);
while(!queue.isEmpty()){
int tem = queue.poll();
int row = tem / colNum;
int col = tem % colNum;
if (row - 1 >= 0 && grid[row-1][col] == '1'){
queue.offer((row-1)*colNum+col);
grid[row-1][col] = '0';
}
if (col + 1 < colNum && grid[row][col+1] == '1'){
queue.offer(row * colNum + col + 1);
grid[row][col+1] = '0';
}
if (row + 1 < rowNum && grid[row+1][col] == '1'){
queue.offer((row+1) * colNum + col);
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1]=='1'){
queue.offer(row * colNum + col -1);
grid[row][col - 1] = '0';
}
}
}
}
}
return num_islands;
}
}
复杂度分析
时间复杂度:O(MN),其中 M 和 N 分别为行数和列数。
空间复杂度:O(min(M,N)),在最坏情况下,整个网格均为陆地,队列的大小可以达到 min(M,N)。