Conversation
| class Solution { | ||
| public int numIslands(char[][] grid) { | ||
| int numberOfIslands = 0; | ||
| boolean[][] traversed = new boolean[grid.length][grid[0].length]; |
There was a problem hiding this comment.
step2 のように下の行と入れ替えて、
boolean[][] traversed = new boolean[m][n];
としたほうがシンプルになると思います。
| int n = grid[0].length; | ||
| for (int i = 0; i < m; i++) { | ||
| for (int j = 0; j < n; j++) { | ||
| if (grid[i][j] == '0' || traversed[i][j] == true) { |
There was a problem hiding this comment.
if (grid[i][j] == '0' || traversed[i][j]) { のほうがシンプルだと思います。
| } | ||
|
|
||
| private void traverseAdjacentCells(char[][] grid, boolean[][] traversed, int m, int n, int i, int j) { | ||
| boolean isOutOfBounds = i < 0 || i >= m || j < 0 || j >= n; |
There was a problem hiding this comment.
boolean isInside = 0 <= i && i < m && 0 <= j && j < n;
if (!isInside || grid[i][j] == '0' || traversed[i][j]) {
return;
}
としたほうが読みやすく感じます。
不等号の式の左右の方向は、比較されるほうを左に持ってくる派と、数直線上に一直線上に並べる派がいるように思います。実際の現場においては、チームのやり方に合わせることをお勧めします。
また、一般に、変数には肯定的な意味合いを持たせ、式の中で ! を使って否定したほうが読みやすくなると思います。今回の場合とは違うのですが、否定的な意味合いの変数を ! を使って否定すると、二重否定による肯定になり、認知負荷が上がり、読みにくく感じます。
There was a problem hiding this comment.
ありがとうございます。
肯定的な意味合いの単語で構成され、判定の際に否定演算子を付与した上記の例の方が見やすく感じました。
また、たしかに数直線上に一直線上に並べる方が読みやすく感じました。今回特に意識せずに書いたら比較される方を左にしていましたが、仰る通り実際の業務ではチームのやり方に合せられればと思います。
ちなみに、どちらの書き方にするかについて、これまでの仕事においてチームで決まってたことってありましたか?
今までこれについて決めていたチームを見たことがなかったので、外資の大きい企業ではそういうこともあるのかな、と興味本位で気になりました。もし心当たりあれば参考までに教えて頂けると嬉しいです。
There was a problem hiding this comment.
チームで決まっていたことはなかったように思います。 G 社時代に、自分が数直線上に並べる書き方をしていたところ、コードレビューで小西さんから比べられる変数を左側に持ってきた方が読みやすいと言われ、自分は数直線上に並べたほうが読みやすいと主張し、許してもらったことがあります。
There was a problem hiding this comment.
お二人ともありがとうございます!なるほどです。
(nodchip さんは小西さんと一緒に働かれてたんですね)
| return numberOfIslands; | ||
| } | ||
|
|
||
| private void traverseAdjacentCells(char[][] grid, boolean[][] traversed, int m, int n, int i, int j) { |
There was a problem hiding this comment.
1 <= m, n <= 300 のため、最大で 90,000 回再帰する可能性があります。これによりスタックオーバーフローを引き起こす可能性があります。この点については考慮されましたか?
| boolean[][] traversed = new boolean[grid.length][grid[0].length]; | ||
| int m = grid.length; | ||
| int n = grid[0].length; | ||
| for (int i = 0; i < m; i++) { |
There was a problem hiding this comment.
個人的には二次元の座標を表す変数は (x, y)、(row, column)、(r, c) を使うことが多いです。ただ、最近は (i, j) も使われているようです。
https://x.com/nodchip/status/1809223275442315414
ただし、 (i, j) が横方向と縦方向のどちらを表しているかについては注意が必要です。
|
|
||
| ## 再帰関数の代わりにスタックを使う | ||
|
|
||
| (Leetcode の constraints ではそうならないとは書いてあるものの) 大きな grid が渡されたらスタックオーバーフローになる可能性があるため、スタックを用いたより安全な実装にしてみる。 |
There was a problem hiding this comment.
Leetcode の constraints ではそうならないとは書いてある
これはどのように書かれていましたか?
There was a problem hiding this comment.
すみません、こちら勘違いでした。別のコメントでも指摘頂いている通り最大90,000スタックフレームが積まれるので、スタックオーバーフローになる可能性はあると思います。
確か oda さんが以前 Java の再帰呼び出しの限界について資料を貼ってくれていたので、そちら確認の上、後ほど訂正します。
There was a problem hiding this comment.
以下 oda さんの投稿からの引用です。Leetcode の constraints には "1 <= m, n <= 300" とあり、別のコメントでも指摘して頂いている通り 90,000 回再帰する可能性がありますので、スタックオーバーフローが起こる可能性があることは constraints からも明らかです。
Java は環境依存だけれども、1M くらいなので1万も行けばいいほうだよな、という感覚はあります。
from: https://discord.com/channels/1084280443945353267/1235829049511903273/1236256946403807323
| ``` | ||
|
|
||
| - "This class is likely to be faster than Stack when used as a stack" とある ArrayDeque を Stack クラスの代わりに使ってみた | ||
| - nodachip さんも言及されてた: https://github.com/goto-untrapped/Arai60/pull/38#discussion_r1683649014 |
There was a problem hiding this comment.
失礼しました😇(普段の呼び方に引きずられた…)
| - しかしメモリ消費量は1~2割ほど削減される | ||
| - 再帰関数を使った場合と比べおよそ2倍の処理時間がかかっている | ||
| - スタック的な役割のオブジェクトの利用よりも、スタックフレームの利用の方がよっぽど最適化されているということなのだろうか? | ||
| - 他に致命的な要因が無いかと record の代わりに整数型配列を使ったりもしたが、それほど大きな影響はなかった |
There was a problem hiding this comment.
ArrayDeque に挿入するオブジェクトのヒープメモリの確保に時間がかかっている可能性があります。
一般に new は重い処理ですので、自然に避けられるのであれば避けたほうが無難だと思います。
There was a problem hiding this comment.
ありがとうございます。現在セルの数だけ new するようになっているので、避けるように工夫してみます。
There was a problem hiding this comment.
ArrayDeque の宣言を numIslands で一度だけ実施し、走査の度に実行される traverseAdjacentLands に渡すように修正したところ、興味深いことにむしろ少し処理が遅くなりました (7ms -> 9ms)。
こちら修正後の実装です。
class Solution {
private record Cell(int row, int column) {};
public int numIslands(char[][] grid) {
int islandCount = 0;
int rowCount = grid.length;
int columnCount = grid[0].length;
boolean[][] traversedLands = new boolean[rowCount][columnCount];
Deque<Cell> traversingCells = new ArrayDeque<>();
for (int row = 0; row < rowCount; row++) {
for (int column = 0; column < columnCount; column++) {
if (grid[row][column] == '0' || traversedLands[row][column] == true) {
continue;
}
traverseAdjacentLands(grid, traversedLands, traversingCells, rowCount, columnCount, row, column);
islandCount++;
}
}
return islandCount;
}
private void traverseAdjacentLands(char[][] grid, boolean[][] traversedLands, Deque<Cell> traversingCells, int rowCount, int columnCount, int row, int column) {
traversingCells.add(new Cell(row, column));
while (!traversingCells.isEmpty()) {
Cell cell = traversingCells.removeFirst();
boolean isOutOfBounds = !(cell.row >= 0 && cell.row < rowCount && cell.column >= 0 && cell.column < columnCount);
if (isOutOfBounds || grid[cell.row][cell.column] == '0' || traversedLands[cell.row][cell.column] == true) {
continue;
}
traversedLands[cell.row][cell.column] = true;
traversingCells.add(new Cell(cell.row + 1, cell.column));
traversingCells.add(new Cell(cell.row - 1, cell.column));
traversingCells.add(new Cell(cell.row, cell.column + 1));
traversingCells.add(new Cell(cell.row, cell.column - 1));
}
}
}There was a problem hiding this comment.
JVM が実行していた何らかの最適化がされなくなり、その影響と修正の結果生じる関数への値渡し (厳密に言うと参照値の値渡しでしょうか) のオーバーヘッドが合わさって実行時間がよりかかるようになってしまったのかな、と推測しています。
もし他に原因になってそうなものがあればご教示頂けますと嬉しいです。
There was a problem hiding this comment.
ArrayDeque の宣言を numIslands で一度だけ実施し、走査の度に実行される traverseAdjacentLands に渡すように修正したところ、興味深いことにむしろ少し処理が遅くなりました (7ms -> 9ms)。
他の方がおっしゃっていたのですが、 7ms -> 9ms というのが LeetCode 上での実行時間のことであれば、計測誤差が大きいため、あまり気にしないほうが良いと思います。ただ、桁が変わってくる場合には、根本的な部分の処理不可が変わった可能性もあるため、気にしたほうが良いかもしれません。
There was a problem hiding this comment.
はい、 Leetcode 上の実行時間のことです。そうですね、仕様上それなりの計測誤差が発生し得るのはしょうがないので、桁が変わるぐらいの変化で無い限りは基本的に気にしないようにしようと思います。
| return; | ||
| } | ||
|
|
||
| grid[row][column] = '0'; |
There was a problem hiding this comment.
'0'がマジックナンバー化してしまってるので名前をつけてあげたほうが良いと思います
あと、訪問済みとするなら訪問済みを表現するための他の値を使ったほうがいいかと
grid[row][column] = '0';が海に塗り替える操作なのか、訪問済みとする操作なのか読者が意図を読み取れないので
There was a problem hiding this comment.
ありがとうございます。修正して Step 4 の解答に反映しました。
| int islandCount = 0; | ||
| int rowCount = grid.length; | ||
| int columnCount = grid[0].length; | ||
| boolean[][] traversedLands = new boolean[rowCount][columnCount]; |
There was a problem hiding this comment.
型と名前が合ってないと感じました
traversedLandsという変数名に格納されるデータは何かしらLandを表現するものであるべきかなと
この変数は訪問済みかどうかを表す真偽値を格納するのでisVisitedとかで十分なのではないでしょうか
この問題も疑問形で始まる変数名なら大丈夫かなと
There was a problem hiding this comment.
確かにそうですね。修正して Step 4 の解答に反映しました。
|
|
||
| grid[row][column] = '0'; | ||
| if (row - 1 >= 0 && grid[row - 1][column] == '1') { | ||
| unionFindIslands.union(row * columnCount + column, (row - 1) * columnCount + column); |
There was a problem hiding this comment.
row * columnCount + columnを使いまわされているので変数に入れてもいいかもですね。
|
全体的に読みやすいと感じました。 |
|
|
||
| private void traverseAdjacentLands(char[][] grid, boolean[][] traversedLands, int rowCount, int columnCount, int row, int column) { | ||
| Deque<Cell> traversingCells = new ArrayDeque<>(); | ||
| traversingCells.add(new Cell(row, column)); |
There was a problem hiding this comment.
Deque, removeFirst() を使われているので、addLast() のほうがなじむ気がします。
There was a problem hiding this comment.
ありがとうございます!こちらレビューで指摘してたのに自分のコードには反映出来てませんでしたね😇 Step 4 の実装に反映させました。
|
Java の裏側の処理など、参考になりました🙏 |
| } | ||
|
|
||
| private int find(int i) { | ||
| if (parent[i] != i) { |
There was a problem hiding this comment.
細かいですが、個人的にはparent[i] == iの方を先に書きたい気持ちになりました(特殊な処理を先に書きたいので)
There was a problem hiding this comment.
確かにそうした方がよりしっくりくるなとと思いました。ありがとうございます。
| if (row - 1 >= 0 && grid[row - 1][column] == '1') { | ||
| unionFindIslands.union(root, (row - 1) * columnCount + column); | ||
| } | ||
| if (row + 1 < rowCount && grid[row + 1][column] == '1') { | ||
| unionFindIslands.union(root, (row + 1) * columnCount + column); | ||
| } | ||
| if (column - 1 >= 0 && grid[row][column - 1] == '1') { | ||
| unionFindIslands.union(root, row * columnCount + column - 1); | ||
| } | ||
| if (column + 1 < columnCount && grid[row][column + 1] == '1') { | ||
| unionFindIslands.union(root, root + 1); | ||
| } |
There was a problem hiding this comment.
自分の脳内処理が間違ってたら申し訳ないのですが、これって4方向繋ぐ必要ありましたっけ(左と上だけでいいような)
また個人的には、変数名をcurrent_column(rootのやつ), up_column((row - 1) * columnCount + columnのやつ)などとつけた方がいいように思いました。
rootは多分UnionFindを意識して書いていると思うのですが、読んでいる側からすると既にそれはわかってて、知りたいのはどの位置のgridを繋げているか、だと思いました。
There was a problem hiding this comment.
ありがとうございます。仰るとおり繋ぐのは2方向だけで良いですね。
root の変数名についても確かにその通りだなと思ったので、 coordinate に修正しました。
f086619
https://leetcode.com/problems/number-of-islands/description/