조합(Combination)
순서를 고려하지 않고 n개 중에서 r개를 뽑는 경우의 수
예시: [1, 2, 3]에서 2개 뽑기
결과: [1,2], [1,3], [2,3] (총 3개)
❌ [2,1]은 [1,2]와 같은 조합이므로 제외!
순열과의 차이
- 순열: [1,2]와 [2,1]은 다름
- 조합: [1,2]와 [2,1]은 같음
public class prac {
static int[] array = {1, 2, 3};
static int n = array.length;
static int r = 2;
static int[] result = new int[r];
public static void main(String[] args) {
combination(0,0);
}
public static void combination(int depth, int startIndex) { // 조합은 startIndex가 매개변수로 추가된다.
if (depth == r) {
System.out.println(Arrays.toString(result));
return;
}
for (int i = startIndex; i < n; i++) {
result[depth] = array[i];
combination(depth+1,i+1);
}
}
}
/*
depth는 재귀의 깊이를 의미한다.
하나를 선택할때마다 depth가 증가하는것은 그 다음 재귀로 들어가게 됨을 의미한다.
그래서 depth와 r이 같으면 다 뽑은것이다.
depth 1 -> [ 1, 2, 3, 4, 5 ] 를 반복문을 돌면서 하나를 선택하거나 선택하지않거나
depth 2 -> [ 1, 2, 3, 4, 5 ] 를 반복문을 돌면서 하나를 선택하거나 선택하지않거나
depth 3 -> [ 1, 2, 3, 4, 5 ] 를 반복문을 돌면서 하나를 선택하거나 선택하지않거나
물론 startIndex를 이용해서 반복문을 돌기에 이전 depth선택한것의 그 다음부터 살펴본다.
*/
조합 코드 특징
조합은 순서를 고려하지 않고 선택하는 것이니까 이전 재귀스택에서 뽑힌 요소의 오른쪽으로만 살피면서 모든 경우를 살핀다.
예를들어서 {1,2,3}이 있고 2개를 뽑는 모든 경우의수를 살펴본다고 할때
진행순서는
첫번째 자리에 1을 뽑고 1이후의 2,3만 살펴보면 되고
반복문이 다음 인덱스로 진행되어서 첫번째 자리에 2를 뽑았다면 2이후의 3만 살펴보면 된다.
(= 조합은 순서를 고려하지 않으니까)
이전 재귀스택에서
1을 선택했다면?
→ 1은 다시 선택 안하고 다음 선택은 2, 3 중에서만 해야하니까 1의 대한 인덱스에 1을 더해서 시작인덱스로 전달한다.
2를 선택했다면?
→ 1,2는 다시 선택 안하고 다음 선택은 3만 해야하니까 2의 대한 인덱스에 1을 더해서 시작인덱스로 전달한다.
3을 선택했다면?
→ 1,2,3는 다시 선택 안해야하니까 3의 대한 인덱스에 1을 더해서 시작인덱스로 전달한다.
조합에서는 순서를 고려하지않고, 중복을 방지할 수 있는 수단으로 매개변수 StartIndex가 추가되었다.
반복문의 시작인덱스를 관리해서 순서를 고려하지않는 모든 경우의 수를 찾을 수 있는거고,
StartIndex의 값을 조절해서 중복이 가능하거나 못하게 만들 수 있다.
중복 조합
public class prac {
static int[] array = {1, 2, 3};
static int n = array.length;
static int r = 2;
static int[] result = new int[r];
public static void main(String[] args) {
combinationWithDuplicates(0,0);
}
public static void combinationWithDuplicates(int depth, int startIndex) {
if (depth == r) {
System.out.println(Arrays.toString(result));
return;
}
for (int i = startIndex; i < n; i++) {
result[depth] = array[i];
combinationWithDuplicates(depth+1,i); // 중복을 허용하기위해 방금 뽑은 요소의 인덱스를 시작 인덱스로 전달해준다.
}
}
}
일반 조합과 중복조합의 차이는 단 한글자 차이가 난다.
전달하는 StartIndex에 값을 방금 뽑은 요소의 인덱스로 전달해주면 된다. 그러면 중복요소의 경우까지 처리된다.
combinationWithRepetition(depth + 1, i); // 다음은 i부터 (자기 자신 포함!)
차이점:
- 일반조합: 이전 요소의 오른쪽으로만 진행 + 자기 자신 제외
- 중복조합: 이전 요소의 오른쪽으로만 진행 + 자기 자신 포함
소수구하기 ( Prime )
public boolean isPrime(int n) {
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) return false;
}
return true;
}
소수는 1과 자기자신을 약수로 가지는 수이다.
그래서 1은 소수가 아니다.
2로 나눠지는 짝수들은 소수가 아니다.
1,2,짝수들은 미리 검사하고
홀수들에 대해서만 소수인지 검사한다.
i * i <= n
로 제곱근까지만 검사해서 검사 범위를 줄인다. ( 약수는 항상 Pari(쌍)으로 존재하기 때문이다 )
Collection 주의점
컬렉션 순회 중 요소 삭제 시 주의사항
List나 Set 같은 Collection을 순회하면서 특정 요소를 삭제해야 할 때가 있다.
이때 평소처럼 for문을 사용하면 예상치 못한 에러나 버그를 마주하게 된다.
향상된 for문(for-each) 사용 시: 에러 발생
향상된 for문 내에서 list.remove() 등을 호출하면 ConcurrentModificationException 예외가 발생하며
프로그램이 강제 종료된다.
- 원인: 향상된 for문은 내부적으로 Iterator를 사용한다.
순회 도중 컬렉션이 직접 수정되면, Iterator가 관리하는 상태 값과 실제 컬렉션의 상태가 달라져
"Fail-Fast" 전략에 의해 예외를 던진다.
일반 index for문 사용 시: 논리적 오류(Bug)
일반적인 for (int i = 0; i < list.size(); i++) 방식은 예외는 발생하지 않지만, 데이터가 누락되는 치명적인 버그가 생길 수 있다
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 2, 3));
for (int i = 0; i < list.size(); i++) {
if (list.get(i) == 2) {
list.remove(i);
}
}
// 결과: [1, 2, 3] (연속된 '2' 중 하나가 삭제되지 않고 남음)
- 원인: 리스트의 요소를 삭제하면 뒤에 있던 데이터들이 앞으로 한 칸씩 당겨진다.
하지만 인덱스 i는 다음 루프에서 그대로 증가하기 때문에, 당겨져 온 요소를 검사하지 못하고 건너뛰게 된다.
해결방안
① Java 8 removeIf() (가장 권장)
별도의 루프 없이 조건식(람다)만 전달하면 된다. 가독성이 가장 좋고 안전하다.
// List
list.removeIf(num -> num == 2);
// Set
set.removeIf(item -> item.equals("B"));
// Map ( EntrySet을 이용한 조건 삭제 (가장 많이 사용) )
Map<Integer, String> map = new HashMap<>();
map.put(1, "Apple");
map.put(2, "Banana");
map.entrySet().removeIf(entry -> entry.getValue().equals("Banana"));
// KeySet을 이용한 삭제 ( 특정 키 값을 기준으로 삭제할 때 유용하다. )
map.keySet().removeIf(key -> key == 1);
// Values를 이용한 삭제 ( value는 key와 다르게 중복값을 가지고있다. 그래서 조건에 맞는 모든 요소들이 지워진다.)
map.values().removeIf(value -> value > 25);
② Iterator 직접 사용
Iterator 객체의 remove() 메서드를 사용하면 순회 상태를 유지하면서 안전하게 삭제할 수 있다.
// List
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() == 2) {
it.remove();
}
}
// Set
Iterator<String> it = set.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove();
}
}'Backend > Problem Solving' 카테고리의 다른 글
| 코데 대비5 - Deque(덱),동적프로그래밍 (0) | 2025.12.31 |
|---|---|
| 코데 대비4 - 정규표현식(문자열검증), 우선순위큐,n진법 (0) | 2025.12.29 |
| 코테 대비 2 - 약수구하기, Collection 최대값 구하기, 순열,백트래킹 (0) | 2025.12.10 |
| 코테 대비 1 - List,Map,Comparator,Stream API (0) | 2025.12.10 |
| 프로그래머스] n으로 표현 (0) | 2023.05.17 |