문제 설명
정수 배열 nums
가 주어졌을 때, 가능한 모든 순열(permutation)을 반환하라. 순열은 원소들의 모든 가능한 순서를 의미한다.
입력 및 출력 예시
Example 1:
- Input:
nums = [1, 2, 3]
- Output:
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
Example 2:
- Input:
nums = [0, 1]
- Output:
[[0, 1], [1, 0]]
Example 3:
- Input:
nums = [1]
- Output:
[[1]]
제약사항
- 1 <= nums.length <= 6
- -10 <= nums[i] <= 10
- nums의 모든 원소는 고유하다.
풀이 1: 백트래킹 (Backtracking) 사용
시간 복잡도
- 시간복잡도: O(n * n!) (n은 배열의 길이)
- T(n)=n!×(c1×n + c2 + c3×n)
- : 가능한 모든 순열의 수
- : 요소 선택 및 추가 연산의 상수 시간
- : 경로 완성 확인 및 결과 추가 연산의 상수 시간
- : 요소 제거 및 사용 여부 업데이트 연산의 상수 시간
핵심 아이디어
- 백트래킹을 통해 가능한 모든 경로를 탐색하고, 모든 경로가 완성되면 결과에 추가하는 방식으로 구현한다.
코드 구현
public class L_46_순열 {
public static void main(String[] args) {
L_46_순열 l46 = new L_46_순열();
int[] nums = {1, 2, 3};
System.out.println(l46.permute(nums));
}
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
// 백트래킹을 통해 모든 순열을 탐색
backtrack(result, new ArrayList<>(), nums, new boolean[nums.length]);
return result;
}
// 백트래킹을 통해 가능한 모든 경로 탐색
private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, boolean[] used) { // 1ms
// 경로의 길이가 nums의 길이와 같아지면 결과에 추가
if (path.size() == nums.length) {
result.add(new ArrayList<>(path)); // 현재 경로를 결과에 추가
return;
}
// 모든 숫자에 대해 사용되지 않은 숫자를 경로에 추가
for (int i = 0; i < nums.length; i++) {
if (used[i]) { // 백트래킹: 이미 사용된 숫자는 스킵
continue;
}
// 숫자를 경로에 추가하고 사용 상태 업데이트
path.add(nums[i]);
used[i] = true;
// 재귀 호출을 통해 다음 단계 탐색
backtrack(result, path, nums, used);
// 이전 상태로 되돌리기
path.remove(Integer.valueOf(nums[i])); // 경로에서 현재 숫자 제거
used[i] = false; // 숫자를 사용되지 않은 상태로 변경
}
}
}
설명
- 초기화:
result
리스트는 모든 가능한 순열을 저장한다.path
리스트는 현재까지 탐색한 경로를 저장한다.used
배열은 각 숫자가 현재 경로에서 사용되었는지를 추적한다.
- 백트래킹 과정:
- 경로의 길이가
nums
의 길이와 같아지면 현재 경로를result
에 추가한다. for
루프를 통해 사용되지 않은 숫자를 경로에 추가하고, 재귀 호출을 통해 깊이 탐색을 진행한다.- 재귀 호출이 끝나면, 이전 상태로 돌아가며 숫자를 사용하지 않은 상태로 복원한다.
- 각 단계에서 가능한 모든 요소를 고려하지만, 이미 사용된 요소는 스킵하여 불필요한 탐색을 줄인다.
- 경로의 길이가
풀이 2: 깊이 우선 탐색 (DFS) 사용
시간 복잡도
- 시간복잡도: O(n * n!) (n은 배열의 길이)
- T(n)= n! × (c1×n + c2×(n−1) + c3 + c4×n)
- : 가능한 모든 순열의 수
- : 맨 끝에 요소를 추가하는 평균적인 시간
- : 특정 위치에 요소를 추가하는 경우의 시간
- : 경로가 완성되어 결과 리스트에 추가하는 시간
- : 특정 위치에서 요소를 제거하는 경우의 시간
핵심 아이디어
- DFS를 통해 모든 경로를 탐색하며, 사용되지 않은 요소를 경로에 추가하는 방식으로 구현한다.
코드 구현
private void dfs(List<List<Integer>> results, List<Integer> path, List<Integer> elements) { // 2ms
// 탐색해야할 elements가 비어있다면 재귀 종료 및 results에 path 저장
if (elements.isEmpty()) {
results.add(new ArrayList<>(path)); // 현재 경로를 결과에 추가, path 깊은복사
}
// 사용되지 않은 모든 요소에 대해 탐색
for (Integer e : elements) {
List<Integer> nextElements = new ArrayList<>(elements); // 사용하지 않은 요소 복사(깊은 복사)
nextElements.remove(e); // 현재 요소를 제외하여 다음 탐색 준비
path.add(e); // 현재 경로에 요소 추가
dfs(results, path, nextElements); // 다음 단계 탐색
path.remove(e); // 탐색 완료 후 현재 요소 제거
}
}
설명
- 초기화:
results
리스트는 모든 가능한 순열을 저장한다.path
리스트는 현재까지 탐색한 경로를 저장한다.elements
리스트는 현재 탐색에서 사용하지 않은 요소들을 저장한다.
- DFS 과정:
elements
가 비어 있으면 현재 경로를results
에 추가한다.- 각 요소를 경로에 추가하고, 해당 요소를 제외한 리스트로 재귀 호출을 통해 다음 단계 탐색을 진행한다.
- 재귀 호출이 끝나면, 이전 상태로 돌아가며 경로에서 요소를 제거한다.
- 가능한 요소 집합을 재귀적으로 줄여 나가면서 경로를 구성한다.
결론
- 학습 내용: 이번 문제를 통해 백트래킹과 DFS를 활용한 순열 생성 방법을 학습했다. 두 방법 모두 모든 가능한 경로를 탐색하며, 주어진 문제의 제약 조건을 만족한다.
- 향후 계획: DFS를 사용하는 문제를 더 풀면서 더 익숙해져야 할 것 같다. 아직 즉시 문제 해결법이 떠오르지는 않고, 재귀의 사용이 어렵게 느껴진다.
<현재 공부중인 알고리즘 도서>
'Java > Algorithm' 카테고리의 다른 글
LeetCode #77. Combinations(조합) 2가지 DFS로 풀어보자! (0) | 2024.08.06 |
---|---|
LeetCode # 17. Letter Combinations of a Phone Number / DFS(재귀), BFS(Queue)를 이용해 풀어보자 (0) | 2024.08.02 |
LeetCode # 347. HashMap과 PriorityQueue, Stream을 이용해 풀어보자 (0) | 2024.07.24 |
댓글