행렬 순회
예제 - LeetCode 36. Valid Sudoku
https://leetcode.com/problems/valid-sudoku/?envType=study-plan-v2&envId=top-interview-150
1. 문제 이해
- 채워진 칸(.이 아닌)에 대해 스도쿠 조건을 만족하는지 검사
2. 요구사항 분석
- 입력
- board.length == 9
- board[i].length == 9
- board[i][j]는 1-9 사이의 숫자, 빈칸은 '.'
- 출력
- 모든 채워진 칸이 조건을 만족하면 true, 아니면 false
3. 해결 아이디어
- 각 행, 열, 박스의 중복을 검사할 set 할당
- 해당 칸의 숫자를 set에 넣어가며 중복 검사, 중복이 있다면 불가능
4. 시간 복잡도 및 공간 복잡도 확인
- 시간 복잡도
- 9 * 9의 고정 크기를 순회하므로 O(1)
- 공간 복잡도
- 최대 9개 크기의 27개의 set을 사용하므로 O(1)
5. 구현
class Solution {
public boolean isValidSudoku(char[][] board) {
// 1. 각 행, 열, 박스에 대한 숫자 기록용 Set 할당
Set<Character>[] rows = new HashSet[9];
Set<Character>[] cols = new HashSet[9];
Set<Character>[] boxes = new HashSet[9];
// 2. 각 Set 초기화
for (int i = 0; i < 9; i++) {
rows[i] = new HashSet<>();
cols[i] = new HashSet<>();
boxes[i] = new HashSet<>();
}
// 3. 전체 보드를 순회하며 검사
for (int r = 0; r < 9; r++) {
for (int c = 0; c < 9; c++) {
char val = board[r][c]; // 현재 숫자
if (val == '.') continue; // 빈칸은 검사 안함
// 3-1) 3x3 박스 인덱스 계산
int boxIndex = (r / 3) * 3 + (c / 3);
// 3-2) 중복 검사 -> 이미 존재하면 조건에 어긋남
if (rows[r].contains(val) || cols[c].contains(val) || boxes[boxIndex].contains(val)) {
return false;
}
// 3-3) 현재 숫자를 set에 기록
rows[r].add(val);
cols[c].add(val);
boxes[boxIndex].add(val);
}
}
return true;
}
}
예제 - LeetCode 54. Spiral Matrix
1. 문제 이해
- 2차원 행렬을 시계 방향 나선형으로 배열을 순회
2. 요구사항 분석
- 입력
- 1 <= matrix.length, matrix[i].lenght <= 10
- -100 <= matrix[i][j] <= 100
- 출력
- 조건 순서대로 정수를 나열한 리스트
3. 해결 아이디어
- 경계를 할당하고 왼 -> 오, 위 -> 아래, 오 -> 왼, 아래 -> 위 방향으로 순회
- 경계를 하나씩 줄여가며 반복
4. 시간 복잡도 및 공간 복잡도 확인
- 시간 복잡도
- 행렬의 크기만큼 순회하므로 O(m * n)
- 공간 복잡도
- 결과를 저장할 리스트를 사용하므로 O(m * n)
5. 구현
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
// 1. 결과를 저장할 리스트 및 경계 할당
List<Integer> result = new ArrayList<>();
int top = 0;
int bottom = matrix.length - 1;
int left = 0;
int right = matrix[0].length - 1;
// 2. 나선 순서대로 순회하며 경계를 하나씩 줄여가기
while (top <= bottom && left <= right) {
// 2-1) 왼 → 오
for (int i = left; i <= right; i++) {
result.add(matrix[top][i]);
}
top++;
// 2-2) 위 → 아래
for (int i = top; i <= bottom; i++) {
result.add(matrix[i][right]);
}
right--;
// 2-3) 오 → 왼
if (top <= bottom) {
for (int i = right; i >= left; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
}
// 2-4) 아래 → 위
if (left <= right) {
for (int i = bottom; i >= top; i--) {
result.add(matrix[i][left]);
}
left++;
}
}
return result;
}
}
행렬 변환
예제 - LeetCode 48. Rotate Image
1. 문제 이해
- 2차원 배열을 시계 방향으로 회전, 단 새로운 배열 없이 직접 수정
2. 요구사항 분석
- 입력
- n = matrix.length = matrix[i].length
- 1 <= n <= 20
- -1,000 <= matrix[i][j] <= 1,000
- 출력
- 없음, 주어진 행렬을 변환
3. 해결 아이디어
- 정사각형 행렬에서 전치(행렬의 행과 열을 바꿈) + 행 뒤집기(좌우반전) -> 시계방향 90도 회전임을 이용
다음 뒤집기 전략도 기억해두면 편리
- 전치 + 열 뒤집기(상하반전) -> 반시계방향 90도 회전
- 행 뒤집기 + 열 뒤집기 -> 180도 회전
4. 시간 복잡도 및 공간 복잡도 확인
- 시간 복잡도
- 전치, 뒤집기 각각 행렬을 순회하므로 O(n * n)
- 공간 복잡도
- 추가 행렬을 사용하지 않으므로 O(1)
5. 구현
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 1. 전치 → 대각선을 기준으로 뒤집기
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 2. 각 행을 좌우 반전
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[i][n - 1 - j];
matrix[i][n - 1 - j] = temp;
}
}
}
}
예제 - LeetCode 73. Set Matrix Zeroes
1. 문제 이해
- 2차원 배열에서 어떤 요소가 0이라면 해당 행과 열의 모든 요소를 0으로 변경, 단 추가 행렬 없이 처리
2. 요구사항 분석
- 입력
- 1 <= matrix.length = matrix[i].length <= 200
- matrix[i][j]는 int 범위
- 출력
- 없음, 주어진 행렬을 변환
3. 해결 아이디어
- 첫 행/열을 해당 행/열을 0으로 바꿔야하는지에 대한 마커로 표시
- 첫 행/열에 0이 포함되었는지 기록 -> 마지막에 첫 행/열 부분 마무리 하기 위함
- 그 이외 셀을 확인하며 0으로 바꿔야하는 행/열에 0으로 마커 표시
- 마커를 기준으로 해당 행/열을 모두 0으로 변경
- 첫 행/열을 기록에 맞춰 0으로 마무리
4. 시간 복잡도 및 공간 복잡도 확인
- 시간 복잡도
- 전체 행렬을 한 번 순회하므로 O(m * n)
- 공간 복잡도
- 추가 행렬을 사용하지 않으므로 O(1)
5. 구현
class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean firstRowZero = false, firstColZero = false;
// 1. 첫 행/열이 0이 포함되어 있는지 별도로 기록
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) firstColZero = true;
}
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) firstRowZero = true;
}
// 2. 나머지 셀 검사 → 첫 행/열을 마커로 사용
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 3. 마커를 참고해서 0으로 세팅
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 4. 첫 행/열 마무리 처리
if (firstColZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
if (firstRowZero) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
}
}
예제 - LeetCode 289. Game of Life
1. 문제 이해
- 2차원 배열에서 각 셀은 1(생존), 0(죽음)으로 표현되고 다음 규칙으로 동시에 갱신됨
- 살아있는 셀 주변 1개 이하 살아있는 이웃이 있다면 죽음
- 살아있는 셀 주변 2개 또는 3개 살아있는 이웃이 있다면 생존
- 살아있는 셀 주변 3개 초과의 살아있는 이웃이 있다면 죽음
- 죽어있는 셀 주변 3개의 살아있는 이웃이 있다면 생존
- 각 셀의 주변은 8방향을 의미
2. 요구사항 분석
- 입력
- 1 <= board.length = board[i].length <= 25
- board[i][j]는 0 또는 1
- 출력
- 없음, 주어진 행렬을 변환
3. 해결 아이디어
- 이전 상태와 이후 상태를 구분하기 위해 -2(죽을 셀), -1(살아날 셀) 마킹을 추가로 사용
4. 시간 복잡도 및 공간 복잡도 확인
- 시간 복잡도
- 전체 행렬을 한 번 순회하고 각 8방향 탐색하므로(m*n*8) O(m * n)
- 공간 복잡도
- 추가 행렬을 사용하지 않으므로 O(1)
5. 구현
class Solution {
public void gameOfLife(int[][] board) {
int m = board.length, n = board[0].length;
int[][] dirs = {
{-1, -1}, {-1, 0}, {-1, 1},
{ 0, -1}, { 0, 1},
{ 1, -1}, { 1, 0}, { 1, 1}
};
// 1. 각각 셀의 상태를 마킹
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int liveNeighbors = 0;
for (int[] dir : dirs) {
int ni = i + dir[0], nj = j + dir[1];
if (0 <= ni && ni < m && 0 <= nj && nj < n) {
// 1 or -2는 이전 상태가 살아 있었던 것
if (board[ni][nj] == 1 || board[ni][nj] == -2) {
liveNeighbors++;
}
}
}
// 살아 있는 셀
if (board[i][j] == 1) {
if (liveNeighbors < 2 || liveNeighbors > 3) {
board[i][j] = -2; // 살았는데 죽을 셀 (1 → 0)
}
}
// 죽어 있는 셀
else {
if (liveNeighbors == 3) {
board[i][j] = -1; // 죽었는데 살아날 셀 (0 → 1)
}
}
}
}
// 2. 마킹을 실제 값으로 변환(-2 -> 0, -1 -> 1)
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == -2) board[i][j] = 0;
else if (board[i][j] == -1) board[i][j] = 1;
}
}
}
}
만일 board가 무한하다면 2차원 배열을 사용할 수 없으므로, Set으로 살아있는 셀을 관리하고, Map으로 셀의 주변 8칸의 이웃 수를 세는 방법을 활용해 Set을 갱신하는 방법으로 구현 가능
의사코드
Set<Pair> alive = new HashSet<>(); // 현재 살아 있는 셀
Map<Pair, Integer> neighborCount = new HashMap<>();
// 1. 살아 있는 셀의 주변 8칸의 이웃 수를 세기
for (Pair cell : alive) {
for (int[] dir : directions) {
Pair neighbor = new Pair(cell.x + dir[0], cell.y + dir[1]);
neighborCount.put(neighbor, neighborCount.getOrDefault(neighbor, 0) + 1);
}
}
// 2. 다음 세대의 살아 있는 셀 결정
Set<Pair> nextAlive = new HashSet<>();
for (Map.Entry<Pair, Integer> entry : neighborCount.entrySet()) {
Pair cell = entry.getKey();
int count = entry.getValue();
if (alive.contains(cell)) {
if (count == 2 || count == 3) nextAlive.add(cell); // 생존
} else {
if (count == 3) nextAlive.add(cell); // 새로 살아남
}
}
// 3. 상태 갱신
alive = nextAlive;
'코딩테스트 > 알고리즘' 카테고리의 다른 글
예제로 배우는 알고리즘 - 스택 (0) | 2025.05.18 |
---|---|
예제로 배우는 알고리즘 - 구간 (0) | 2025.05.06 |
예제로 배우는 알고리즘 - 슬라이딩 윈도우 (0) | 2025.05.02 |
예제로 배우는 알고리즘 - 문자열 (0) | 2025.04.28 |
예제로 배우는 알고리즘 - 해싱 (0) | 2025.04.19 |