
1주차의 주제 정렬 알고리즘!
정렬이란 무엇인가...
그래서 정렬 알고리즘에 대해 찾아봣습니다.
1. 정렬 알고리즘 종류
(1) 선택 정렬 (Selection Sort)
- 설명: 데이터 집합에서 최소값을 찾아 첫 번째 위치와 교환하고, 그 다음 최소값을 찾아 두 번째 위치와 교환하는 방식으로 정렬을 수행합니다. 이러한 과정을 반복하여 전체를 정렬합니다.
- 특징: 구현이 간단하지만, 시간 복잡도가 O(n²)로 비효율적입니다. 안정성을 보장하지 않습니다.

(2) 삽입 정렬 (Insertion Sort)
- 설명: 데이터 집합을 순회하면서 각 요소를 적절한 위치에 삽입하여 정렬을 수행합니다. 이미 정렬된 부분과 비교하여 새로운 요소를 삽입하는 방식입니다.
- 특징: 대부분의 요소가 이미 정렬되어 있는 경우 효율적이며, 최선의 경우 O(n)의 시간 복잡도를 가집니다. 안정성을 보장합니다.

(3) 버블 정렬 (Bubble Sort)
- 설명: 인접한 두 요소를 비교하여 필요에 따라 위치를 교환하며 정렬을 수행합니다. 이러한 과정을 반복하여 가장 큰 값이 맨 끝으로 이동하게 됩니다.
- 특징: 구현이 간단하지만, 시간 복잡도가 O(n²)로 비효율적입니다. 안정성을 보장합니다.

(4) 병합 정렬 (Merge Sort)
- 설명: 분할 정복(divide and conquer) 방식을 사용하여 배열을 반으로 나누고, 각각을 재귀적으로 정렬한 후 병합하여 전체를 정렬합니다.
- 특징: 시간 복잡도가 O(n log n)으로 안정적이며, 안정성을 보장합니다. 그러나 추가적인 메모리 공간이 필요합니다.

(5) 퀵 정렬 (Quick Sort)
- 설명: 피벗(pivot)을 선택하여 이를 기준으로 작은 값은 왼쪽, 큰 값은 오른쪽에 배치하고, 이러한 과정을 재귀적으로 수행하여 정렬합니다.
- 특징: 평균적으로 O(n log n)의 시간 복잡도를 가지며, 추가 메모리 공간이 거의 필요하지 않습니다. 그러나 최악의 경우 O(n²)의 시간 복잡도를 가질 수 있으며, 일반적으로 안정성을 보장하지 않습니다.

(6) 힙 정렬 (Heap Sort)
- 설명: 최대 힙(max heap) 또는 최소 힙(min heap)을 구성하여 최댓값 또는 최솟값을 추출하고, 이를 반복하여 정렬을 수행합니다.
- 특징: 시간 복잡도가 O(n log n)이며, 추가 메모리 공간이 거의 필요하지 않습니다. 그러나 일반적으로 안정성을 보장하지 않습니다.

(7) 계수 정렬 (Counting Sort)
- 설명: 데이터의 범위를 기준으로 각 값의 개수를 세어 정렬을 수행합니다. 주로 데이터의 범위가 제한적인 경우에 사용됩니다.
- 특징: 시간 복잡도가 O(n + k)로 매우 효율적이지만, 데이터의 범위가 클 경우 메모리 사용량이 많아질 수 있습니다. 안정성을 보장합니다.

