아래의 내용은 java_grammer 레파지토리 C02MethodClass 디렉터리에 저장되어있는 내용을 정리함
대표적으로 백트래킹(Backtracking), DFS, 분할 정복(Divide & Conquer) 알고리즘이 모두 재귀 구조 위에서 동작한다
- 백트래킹 문제 모음: 백준 알고리즘 분류 링크
- DFS 문제 모음: DFS 알고리즘 링크
- 분할정복 문제 모음: Divide & Conquer 링크
for문의 반복 횟수가 정적이라면, 재귀로 깊이를 제어해야 함.
아래 예제는 일반적인 중첩 for문을 재귀로 바꾼 구조임.
public static void recurForLoop(int nowDepth, int targetDepth) {
if (nowDepth == targetDepth) {
return; // 끝자리에서 종결
}
for (int i = 0; i < 3; i++) {
recurForLoop(nowDepth + 1, targetDepth);
}
}
실행 구조 (targetDepth=3):
for(i=0;i<3;i++){
for(j=0;j<3;j++){
for(k=0;k<3;k++){
...
}
}
}
즉, recurForLoop(0,3)은 3중 for문처럼 동작함.
스택이 깊이가 되어 for 반복의 중첩 구조를 대체하는 셈임.
예시: 1,2,3,4에서 2개 뽑기.
결과: [1,2], [1,3], [1,4], [2,3], [2,4], [3,4]
List<List<Integer>> doubleList = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
for (int i = 0; i < myList.size(); i++) {
temp.add(myList.get(i));
for (int j = i + 1; j < myList.size(); j++) {
temp.add(myList.get(j));
doubleList.add(temp); // 같은 리스트 주소 공유 -> 잘못된 결과
}
}
이 코드의 문제는 temp의 주소가 계속 같음.
즉, 모든 add() 결과가 누적된 동일 리스트가 반복해서 들어감.
List<List<Integer>> doubleList1 = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
for (int i = 0; i < myList.size(); i++) {
temp.add(myList.get(i));
for (int j = i + 1; j < myList.size(); j++) {
temp.add(myList.get(j));
doubleList1.add(new ArrayList<>(temp)); // 복제
temp.remove(temp.size() - 1); // j루프 끝나면 제거
}
temp.remove(temp.size() - 1); // i루프 끝나면 제거
}
System.out.println(doubleList1);
결과: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
public static void recurForComb(List<Integer> temp, int start, List<Integer> myList, int n, List<List<Integer>> doubleList) {
if (temp.size() == n) {
doubleList.add(new ArrayList<>(temp));
return;
}
for (int i = start; i < myList.size(); i++) {
temp.add(myList.get(i));
recurForComb(temp, i + 1, myList, n, doubleList);
temp.remove(temp.size() - 1);
}
}
호출 예시:
recurForComb(new ArrayList<>(), 0, myList, 2, combDoubleList);
핵심
start 인덱스로 다음 루프가 이전 인덱스를 다시 방문하지 않도록 관리 temp는 현재 조합 상태를 저장 출력 결과:
[[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
예시: 1,2,3,4에서 2자리 순열
결과: [1,2], [1,3], [1,4], [2,1], [2,3], [2,4], ...]
List<List<Integer>> permuDoubleList1 = new ArrayList<>();
List<Integer> permuTemp = new ArrayList<>();
boolean[] visited = new boolean[myList.size()];
for (int i = 0; i < myList.size(); i++) {
permuTemp.add(myList.get(i));
visited[i] = true;
for (int j = 0; j < myList.size(); j++) {
if (visited[j]) continue;
permuTemp.add(myList.get(j));
visited[j] = true;
permuDoubleList1.add(new ArrayList<>(permuTemp));
permuTemp.remove(permuTemp.size() - 1);
visited[j] = false;
}
permuTemp.remove(permuTemp.size() - 1);
visited[i] = false;
}
System.out.println(permuDoubleList1);
이중 for문으로 순열 2자리까진 가능하지만, 자리 수 늘어나면 복잡도 급상승함.
이걸 재귀로 바꾸면 훨씬 깔끔함.
public static void recurForPermu(boolean[] visited, List<Integer> temp, List<Integer> myList, int n, List<List<Integer>> doubleList) {
if (temp.size() == n) {
doubleList.add(new ArrayList<>(temp));
return;
}
for (int i = 0; i < myList.size(); i++) {
if (visited[i]) continue;
temp.add(myList.get(i));
visited[i] = true;
recurForPermu(visited, temp, myList, n, doubleList);
temp.remove(temp.size() - 1);
visited[i] = false;
}
}
호출 예시:
recurForPermu(new boolean[myList.size()], new ArrayList<>(), myList, 2, permuDoubleList);
작동 구조
1. 방문한 원소 표시 (visited)
2. 현재 값 추가하고 재귀 호출
3. 복귀 시 backtrack (temp와 visited 되돌림)
결과:
[[1,2], [1,3], [1,4], [2,1], [2,3], [2,4], [3,1], [3,2], [3,4], [4,1], [4,2], [4,3]]
start=i+1) visited[i])