Company
교육 철학

정렬 알고리즘

정렬 알고리즘 시간복잡도 비교

정렬 알고리즘
시간복잡도
선택정렬
n^2
삽입정렬
n^2
버블정렬
n^2
병합정렬
nlogn
힙정렬
nlogn
퀵정렬
nlogn
트리정렬
nlogn
팀정렬
nlogn
핵심: 특정 정렬알고리즘이 무조건 좋다는 개념은 없다. 원소의 개수, 메모리의 크기 등등을 전부 고려하여 버블정렬이라도 최고의 효율을 낼 때가 있다.

선택정렬 (Selection Sort) 개념

동작 원리

1번째부터 끝날 때까지 모든 원소들을 순회한후 가장 작은걸 1번째로 교환
그다음에는 2번째 위어값은 방법을 반복한다

예시 배열

[8, 17, 18, 22, 50, 32, 33, 44, 29, 25]
Plain Text
복사

시간 복잡도

모든 경우에서 동일한 성능

Average (평균): O(n^2)
Worst (최악): O(n^2)
Best (최선): O(n^2)
특징: 입력 데이터의 상태와 관계없이 항상 동일한 시간복잡도를 가짐

선택정렬 구현 코드

void selecSort(std::vector<int>& arr) { int n = arr.size(); for (int i = 0; i < n - 1; i++) { int min = i; // 최소값의 인덱스를 현재 위치로 초기화 // i+1부터 끝까지 최소값 찾기 for (int j = i + 1; j < n; j++) { if (arr[j] < arr[min]) { min = j; // 더 작은 값의 인덱스 저장 } } // 최소값과 현재 위치의 값 교환 if (min != i) { std::swap(arr[i], arr[min]); } } }
C++
복사

동작 과정 단계별 분석

1단계: 첫 번째 자리에 최소값 배치

전체 배열에서 최소값을 찾아 첫 번째 자리와 교환
[8, 17, 18, 22, 50, 32, 33, 44, 29, 25] → 최소값 8은 이미 제자리

2단계: 두 번째 자리에 나머지 중 최소값 배치

두 번째부터 끝까지 중에서 최소값을 찾아 두 번째 자리와 교환

반복

이 과정을 n-1번 반복 (마지막 원소는 자동으로 정렬됨)

알고리즘 특징

장점

1.
구현이 간단: 이해하기 쉽고 코드가 직관적
2.
메모리 효율적: 제자리 정렬(In-place sorting)
3.
안정적인 성능: 입력 상태와 무관하게 일정한 성능

단점

1.
느린 속도: O(n^2) 시간복잡도
2.
불안정 정렬: 동일한 값의 상대적 순서가 보장되지 않음
3.
최적화 불가: 이미 정렬된 배열에 대해서도 동일한 시간

삽입정렬 (Insertion Sort)

핵심 개념

k번째 원소를 1부터 k-1까지와 비교해 적절한 위치에 끼워넣고 그 뒤의 자료를 한칸 씩 뒤로 밀어내는 형식이다.

시간복잡도

Average (평균): O(n^2)
Worst (최악): O(n^2)
Best (최선): O(n) 이미 정렬된 경우

삽입정렬 구현 코드

void insertSort(std::vector<int>& arr) { for (int i = 1; i < arr.size(); i++) { int key = arr[i]; // 현재 삽입할 원소 int j = i - 1; // 정렬된 부분의 마지막 인덱스 // key보다 큰 원소들을 한 칸씩 뒤로 이동 while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; // key를 적절한 위치에 삽입 } }
C++
복사

동작 과정

1.
2번째 원소부터 시작 (첫 번째는 이미 정렬됨)
2.
현재 원소(key)를 저장
3.
앞쪽 정렬된 부분과 비교하며 적절한 위치 찾기
4.
큰 원소들을 한 칸씩 뒤로 이동
5.
찾은 위치에 key 삽입

🫧 버블정렬 (Bubble Sort)

핵심 개념

시간복잡도가 안좋긴 하지만 코드가 단순하다. 원소의 이동이 서로 앞뒤로 되어있으면 거품처럼 정렬되는 듯한 모습을 보여주어서 버블정렬이라고 한다.

시간복잡도

Average (평균): O(n^2)
Worst (최악): O(n^2)
Best (최선): O(n) 이미 정렬된 경우 (최적화 시)

버블정렬 구현 코드 (수정됨 )

void bubbleSort(std::vector<int>& arr) // 🔧 bubleSort → bubbleSort 수정 { for (int i = 0; i < arr.size(); i++) { // 🔧 최적화: 매 패스마다 가장 큰 원소가 뒤로 가므로 범위 축소 for (int j = 0; j < arr.size() - 1 - i; j++) { if (arr[j] > arr[j + 1]) { std::swap(arr[j], arr[j + 1]); } } } }
C++
복사

코드 수정 사항

1.
함수명 수정: bubleSortbubbleSort (올바른 철자)
2.
효율성 개선: 내부 루프 조건을 j < arr.size() - 1 - i로 변경
매 패스마다 가장 큰 원소가 맨 뒤로 가므로 비교 범위를 줄임

동작 과정

1.
인접한 두 원소 비교
2.
순서가 잘못되면 교환
3.
배열 끝까지 반복 (한 패스)
4.
전체 과정을 n번 반복
5.
매 패스마다 가장 큰 원소가 "거품처럼" 뒤로 이동

삽입정렬 추천

작은 배열 (n < 50)
거의 정렬된 데이터
온라인 정렬이 필요한 경우

버블정렬 추천

알고리즘 학습 목적
정렬 개념 이해를 위한 교육
실제 프로젝트에서는 권장하지 않음

병합정렬 - Merge Sort (머지소트)

핵심 개념

배열의 길이가 1이 될때까지 계속 분할하고, 그 후 2개의 부분 배열에서 값을 비교하여 합병을 진행한다. 이와 같은 과정을 모든 부분의 배열이 합병될때까지 반복한다.
기존에 있는 배열을 여러개로 나누어서 사용하기 때문에 추가적인 메모리 공간이 필요하다.

시간복잡도

Best (최선): O(nlogn)
Worst (최악): O(nlogn)
Average (평균): O(nlogn)
특징: 입력 데이터의 상태와 관계없이 항상 안정적인 O(nlogn) 성능을 보장

시간복잡도 계산 과정

분할 과정 (예: 16개 원소)

16회 반복 노드는 1개 → 16 x 1 = 16회 반복
8회 반복 노드는 2개 → 8 x 2 = 16회 반복
4회 반복 노드는 4개 → 4 x 4 = 16회 반복
2회 반복 노드는 8개 → 2 x 8 = 16회 반복

총 반복문 수 계산

총 반복문 수 = (트리높이) × (16회 반복) = (log₂ N + 1) × (2N) = 2N log₂ N + 2N → O(n log n)
Plain Text
복사

병합정렬 완전 구현

#include <vector> #include <iostream> std::vector<int> vect = {3, 5, 15, 26, 27}; // 전역 배열 std::vector<int> result(100); // 임시 배열 (추가 메모리) void run(int start, int end) { int mid = (start + end) / 2; // 기저 조건: 원소가 1개면 종료 if (start == end) { return; } // 분할 (Divide) run(start, mid); // 왼쪽 절반 정렬 run(mid + 1, end); // 오른쪽 절반 정렬 // 정복 (Conquer) - 병합 과정 int a = start; // 왼쪽 부분 배열 포인터 (start ~ mid) int b = mid + 1; // 오른쪽 부분 배열 포인터 (mid+1 ~ end) int idx = 0; // 결과 배열 인덱스 // 두 부분 배열을 비교하며 병합 while (true) { // 두 부분 배열을 모두 처리했으면 종료 if (a > mid && b > end) break; // 왼쪽 배열을 모두 처리한 경우 if (a > mid) result[idx++] = vect[b++]; // 오른쪽 배열을 모두 처리한 경우 else if (b > end) result[idx++] = vect[a++]; // 두 원소 비교하여 작은 것 선택 else if (vect[a] < vect[b]) result[idx++] = vect[a++]; else result[idx++] = vect[b++]; } // 임시 배열의 결과를 원본 배열로 복사 for (int i = 0; i < idx; i++) { vect[start + i] = result[i]; } } int main() { run(0, 4); // 0번 인덱스부터 4번 인덱스까지 정렬 return 0; }
C++
복사

알고리즘 동작 과정

1단계: 분할 (Divide)

[3, 5, 15, 26, 27] ↓ [3, 5] [15, 26, 27] ↓ ↓ [3] [5] [15] [26, 27] ↓ [26] [27]
Plain Text
복사

2단계: 정복 (Conquer)

[3] [5] → [3, 5] [26] [27] → [26, 27] [15] [26, 27] → [15, 26, 27] [3, 5] [15, 26, 27] → [3, 5, 15, 26, 27]
Plain Text
복사

분할정복 전략

핵심 3단계

1.
분할 (Divide): 배열을 반으로 나누기
2.
정복 (Conquer): 각 부분을 재귀적으로 정렬
3.
결합 (Combine): 정렬된 두 부분을 병합

재귀 구조

기저 조건: start == end (원소 1개)
재귀 호출: 왼쪽 절반 + 오른쪽 절반
병합: 정렬된 두 부분을 합치기

장점과 단점

장점

1.
안정적인 성능: 항상 O(nlogn) 보장
2.
안정 정렬: 동일한 값의 순서 유지
3.
예측 가능: 최악의 경우에도 성능 저하 없음
4.
대용량 데이터: 큰 데이터셋에 효율적

단점

1.
추가 메모리: O(n)의 공간 복잡도 필요
2.
재귀 오버헤드: 함수 호출 스택 사용
3.
작은 배열: 작은 데이터에서는 오버헤드 존재

최적화 기법

1. 작은 배열에서 삽입정렬 사용

if (end - start < 10) { insertionSort(start, end); // 작은 부분은 삽입정렬 return; }
C++
복사

2. 이미 정렬된 경우 체크

if (vect[mid] <= vect[mid + 1]) { return; // 이미 정렬되어 있으면 병합 생략 }
C++
복사

실전 활용

언제 사용할까?

안정적인 성능이 중요한 경우
안정 정렬이 필요한 경우
대용량 데이터 처리
최악의 경우 성능을 보장해야 할 때

메모리가 충분한 환경에서 추천!

연습 문제

1.
분할 과정: [8, 3, 5, 4, 7, 6, 1, 2]를 트리로 그려보기
2.
병합 과정: 위 배열의 병합 단계별 과정 써보기
3.
시간복잡도: n=32일 때 총 비교 횟수 계산하기

힙정렬 (Heap Sort)

핵심 개념

선택정렬과 비슷하지만, 힙(자료구조)을 사용해서 가장 큰 원소를 찾는다는 차이점이 있다.
트리 기반으로 최대힙트리, 최소힙 트리를 구성하여 데이터를 정렬한다.
항상 nlogN의 성능을 발휘해 안정적인 속도를 보여준다.

힙(Heap)이란?

완전 이진트리의 일종으로 우선순위 큐를 위해 만들어진 자료구조이다. 여러값 중 최솟값, 최댓값을 빠르게 찾기 위해서 데이터를 삽입할때 이미 정렬을 같이 진행해준다.
(이진 탐색 트리와 다르게 중복값을 허용한다)

힙의 종류

최대 힙 (Max Heap): 부모 노드가 자식 노드보다 큰 값
최소 힙 (Min Heap): 부모 노드가 자식 노드보다 작은 값

힙의 특징

완전 이진 트리 구조
우선순위 큐 구현에 최적
루트 노드에 항상 최대/최소값 위치

시간복잡도

Average (평균): O(nlogn)
Worst (최악): O(nlogn)
Best (최선): O(nlogn)
특징: 병합정렬과 마찬가지로 모든 경우에서 안정적인 O(nlogn) 성능

힙정렬 구현 (STL 사용)

#include <vector> #include <queue> void heapSort(std::vector<int>& arr) { // 1단계: 모든 원소를 우선순위 큐(최대 힙)에 삽입 std::priority_queue<int> pq; // 기본적으로 최대 힙 for (int i = 0; i < arr.size(); i++) { pq.push(arr[i]); // 힙에 원소 삽입 O(logn) } // 2단계: 힙에서 최대값부터 꺼내어 배열에 저장 for (int i = 0; i < arr.size(); i++) { arr[i] = pq.top(); // 최대값 가져오기 pq.pop(); // 힙에서 제거 O(logn) } }
C++
복사

동작 과정

예시: [5, 3, 1, 8, 7, 2, 4] 정렬

1단계: 힙 구성 (Heapify)

원본 배열: [5, 3, 1, 8, 7, 2, 4] ↓ 우선순위 큐에 삽입하면서 최대 힙 구성: 8 / \ 7 4 / \ / \ 5 3 2 1
Plain Text
복사

2단계: 정렬 (Extract)

1. pq.top() = 8 → arr[0] = 8, 힙에서 제거 2. pq.top() = 7 → arr[1] = 7, 힙에서 제거 3. pq.top() = 5 → arr[2] = 5, 힙에서 제거 4. pq.top() = 4 → arr[3] = 4, 힙에서 제거 5. pq.top() = 3 → arr[4] = 3, 힙에서 제거 6. pq.top() = 2 → arr[5] = 2, 힙에서 제거 7. pq.top() = 1 → arr[6] = 1, 힙에서 제거 결과: [8, 7, 5, 4, 3, 2, 1] (내림차순)
Plain Text
복사

선택정렬과의 비교

구분
선택정렬
힙정렬
최대값 찾기
O(n) 선형 탐색
O(1) 힙의 루트
시간복잡도
O(n²)
O(nlogn)
자료구조
배열만 사용
힙(트리) 구조
안정성
불안정 정렬
불안정 정렬

힙정렬의 전체 과정

1. 힙 구성 (Build Heap): O(n)

배열을 최대 힙으로 변환

2. 정렬 (Sort): O(nlogn)

루트(최대값)를 배열 끝으로 이동
힙 크기를 1 감소
힙 속성 복구 (Heapify)
n번 반복

장점과 단점

장점

1.
안정적인 성능: 항상 O(nlogn) 보장
2.
제자리 정렬: 추가 메모리 불필요 (배열 기반 구현 시)
3.
최악의 경우에도 효율적: 퀵정렬보다 안정적
4.
큰 데이터셋: 대용량 데이터 처리에 적합

단점

1.
불안정 정렬: 동일 값의 순서 변경 가능
2.
캐시 성능: 배열 기반 알고리즘보다 캐시 미스 많음
3.
구현 복잡도: 단순 정렬보다 복잡
4.
STL 의존: 위 코드는 STL priority_queue 사용

실제 힙정렬 구현 (배열 기반)

void heapify(std::vector<int>& arr, int n, int i) { int largest = i; // 루트를 가장 큰 값으로 초기화 int left = 2 * i + 1; // 왼쪽 자식 int right = 2 * i + 2; // 오른쪽 자식 // 왼쪽 자식이 루트보다 크면 if (left < n && arr[left] > arr[largest]) largest = left; // 오른쪽 자식이 현재 가장 큰 값보다 크면 if (right < n && arr[right] > arr[largest]) largest = right; // 가장 큰 값이 루트가 아니면 if (largest != i) { std::swap(arr[i], arr[largest]); heapify(arr, n, largest); // 재귀적으로 힙 속성 유지 } } void heapSort(std::vector<int>& arr) { int n = arr.size(); // 1단계: 최대 힙 구성 for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i); // 2단계: 힙에서 원소를 하나씩 추출 for (int i = n - 1; i >= 0; i--) { std::swap(arr[0], arr[i]); // 루트(최대값)를 끝으로 이동 heapify(arr, i, 0); // 힙 크기를 줄이고 다시 힙 구성 } }
C++
복사

실전 활용

언제 사용할까?

안정적인 성능이 중요할 때
메모리가 제한적인 환경
최악의 경우를 대비해야 할 때
우선순위 큐 기반 알고리즘

대용량 데이터에서 신뢰할 수 있는 선택!

연습 문제

1.
힙 구성: [4, 10, 3, 5, 1]을 최대 힙으로 만드는 과정
2.
정렬 과정: 위 힙에서 힙정렬 전 과정 단계별 설명
3.
최소 힙: 최소 힙을 사용한 오름차순 정렬 구현

퀵정렬 (Quick Sort)

핵심 개념

데이터 집합에서 임의 기준 피벗을 정하고, 집합을 해당하는 피벗을 기준으로 두개의 집합으로 나눈다. 한쪽 집합에서는 피벗보다 작은 값을, 나머지 한쪽은 큰값을 모아서 넣는다.
더이상 쪼갤 부분이 없을때까지 위 과정을 반복한다.

분할정복 전략

핵심 3단계

1.
피벗 선택 (Pivot): 기준값 선정
2.
분할 (Partition): 피벗보다 작은 값 / 큰 값으로 분리
3.
정복 (Conquer): 각 부분을 재귀적으로 정렬

시간복잡도

Best (최선): O(nlogn) 피벗이 중간값일 때
Average (평균): O(nlogn) 일반적인 경우
Worst (최악): O(n²) 이미 정렬되었거나 역순일 때
특징: 평균적으로는 매우 빠르지만 최악의 경우 성능 저하
void quickSort(std::vector<int>& arr) { if (arr.size() <= 1) return; std::stack<int> s; s.push(0); s.push(arr.size() - 1); while (!s.empty()) { int end = s.top(); s.pop(); int start = s.top(); s.pop(); int pivot = arr[end]; int i = start - 1; for (int j = start; j < end; j++) { if (arr[j] < pivot) { i++; std::swap(arr[i], arr[j]); } } std::swap(arr[i + 1], arr[end]); int p = i + 1; if (p - 1 > start) { s.push(start); s.push(p - 1); } if (p + 1 < end) { s.push(p + 1); s.push(end); } } }
Python
복사

언제 사용할까?

퀵정렬 추천 상황

일반적인 정렬 (가장 범용적)
메모리가 제한적인 환경
평균적으로 빠른 성능이 필요할 때
캐시 효율성이 중요할 때

퀵정렬 피해야 할 상황

최악 성능을 절대 허용할 수 없을 때
안정 정렬이 반드시 필요할 때
이미 정렬된 데이터를 자주 처리할 때