2. 정렬 알고리즘 선택 시 고려사항
정렬 알고리즘을 선택할 때는 다음과 같은 요소를 고려해야 합니당
- 데이터의 크기: 데이터의 양이 많을수록 O(n log n)의 시간 복잡도를 가지는 알고리즘이 유리합니다.
- 데이터의 정렬 상태: 이미 정렬된 데이터에 가까울 경우 삽입 정렬이 효율적일 수 있습니다.
- 메모리 사용량: 추가적인 메모리 사용이 제한적인 경우 힙 정렬이나 퀵 정렬이 적합합니다.
- 안정성: 동일한 키 값을 가진 요소들의 순서를 유지해야 하는 경우 안정적인 정렬 알고리즘을 선택해야 합니다.
3. 정렬 알고리즘 실전 적용 예제
이제 위에서 정리한 정렬 개념을 실제 알고리즘 문제에 어떻게 적용할 수 있는지 3개의 문제를 통해 알아보자~
- 좌표 압축: 정렬을 통해 상대적인 위치(순위)를 계산하는 문제
- 카드: Map과 정렬을 조합하여 가장 빈도 높은 값을 찾는 문제
- 수 정렬하기 3: 계수 정렬을 활용해 메모리와 시간을 모두 절약하는 정렬 최적화 문제
(1) 좌표 압축
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
/*
입력이 많을 때 Scanner는 매우 느림
내부적으로 정규표현식을 사용해서 속도가 느리고
많은 양의 입력을 처리할 때 시간 초과가 날 수 있음
그래서 BufferedReader를 사용함
BufferedReader는 입력을 버퍼에 쌓아서 한 번에 처리하기 때문에 매우 빠름
*/
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] a = new int[n];
int[] b = new int[n];
/*
BufferedReader는 한 줄 전체를 문자열로 받기 때문에
공백으로 구분된 숫자들을 나누려면 별도의 처리가 필요함
StringTokenizer는 split()보다 속도도 빠르고 메모리 사용도 적음
대량의 숫자를 처리할 때 StringTokenizer가 효율적임
*/
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 0; i < n; i++) {
a[i] = Integer.parseInt(st.nextToken());
b[i] = a[i];
}
/*
좌표 압축을 하기 위해 정렬을 사용함
정렬된 배열에서 중복을 제거하고 그 값을 기준으로 인덱스를 부여함
*/
Arrays.sort(b);
/*
Map을 사용해서 각 숫자에 대해 압축된 인덱스를 저장함
중복된 숫자는 한 번만 처리하도록 containsKey로 체크함
*/
Map<Integer, Integer> m = new HashMap<>();
int idx = 0;
for (int x : b) {
if (!m.containsKey(x)) {
m.put(x, idx++);
}
}
/*
출력도 Scanner처럼 반복적으로 System.out.print를 쓰면 매우 느림
StringBuilder를 사용해서 결과를 모두 문자열로 만들어 놓고
마지막에 한 번만 출력하면 성능이 훨씬 좋아짐
*/
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(m.get(a[i])).append(" ");
}
System.out.println(sb.toString().trim());
}
}
이 문제는 정수들이 주어졌을 때 각각의 수를 정렬했을 때의 상대적인 위치(0부터 시작하는 순서)로 바꾸는 문제다.
예를 들어 [1000, 999, 1000, 1001]이 주어졌다면 이를 압축하면 [1, 0, 1, 2]가 되어야 한다.
중복된 값은 같은 순위로 처리되기 때문에 중복 제거가 핵심!
처음엔 단순히 정렬만 하면 되는 줄 알았는데 문제는 값의 상대적인 순위를 출력하는 거라 단순 정렬로는 해결이 되지 않았다.
그래서 다음과 같은 흐름으로 접근했다.
- 원본 배열을 복사해서 정렬한다.
- 정렬된 배열에서 중복을 제거하고, 각 값을 몇 번째인지 Map에 저장한다.
- 원래 배열에서 각 값을 Map에서 찾아서 순위(압축된 값)를 출력한다.
Map을 사용한 이유는 빠르게 특정 값의 순위를 찾아야 했기 때문
주의했던 점
- 입력이 많기 때문에 입출력을 반드시 최적화해야 했다. Scanner와 print를 사용하면 시간 초과가 났다.
- 중복을 제거하지 않으면 동일한 값이 다른 순위를 갖게 돼 정답이 틀릴 수 있다.
- 출력은 반드시 입력 순서를 유지해야 하므로 Map에 저장한 뒤 원래 배열 순서대로 출력해야 했다.
(2) 카드
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws IOException {
// 빠른 입력 처리 (Scanner는 느리므로 BufferedReader 사용)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
Map<Long, Integer> m = new HashMap<>();
// 숫자 카드의 범위가 2^62보다 작다 했으므로 long 타입 사용
for (int i = 0; i < n; i++) {
long x = Long.parseLong(br.readLine());
m.put(x, m.getOrDefault(x, 0) + 1);
}
/*
Map의 keySet을 list로 변환 후 정렬하는 이유
1. 가장 많이 나온 숫자를 찾아야 함
2. 동점인 경우 작은 수를 출력해야 함
그래서 정렬을 먼저 하면 가장 작은 수부터 탐색 가능
이후 count가 가장 큰 것을 찾아내면, 자동으로 조건을 만족함
sort를 먼저 하면 동일 빈도면 작은 수라는 조건을 손쉽게 처리 가능
*/
List<Long> keyList = new ArrayList<>(m.keySet());
Collections.sort(keyList); // 오름차순 정렬 (기본)
long ans = 0;
int max = 0;
for (long k : keyList) {
int cnt = m.get(k);
if (cnt > max) {
max = cnt;
ans = k;
}
// cnt == max 인 경우는 무시, 이미 정렬되어 있어서 작은 수가 먼저 나옴
}
System.out.println(ans);
}
}
이 문제는 입력된 숫자 카드 중 가장 많이 나온 숫자를 찾는 문제다.
단, 같은 횟수로 나온 숫자가 여러 개일 경우에는 더 작은 숫자를 출력해야 한다.
처음엔 배열을 정렬하고 앞에서부터 빈도를 세는 방식으로 접근했지만 동점 처리와 속도 면에서 만족스럽지 않았다.
그래서 다음과 같이 바꿨다.
- Map을 사용해 숫자별 등장 횟수를 저장한다.
- keySet을 리스트로 만들어 정렬한다. (정렬하면 작은 수가 앞에 오게 됨)
- 리스트를 순회하면서 가장 빈도 높은 숫자를 찾는다. 동점일 경우 첫 번째 나온 수를 유지하면 조건을 만족하게 된다.
주의했던 점
- 수의 범위가 매우 크므로 int가 아닌 long을 사용해야 했다.
- 동점 처리에서 정렬을 하지 않으면, 더 큰 수가 출력될 수 있기 때문에 반드시 key 정렬이 필요하다.
- 역시 입력 수가 많기 때문에 입출력 최적화가 필수였다.
(3) 수 정렬하기 3
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
//Scanner 쓰면 시간 초과
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
// 숫자의 최대 범위가 10,000 미만이므로 배열로 카운트 가능
// 각 숫자가 몇 번 나왔는지 배열에 저장
int[] cnt = new int[10001];
for (int i = 0; i < n; i++) {
int x = Integer.parseInt(br.readLine());
cnt[x]++;
}
//StringBuilder로 문자열 한 번에 출력
StringBuilder sb = new StringBuilder();
/*
등장한 숫자를 오름차순으로 탐색하면서 그 수가 몇 번 나왔는지를 기반으로 출력
시간복잡도는 O(N + K), 여기서는 거의 O(N)으로 동작
*/
for (int i = 1; i < 10001; i++) {
while (cnt[i]-- > 0) {
sb.append(i).append('\n');
}
}
System.out.print(sb);
}
}
이 문제는 단순한 오름차순 정렬 문제인데 입력 수가 무려 1,000만 개까지 주어질 수 있다는 점이 핵심
일반적인 정렬 알고리즘을 사용하면 시간 초과가 발생할 수밖에 없는 상황
문제의 조건을 유심히 보니 값의 범위가 1 이상 10,000 미만이었다.
값의 종류는 적지만 개수는 매우 많다는 특성이 있어서 계수 정렬 방식으로 접근했다.
- 값의 개수를 저장할 배열을 선언한다. (크기는 10001)
- 입력으로 받은 숫자들의 개수를 배열에 기록한다.
- 배열을 순회하면서 등장한 횟수만큼 해당 숫자를 출력한다.
주의했던 점
- Scanner나 System.out.println()을 사용하면 절대 시간 안에 못 끝난다. BufferedReader와 StringBuilder는 필수다.
- 배열 크기를 실수하지 않도록 값의 최대값 + 1까지 선언해야 한다.
- 계수 정렬은 값의 범위가 크면 쓸 수 없지만, 이 문제처럼 범위가 작을 땐 최고의 선택이다.
4. 후기
아 집가고싶다.
'FOSCAR-(Autonomous Driving) > 알고리즘 스터디' 카테고리의 다른 글
[2025 1학기 알고리즘 스터디] 김규현 #2주차 (0) | 2025.04.09 |
---|---|
[2025 1학기 알고리즘 스터디] 신지은 #1주차 (0) | 2025.04.02 |
[2023 알고리즘 스터디] 2조 박주빈 5주차 - 백준 23882, 10989, 2751, 11650 (1) | 2023.02.27 |
[2023 알고리즘 스터디] 4조 이은선 5주차 - 백준 2075 (0) | 2023.02.26 |
[2023 알고리즘 스터디] 1조 정혁제 5주차 - 정렬 (1) | 2023.02.26 |