給定一個二維的矩陣,包含 'X'
和 'O'
(字母 O)。java
找到全部被 'X'
圍繞的區域,並將這些區域裏全部的 'O'
用 'X'
填充。node
示例:面試
X X X X
X O O X
X X O X
X O X X
複製代碼
運行你的函數後,矩陣變爲:數組
X X X X
X X X X
X X X X
X O X X
複製代碼
解釋:bash
被圍繞的區間不會存在於邊界上,換句話說,任何邊界上的 'O'
都不會被填充爲 'X'
。 任何不在邊界上,或不與邊界上的 'O'
相連的 'O'
最終都會被填充爲 'X'
。若是兩個元素在水平或垂直方向相鄰,則稱它們是「相連」的。數據結構
這道題咱們拿到基本就能夠肯定是圖的dfs、bfs遍歷的題目了。題目中解釋說被包圍的區間不會存在於邊界上,因此咱們會想到邊界上的o要特殊處理,只要把邊界上的o特殊處理了,那麼剩下的o替換成x就能夠了。問題轉化爲,如何尋找和邊界聯通的o,咱們須要考慮以下狀況。分佈式
X X X X
X O O X
X X O X
X O O X
複製代碼
這時候的o是不作替換的。由於和邊界是連通的。爲了記錄這種狀態,咱們把這種狀況下的o換成#做爲佔位符,待搜索結束以後,遇到o替換爲x(和邊界不連通的o);遇到#,替換回o(和邊界連通的o)。函數
如何尋找和邊界聯通的o? 從邊界出發,對圖進行dfs和bfs便可。這裏簡單總結下dfs和dfs。ui
那麼基於上面這種想法,咱們有四種方式實現。this
class Solution {
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 從邊緣o開始搜索
boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
if (isEdge && board[i][j] == 'O') {
dfs(board, i, j);
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '#') {
board[i][j] = 'O';
}
}
}
}
public void dfs(char[][] board, int i, int j) {
if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
// board[i][j] == '#' 說明已經搜索過了.
return;
}
board[i][j] = '#';
dfs(board, i - 1, j); // 上
dfs(board, i + 1, j); // 下
dfs(board, i, j - 1); // 左
dfs(board, i, j + 1); // 右
}
}
複製代碼
非遞歸的方式,咱們須要記錄每一次遍歷過的位置,咱們用stack來記錄,由於它先進後出的特色。而位置咱們定義一個內部類Pos來標記橫座標和縱座標。注意的是,在寫非遞歸的時候,咱們每次查看stack頂,可是並不出stack,直到這個位置上下左右都搜索不到的時候出Stack。
class Solution {
public class Pos{
int i;
int j;
Pos(int i, int j) {
this.i = i;
this.j = j;
}
}
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 從邊緣第一個是o的開始搜索
boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
if (isEdge && board[i][j] == 'O') {
dfs(board, i, j);
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '#') {
board[i][j] = 'O';
}
}
}
}
public void dfs(char[][] board, int i, int j) {
Stack<Pos> stack = new Stack<>();
stack.push(new Pos(i, j));
board[i][j] = '#';
while (!stack.isEmpty()) {
// 取出當前stack 頂, 不彈出.
Pos current = stack.peek();
// 上
if (current.i - 1 >= 0
&& board[current.i - 1][current.j] == 'O') {
stack.push(new Pos(current.i - 1, current.j));
board[current.i - 1][current.j] = '#';
continue;
}
// 下
if (current.i + 1 <= board.length - 1
&& board[current.i + 1][current.j] == 'O') {
stack.push(new Pos(current.i + 1, current.j));
board[current.i + 1][current.j] = '#';
continue;
}
// 左
if (current.j - 1 >= 0
&& board[current.i][current.j - 1] == 'O') {
stack.push(new Pos(current.i, current.j - 1));
board[current.i][current.j - 1] = '#';
continue;
}
// 右
if (current.j + 1 <= board[0].length - 1
&& board[current.i][current.j + 1] == 'O') {
stack.push(new Pos(current.i, current.j + 1));
board[current.i][current.j + 1] = '#';
continue;
}
// 若是上下左右都搜索不到,本次搜索結束,彈出stack
stack.pop();
}
}
}
複製代碼
dfs非遞歸的時候咱們用stack來記錄狀態,而bfs非遞歸,咱們則用隊列來記錄狀態。和dfs不一樣的是,dfs中搜索上下左右,只要搜索到一個知足條件,咱們就順着該方向繼續搜索,因此你能夠看到dfs代碼中,只要知足條件,就入Stack,而後continue本次搜索,進行下一次搜索,直到搜索到沒有知足條件的時候出stack。而bfs中,咱們要把上下左右知足條件的都入隊,因此搜索的時候就不能continue。你們能夠對比下二者的代碼,體會bfs和dfs的差別。
class Solution {
public class Pos{
int i;
int j;
Pos(int i, int j) {
this.i = i;
this.j = j;
}
}
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
int m = board.length;
int n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 從邊緣第一個是o的開始搜索
boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
if (isEdge && board[i][j] == 'O') {
bfs(board, i, j);
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
}
if (board[i][j] == '#') {
board[i][j] = 'O';
}
}
}
}
public void bfs(char[][] board, int i, int j) {
Queue<Pos> queue = new LinkedList<>();
queue.add(new Pos(i, j));
board[i][j] = '#';
while (!queue.isEmpty()) {
Pos current = queue.poll();
// 上
if (current.i - 1 >= 0
&& board[current.i - 1][current.j] == 'O') {
queue.add(new Pos(current.i - 1, current.j));
board[current.i - 1][current.j] = '#';
// 沒有continue.
}
// 下
if (current.i + 1 <= board.length - 1
&& board[current.i + 1][current.j] == 'O') {
queue.add(new Pos(current.i + 1, current.j));
board[current.i + 1][current.j] = '#';
}
// 左
if (current.j - 1 >= 0
&& board[current.i][current.j - 1] == 'O') {
queue.add(new Pos(current.i, current.j - 1));
board[current.i][current.j - 1] = '#';
}
// 右
if (current.j + 1 <= board[0].length - 1
&& board[current.i][current.j + 1] == 'O') {
queue.add(new Pos(current.i, current.j + 1));
board[current.i][current.j + 1] = '#';
}
}
}
}
複製代碼
bfs 通常咱們不會去涉及,並且比較繞,以前咱們惟一A過的用bfs遞歸的方式是層序遍歷二叉樹的時候能夠用遞歸的方式。
並查集這種數據結構好像你們不太經常使用,實際上頗有用,我在實際的production code中用過並查集。並查集經常使用來解決連通性的問題,即將一個圖中連通的部分劃分出來。當咱們判斷圖中兩個點之間是否存在路徑時,就能夠根據判斷他們是否在一個連通區域。 而這道題咱們其實求解的就是和邊界的O在一個連通區域的的問題。
並查集的思想就是,同一個連通區域內的全部點的根節點是同一個。將每一個點映射成一個數字。先假設每一個點的根節點就是他們本身,而後咱們以此輸入連通的點對,而後將其中一個點的根節點賦成另外一個節點的根節點,這樣這兩個點所在連通區域又相互連通了。 並查集的主要操做有:
find(int m):這是並查集的基本操做,查找m的根節點。
isConnected(int m,int n):判斷m,n兩個點是否在一個連通區域。
union(int m,int n):合併m,n兩個點所在的連通區域。
class UnionFind {
int[] parents;
public UnionFind(int totalNodes) {
parents = new int[totalNodes];
for (int i = 0; i < totalNodes; i++) {
parents[i] = i;
}
}
// 合併連通區域是經過find來操做的, 即看這兩個節點是否是在一個連通區域內.
void union(int node1, int node2) {
int root1 = find(node1);
int root2 = find(node2);
if (root1 != root2) {
parents[root2] = root1;
}
}
int find(int node) {
while (parents[node] != node) {
// 當前節點的父節點 指向父節點的父節點.
// 保證一個連通區域最終的parents只有一個.
parents[node] = parents[parents[node]];
node = parents[node];
}
return node;
}
boolean isConnected(int node1, int node2) {
return find(node1) == find(node2);
}
}
複製代碼
咱們的思路是把全部邊界上的O看作一個連通區域。遇到O就執行並查集合並操做,這樣全部的O就會被分紅兩類
因爲並查集咱們通常用一維數組來記錄,方便查找parants,因此咱們將二維座標用node函數轉化爲一維座標。
public void solve(char[][] board) {
if (board == null || board.length == 0)
return;
int rows = board.length;
int cols = board[0].length;
// 用一個虛擬節點, 邊界上的O 的父節點都是這個虛擬節點
UnionFind uf = new UnionFind(rows * cols + 1);
int dummyNode = rows * cols;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (board[i][j] == 'O') {
// 遇到O進行並查集操做合併
if (i == 0 || i == rows - 1 || j == 0 || j == cols - 1) {
// 邊界上的O,把它和dummyNode 合併成一個連通區域.
uf.union(node(i, j), dummyNode);
} else {
// 和上下左右合併成一個連通區域.
if (i > 0 && board[i - 1][j] == 'O')
uf.union(node(i, j), node(i - 1, j));
if (i < rows - 1 && board[i + 1][j] == 'O')
uf.union(node(i, j), node(i + 1, j));
if (j > 0 && board[i][j - 1] == 'O')
uf.union(node(i, j), node(i, j - 1));
if (j < cols - 1 && board[i][j + 1] == 'O')
uf.union(node(i, j), node(i, j + 1));
}
}
}
}
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (uf.isConnected(node(i, j), dummyNode)) {
// 和dummyNode 在一個連通區域的,那麼就是O;
board[i][j] = 'O';
} else {
board[i][j] = 'X';
}
}
}
}
int node(int i, int j) {
return i * cols + j;
}
}
複製代碼