Today I Learned
(24.08.28) 코딩테스트 알고리즘 정리(구현:누적합,유클리드,에라토스테네스의체, 구간탐색:이분탐색,슬라이딩윈도우, Greedy:동전교환 알고리즘)
이전까지는 거의 코드 카타를 맨땅에 헤딩하듯이 풀어서
다시 살펴보니, 간단하게 알고리즘으로 풀수 있는 로직을 굳이 O(N^2) 시간 복잡도로 하나씩 대조하거나 하는 식의 로직이 많았기 때문에,
필수 알고리즘 기초부터 다시 정리를 하기 시작했다.
검색을 통해 정리를 하고, 스스로 코드를 적으면서 정리를 한 뒤, 응용된 문제들을 푸는 방식으로 진행을 해야
여러 알고리즘이 섞인 코딩테스트 문제도 해결할 수 있을 것이라고 판단해서
블로그 TIL과 Notion에 정리를 하도록 한다.
누적합 Prefix Sum
더보기
- 특정 배열 등의 자료 구조 구간의 누적된 합, 즉 계속 지속적으로 더하는 알고리즘
- 배열의 여러 구간에 대해 부분합을 빠르게 계산
- “ 이전까지의 누적합을 다음 누적합에 사용한다” 라는 알고리즘이기 때문에 항상 누적합 배열에서는
누적합 Algorithm Flow
- 전체 배열 A에 대한 index 0~i 까지 누적합 배열 P생성
- 여기서, 배열 P는 배열 A의 index 0 ~ i 까지의 합들의 집합
- ex) P[i] = A[0] + A[1] + ... + A[i-1]
- 누적합 배열 P의 index 0은 아무 원소도 더하지 않는 합 0으로 설정
- 구간 합을 구하려는 시도가 있을 때, 또는 로직을 구축을 할 때, 그 구간이 배열A의 index 0 까지일 경우도 있기 때문에 반드시 필요
- P[0] = 0 (빈 부분합)
- P[1] = 3 (A[0])
- P[2] = 3 + 2 = 5 (A[0] + A[1])
- P[3] = 3 + 2 + 5 = 10 (A[0] + A[1] + A[2])
- P[4] = 3 + 2 + 5 + 1 = 11 (A[0] + A[1] + A[2] + A[3])
- P[5] = 3 + 2 + 5 + 1 + 4 = 15 (A[0] + A[1] + A[2] + A[3] + A[4])
- 구하고자 하는 구간 합 계산
- 시작 인덱스 **A[l]**도 포함을 시켜야 하기 때문에 P[l + 1]이 아님 위의 예제 확인
- **배열 A의 index l ~ r까지의 구간 합 = P[r + 1] - P[l]**
누적합 Alorithm Step
누적합 배열 P 생성 만들기
public class PrefixSum {
public static void main(String[] args) {
int[] A = {3, 2, 5, 1, 4};
int n = A.length;
**// 누적합 배열 P 생성
// P[0] = 0 이 있어야 하기 때문에 크기는 n+1
int[] P = new int[n + 1];
for (int i = 1; i <= n; i++) { // P[0] 설정 안해도 자동으로 0 초기화 이므로 0부터 시작 X
P[i] = P[i - 1] + A[i - 1]; // 하나씩 계속 추가, i-1 이라는 거 항상 생각해야
}**
// 누적합 배열 출력
System.out.println("누적합 배열: " + Arrays.toString(P));
}
}
구간 합 계산
public class PrefixSum {
public static void main(String[] args) {
// 위 누적합 배열 P 생성 로직
// 구간 합 계산 (예: 인덱스 2부터 4까지)
int l = 2; // 시작 인덱스
int r = 4; // 끝 인덱스
**int sum = P[r + 1] - P[l];**
System.out.println("구간 합: " + sum);
}
}
유클리드 알고리즘 = 유클리드 호제법 Euclidean Algorithm
더보기
최대공약수 GCD, Greatest Common Divisor
GCD 유클리드 알고리즘 Flow
- 두 수 중 큰 수를 작은 수로 나누고, 나머지를 다시 작은 수로 나누면서나머지가 0이 되면, 나누는 수가 GCD
- a와 b가 주어졌다고 할 때, a를 b로 나눈 나머지를 r 로 지정
- a를 b로 바꾸기 / b를 r로 바꾸기
- 바꾼 후, b가 0이 될 때까지 반복 (while문 사용을 위해)
- a가 GCD
GCD 유클리드 알고리즘
public class EuclideanAlgorithm {
**public static int gcd(int a, int b) {
int minValue = Math.min(a, b);
int maxValue = Math.max(a, b);
while (minValue != 0) {
int remainder = maxValue % minValue;
maxValue = minValue;
minValue = remainder;
}
return maxValue;
}**
public static void main(String[] args) {
int a = 48;
int b = 18;
int gcdValue = gcd(a, b);
System.out.println("GCD(" + a + ", " + b + ") = " + gcdValue);
}
}
최소공배수 LCM, Least Common Multiple
- 두 자연수의 곱에서 GCD를 나눈 값, 따라서 최소공배수를 쓰려면 유클리드 알고리즘을 통해서 최대 공약수를 구하면 됨
- 두 int 정수 값 중 음수가 하나있더라도 LCM은 양수여야 하기 때문에 절댓값을 사용해야함
public class GCDAndLCM {
**public static int gcd(int a, int b) {
int minValue = Math.min(a, b);
int maxValue = Math.max(a, b);
while (minValue != 0) {
int remainder = maxValue % minValue;
maxValue = minValue;
minValue = remainder;
}
return maxValue;
}**
// GCD를 사용하여 LCM을 계산
**public static int lcm(int a, int b) {
return Math.abs(a * b) / gcd(a, b);
}**
public static void main(String[] args) {
int a = 48;
int b = 18;
int gcdValue = gcd(a, b);
int lcmValue = lcm(a, b);
System.out.println("GCD(" + a + ", " + b + ") = " + gcdValue);
System.out.println("LCM(" + a + ", " + b + ") = " + lcmValue);
}
}
큰 수, 작은 수 판별하기
Math.max(int a, int b) 와 Math.min(int a, int b) 를 활용해서 큰 수 작은 수를 판별 할 수 있도록 해야함
에라토스테네스의 체 Sieve of Eratosthenes
더보기
- 주어진 수 n이하의 모든 소수를 다 찾아내는 알고리즘
- “소수의 배수는 전부 소수가 아니다” 를 기준으로 소수를 찾음
- n 의 제곱근 아래의 수만 소수인지 판별해도 모든 n이하의 소수를 거진 찾을 수 있다는 것을 가정
- 동시에, 방복문은 소수로 판명된 i 에 대하여 i*i의 배수만 처리해도 된다고 판단
에라토스테네스의 체 Algorithm Flow
- 배열 초기화:
- n까지의 모든 수에 대해 소수 여부를 나타내는 배열 isPrime을 초기화
- 초기에는 2부터 n까지 모든 수를 소수로 간주
- 소수 찾기
- 배열의 처음부터 시작하여, 현재 수가 소수(True)이면 그 수의 배수들을 모두 소수가 아니라고 표시 (굳이 다 할 필요가 없음)
- 반복
- 이 과정을 주어진 범위의 수들에 대해 반복하면, 최종적으로 배열에 남아 있는 True 값이 소수
에라토스테네스의 체 Algorithm
- 초기화
- isPrime 배열을 크기 n + 1로 생성하고, 2부터 n까지의 모든 값을 True로 설정
- n+1인 이유는 Java 인덱스와 = 실제 수를 일치 시키기 위해
- 인덱스 0 과 1은 소수가 아니므로 False로 설정
- 수 0, 1은 소수가 아니기 때문에
- isPrime 배열을 크기 n + 1로 생성하고, 2부터 n까지의 모든 값을 True로 설정
—반복 시작
- 첫 번째 소수 2의 배수 제거
- 2는 소수이므로 2의 배수 = 짝수를 모두 False로 설정
- 다음 소수 3의 배수 제거
- 3은 소수이므로 3의 배수(6, 9, 12, ...)를 모두 False로 설정
- 이미 2의 배수로 제거된 숫자는 다시 고려하지 X
- 그 다음 소수
- 다음 소수는 5 이므로, 5의 배수(10, 15, 20, ...)를 모두 False로 설정
- 4는 이미 제거된 숫자(2번에서 False) 이므로 고려하지 X
—반복 종료
- 최종 배열
- isPrime 배열에서 True로 남아 있는 인덱스는 모두 소수가 되는 법
import java.util.Arrays;
public class SieveOfEratosthenes {
**public static boolean[] sieve(int n) {
// n까지의 모든 수를 소수로 간주하여 배열 초기화
boolean[] isPrime = new boolean[n + 1];
Arrays.fill(isPrime, true); // fill 메서드를 사용해서 모두 true
isPrime[0] = false; // 0은 소수가 아님
isPrime[1] = false; // 1도 소수가 아님
// 2부터 시작하여 소수들의 배수를 제거
// (중요) 이 경우 주어진 수 n의 제곱근 만 실시를 해도 소수를 거진 구할 수 있다고 판단
// (중오) 동시에 배수르 전부 처리하는 것이 아닌, 한번 더 고려할 필요없이 i*i 수 부터 배수로 생각해주는 것
for (int i = 2; i * i <= n; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= n; j += i) { // j에 계속
isPrime[j] = false; // i의 배수들은 소수가 아님
}
}
}
return isPrime;
}**
// Method 실행 main
public static void main(String[] args) {
int n = 30;
boolean[] isPrime = sieve(n);
System.out.println("Primes up to " + n + ":");
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
System.out.print(i + " ");
}
}
}
}
이분탐색 Binary Search
더보기
- 정렬된 배열에서 특정 값을 효율적으로 찾기 위한 알고리즘
- 배열을 반씩 줄여가면서 검색 범위를 줄여나가는 방식
- 시간 복잡도가 O(log n) 으로 엄청 빠름
이분탐색 Binary Search Flow
- 시작과 끝 인덱스 설정
- 배열의 시작 인덱스와 끝 인덱스를 설정
- 인덱스 이기 때문에 length로 지정하면 당연히 예외발생
- 중간 인덱스 계산
- 배열의 중간 인덱스를 계산
- 비교
- 중간 값이 찾고자 하는 값과 일치 → 값을 찾은 것이므로 인덱스를 반환
- 중간 값이 찾고자 하는 값보다 작으면 → 찾고자 하는 값이 오른쪽 절반에 있을 것이므로 왼쪽 인덱스 = 중간 인덱스 + 1
- 중간 값이 찾고자 하는 값보다 크면 → 찾고자 하는 값이 왼쪽 절반에 있을 것이므로 오른쪽 인덱스 = 중간 인덱스 - 1
- 반복
- 배열의 시작 인덱스가 끝 인덱스보다 클 때까지 위의 과정을 반복
- 반복에 조건이 있기 때문에 while문을 사용
- mid 값이 target일 경우
- mid (index값) 반환
- 값을 찾지 못한 경우
- 배열에 값이 존재하지 않으면 -1 또는 특정 값을 반환하여 값이 존재하지 않음 반환
- 배열의 시작 인덱스가 끝 인덱스보다 클 때까지 위의 과정을 반복
특정 값이 어느 index에 있는지 확인하는
public class BinarySearch {
// 반복적 방식
public static int binarySearch(int[] arr, int target) {
int left = 0; // 계속 중간 mid가 정해져야하기 때문에 0이더라도 무시하지 않고 지정해줘야함
int right = arr.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 자동 캐스팅으로 내림
if (arr[mid] == target) {
return mid; // 값을 찾은 경우 인덱스 반환
} else if (arr[mid] < target) {
left = mid + 1; // 오른쪽 절반으로 이동
} else {
right = mid - 1; // 왼쪽 절반으로 이동
}
}
return -1; // 값을 찾지 못한 경우 -1 반환
}
public static void main(String[] args) {
int[] arr = {1, 3, 5, 7, 9, 11, 13, 15};
int target = 7;
int result = binarySearch(arr, target);
System.out.println("Iterative Binary Search Result: " + result); // 출력: 3
}
슬라이딩 윈도우
더보기
- Array, List 선형 자료구조에서 연속적인 구간/부분에서의 합, 곱, 최댓값, 최솟값 등 특정 값을 효율적으로 계산하는 알고리즘
- 구간에서 딱 한번만 계산 → 시작/끝 조절을 통한 새로운 구간에서 다음값
- O(n)의 시간 복잡도
- 윈도우 크기 상관없이 요소를 한번씩만 넣었다 빼면서 처리하기 때문에
윈도우
- 구간 내에서 고정된 크기 또는 가변 크기의 연속된 요소
- 구간 내에서 한 요소씩 이동시키면서 필요한 값을 계산
슬라이딩 윈도우 Algorithm Flow
- 초기 계산
- 배열의 처음 k개의 요소로 초기 윈도우의 값을 계산
- 윈도우 이동
- 윈도우를 한 칸 오른쪽으로 이동시키면서 새로운 요소를 추가하고, 이전 윈도우에서 빠져나가는 요소를 제거하여 새로운 값을 계산
윈도우의 최대 합을 구하는 예제
public class SlidingWindow {
public static int maxSum(int[] arr, int k) {
// 윈도우 크기 검증
int n = arr.length;
if (n < k) {
System.out.println("윈도우 크기가 배열보다 큽니다.");
return -1; // int 는 null이 안되기 때문에
}
// 첫 윈도우의 합을 계산
int windowSum = 0;
for (int i = 0; i < k; i++) {
windowSum += arr[i];
}
int maxSum = windowSum;
// 슬라이딩 윈도우로 배열 탐색
for (int i = k; i < n; i++) {
windowSum += arr[i] - arr[i - k]; // 새로 들어온 요소를 더하고, 이전 윈도우의 첫 요소를 뺌
maxSum = Math.max(maxSum, windowSum);
}
return maxSum;
}
public static void main(String[] args) {
int[] arr = {2, 1, 5, 1, 3, 2};
int k = 3;
System.out.println("최대 구간 합: " + maxSum(arr, k)); // 출력: 9
}
}
배열 [2, 1, 5, 1, 3, 2] 와 k = 3 일 때:
- 초기 합: 2 + 1 + 5 = 8
- 다음 윈도우: 1 + 5 + 1 = 7
- 다음 윈도우: 5 + 1 + 3 = 9 (최대 합 갱신)
- 다음 윈도우: 1 + 3 + 2 = 6
동전 교환 알고리즘 Coin Change Problem
더보기
- 동전 사용
- 가장 큰 단위의 동전부터 시작하여, 해당 동전으로 교환 가능한 최대 개수를 계산
- 현재 동전 단위로 교환 가능한 개수
- java코드 복사 int coinCount = amount / coin;
- 해당 동전의 개수를 카운터에 추가
- java코드 복사 count += coinCount;
- 교환 후 남은 금액을 업데이트
- %= 해당 값에 몫을 다시 지정하는 것 직관적으로 생각
java코드 복사 amount %= coin;
- 다음 동전 단위로 이동:
- 다음 작은 단위의 동전으로 이동하여 3단계를 반복
- 이 과정은 동전 단위 배열의 끝까지 진행
- 결과 출력:
- 동전 교환이 완료된 후, 최종 동전 개수를 출력
package Algorithm;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
public class Greedy_CoinChange {
public static void main(String[] args) {
int[] coins = {500, 100, 50, 10};
int amount = 170;
// 동전 별 사용 갯수 저장 map
Map<Integer, Integer> coinUsage = new HashMap<>();
// 전체 사용 코인 갯수 출력
int totalCoin = getMinimumCoins(coins, amount, coinUsage);
System.out.println("전체 동전 숫자 : " + totalCoin);
// 코인 별 가지고 갯수 출력
for (int coin : coins) {
System.out.println(coin + "원 사용 갯수 : " + coinUsage.get(coin));
}
}
public static int getMinimumCoins(int[] coins, int amount, Map<Integer, Integer> coinUsage) {
int count = 0;
// coins 내림차순으로 정렬
Integer[] coinsInteger = Arrays.stream(coins)
.boxed()
.toArray(Integer[]::new);
Arrays.sort(coinsInteger, Comparator.reverseOrder());
// 큰 숫자 coin 부터 계산
for (Integer coin : coinsInteger) {
// 동전 별 갯수 구하기
int numCoins = amount / coin;
// 동전 별 갯수 등록
coinUsage.put(coin, numCoins);
// 총 숫자 세기
count += numCoins;
// 남은 돈 계산
amount %= coin;
}
return count;
}
}
모든 코드를 스스로 적으면서 정리를 하다보니 조금 시간이 초과가 되었지만 그래도 꾸준히 계속 공부할 수 있도록
'Today I Learned' 카테고리의 다른 글
(24.09.04) 코딩테스트 알고리즘 정리(동적 계획법 Dynamic Programming / 다익스트라 알고리즘) (0) | 2024.09.04 |
---|---|
(24.09.02) 코딩테스트 알고리즘 정리(전체탐색 : BFS/DFS) (1) | 2024.09.02 |
(24.08.26) 코딩테스트 - 알고리즘 준비 (0) | 2024.08.26 |
(24.08.13)[18주차] GitHub Actions 환경에서의 CI 테스트코드 이슈 트러블슈팅 (0) | 2024.08.13 |
(24.08.12)[18주차] AWS 의 CloudWatch를 통한 Monitoring & Logging (0) | 2024.08.12 |