정렬 알고리즘 개요
정렬(Sorting) : 데이터를 특정한 기준에 따라서 순서대로 나열하는 것
- '이진 탐색(Binary Search)'을 위한 전처리 과정
- 정렬 알고리즘 종류 : 선택, 삽입, 퀵, 계수 등
- 파이썬의 기본 정렬 라이브러리를 적용하면 좀 더 효과적
- 알고리즘 효율의 중요성 → 면접 단골문제
내림차순 정렬 방법
- 오름차순 정렬 알고리즘에서 크기 비교를 반대로 수행
- 파이썬 리스트 원소 뒤집는 메서드 : 복잡도 O(N)
선택 정렬
매번 '가장 작은 것을 선택'하는 정렬방식
가장 작은 데이터를 선택해 맨 앞에 있는 데이터와 교환, 그다음 작은 데이터를 앞에서 두번째 데이터와 교환 → 반복
선택 정렬 소스코드
# 선택 정렬 소스코드
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
for i in range(len(array)):
min_index = i # 가장 작은 원소의 인덱스
for j in range(i + 1, len(array)):
if array[min_index] > array[j]:
min_index = j
array[i], array[min_index] = array[min_index], array[i] # 스와프
# 출력 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
* 스와프 : 특정 리스트가 주어졌을 때, 두 변수의 위치를 변경하는 작업
다른 프로그래밍 언어에서는 명시적으로 임시 저장용 변수를 만들어 두 원소의 값 변경해야 함
cf. C언어로 구현한 스와프 예제
int a = 3;
int b = 5;
// 스와프 진행
int temp = a;
a = b;
b = temp;
선택 정렬 시간복잡도 O(N^2)
- N - 1번 만큼 가장 작은 수를 찾아서 맨 앞으로 보내야 한다.
- 연산 횟수 : N + (N - 1) + (N - 2) + ... + 2
- 근사치 : N x (N + 1) / 2
- 직관적으로는 소스코드에서 2중 반복문이 사용되었기 때문
- 선택 정렬을 이용하는 경우 데이터 개수가 10,000개 이상이면 속도 급격히 느려짐
- 다른 알고리즘에 비해 매우 비효율적
* 파이썬 내장 기본 정렬 라이브러리는 내부적으로 C언어 기반, 최적화 테크닉으로 빠르게 동작
삽입 정렬
'데이터를 하나씩 확인하며, 각 데이터를 정렬된 적절한 위치에 삽입'
- 필요할 때만 위치를 바꾸므로 '데이터가 거의 정렬되어 있을 때' 훨씬 효율적
- 정렬 데이터 리스트의 적절한 위치를 찾아 삽입
- 적절한 위치에 들어가기 이전까지는 이미 정렬되었다고 가정
- 두번째 데이터부터 정렬 시작 : 첫번째 데이터는 이미 그 자체로 정렬
- 정렬이 이루어진 원소는 항상 오름차순 유지
- 삽입 위치 선정시, 자신보다 작은 데이터 만나면 그 위치에 삽입 : 왼쪽 데이터는 이미 정렬된 상태
삽입 정렬 소스코드
# 삽입 정렬 소스코드
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
for i in range(1, len(array)):
for j in range(i, 0, -1): # 인덱스 i부터 1까지 감소하며 반복하는 문법
if array[j] < array[j - 1]: # 한 칸씩 왼쪽으로 이동
array[j], array[j - 1] = array[j - 1], array[j]
else: # 자기보다 작은 데이터를 만나면 그 위치에서 멈춤
break
print(array)
# 출력 : [1, 2, 3, 4, 5, 6, 7, 8, 9]
삽입 정렬의 시간 복잡도 O(N^2)
반복문이 2번 중첩
현재 리스트의 데이터가 거의 정렬되어 있는 상황이라면 매우 빠르게 동작, 퀵정렬보다 강력 : O(N)
거의 정렬되어 있는 상태로 입력이 주어지는 문제 → 삽입 정렬 이용
퀵 정렬
기준 데이터(피벗)를 설정하고 그 기준보다 큰 데이터와 작은 데이터의 위치 바꿈
- 지금까지 배운 정렬 알고리즘 중 가장 많이 사용(병합 정렬 방식도 빠름)
- 기준을 설정한 후, 큰 수와 작은 수를 교환하고, 리스트를 반으로 나누는 방식으로 동작 → 반복
- 피벗(pivot) : 큰 수와 작은 숫자를 교환할 때, 교환하기 위한 '기준'
- 호어 분할 방식 : 리스트의 첫 번째 데이터를 피벗으로 설정
정렬 방식
- 왼쪽에서부터 피벗보다 큰 데이터, 오른쪽에서부터 피벗보다 작은 데이터
- 큰 데이터와 작은 데이터의 위치 서로 교환
- 1, 2 반복
- 피벗에 대한 정렬 완료
- '재귀 함수'와 동작 원리 동일 : 구현 간결
종료 조건 : 리스트위의 원소가 1개 → 더이상 분할 불가능
퀵 정렬 소스코드 : Original
# 퀵 정렬 소스코드
array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]
def quick_sort(array, start, end):
# 종료 조건
if start >= end: # 원소가 1개인 경우 종료
return
pivot = start # 피벗은 첫번째 원소
left = start + 1
right = end
while left <= right:
# 피벗보다 큰 데이터를 찾을 때까지 반복
# left를 오른쪽으로 계속 옮길거라서 end보다 작아야 함
while (left <= end) and (array[left] <= array[pivot]):
left += 1
# 피벗보다 작은 데이터를 찾을 때까지 반복
while (right < start) and (array[right] >= array[pivot]):
right -= 1
if left > right: # 엇갈렸다면 작은 데이터와 피벗을 교체-> 피벗에 대한 정렬 완료됨
array[right], array[pivot] = array[pivot], array[right]
else: # 엇갈리지 않았다면 작은 데이터와 큰 데이터를 교체 -> 피벗에 대한 정렬 진행중
array[left], array[right] = array[right], array[left]
# 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬 수행
quick_sort(array, start, right - 1)
quick_sort(array, right + 1, end)
quick_sort(array, 0, len(array) - 1)
print(array)
# 결과 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
퀵 정렬 소스코드 : short ver.
# 파이썬의 장점을 살린 퀸 정렬 소스코드
array = [5, 7, 9, 0, 3, 1, 6, 2, 4, 8]
def quick_sort(array):
# 리스트가 하나 이하의 원소만을 담고 있다면 종료
if len(array) <= 1:
return array
pivot = array[0] # 피벗은 첫번째 원소
tail = array[1:] # 피벗을 제외한 리스트
left_side = [x for x in tail if x <= pivot] # 분할된 왼쪽 부분
right_side = [x for x in tail if x > pivot] # 분할되 오른쪽 부분
# 분할 이후 왼쪽 부분과 오른쪽 부분에서 각각 정렬을 수행하고, 전체 리스트를 반환
return quick_sort(left_side) + [pivot] + quick_sort(right_side)
print(quick_sort(array))
# 결과 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
퀵 정렬의 시간 복잡도 : 평균 O(NlogN)
- 증명은 복잡하지만, 도식화를 해보면 N개일 때 높이는 약 logN이라고 판단
- 분할 횟수가 기하급수적으로 감사
- 데이터의 개수가 많을수록 차이가 매우 극명해짐
- 최악의 경우 O(N^2) : 이미 데이터가 정렬되어 있는 경우
계수 정렬
'데이터의 크기 범위가 제한되어 정수 형태로 표현할 수 있을 때'
- 특정한 조건이 부합할 때만 사용, 매우 빠른 정렬 알고리즘
- 모든 데이터가 양의 정수, 데이터 N개, 최댓값 K → 수행 시간 O(N + K) 보장
- 데이터 크기 차이가 너무 크면 사용할 수 없음 → 차이 크기만큼의 리스트 초기화해야 하므로
- 일반적으로 가장 큰 데이터와 작은 데이터 차이가 1,000,000(백만)을 넘지 않을 때 효과적
- 비교 기반의 정렬 알고리즘이 아님
정렬 방식
- 가장 큰 데이터와 가장 작은 데이터의 범위가 모두 담길 수 있도록 하나의 리스트 생성
이때, 리스트 인덱스가 모든 범위를 포함하도록 - 리스트의 모든 데이터 0으로 초기화
- 데이터 값과 동일한 인덱스 데이터 +1
- 완료!
계수 정렬 소스코드
# 계수 정렬 소스코드
# 모든 원소의 값이 0보다 크거나 같다고 가정
array = [7, 5, 9, 0, 3, 1, 6, 2, 9, 1, 4, 8, 5, 2]
# 모든 범위를 포함하는 리스트 선언(모든 값은 0으로 초기화)
count = [0] * (max(array) + 1)
for i in range(len(array)): # 각 데이터에 해당하는 인덱스의 값 증가
count[array[i]] += 1
for i in range(len(count)): # 리스트에 기록된 정렬 정보 확인
for j in range(count[i]):
print(i, end=' ') # 띄어쓰기를 구분으로 등장한 횟수만큼 인덱스 출력
# 출력 : 0 0 1 1 2 2 3 4 5 5 6 7 8 9 9
계수 정렬의 시간 복잡도 O(N + K)
항상 빠르게 동작
기수 정렬과 더불어 가장 빠름
계수 정렬의 공간 복잡도 O(N + K)
때에 따라 심각한 비효율성 ex. 0과 999,999 (2개의 수 정렬)
동일한 값을 가지는 데이터가 여러 개 등장할 때 적합 ex. 성적(동일 성적 여러명)
특성을 파악하기 어렵다면 퀵 정렬이 유리
파이썬의 정렬 라이브러리
미리 만들어진 라이브러리를 이용하는 것이 주로 효과적
최악의 경우에도 시간 복잡도 O(NlogN)
sorted( ) 함수
정렬된 결과를 출력
# sorted 소스코드
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
result = sorted(array)
print(result)
# 출력 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
sort( ) 함수
내부 원소를 바로 정렬
# sorted 소스코드
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8]
array.sort()
print(result)
# 출력 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
key 매개변수 활용
key 값으로 하나의 함수를 받아 정렬 기준으로 삼음
# 정렬 라이브러리에서 key를 활용한 소스코드
array = [('바나나', 2), ('사과', 5), ('당근', 3)]
def setting(data):
return data[1]
result = sorted(array, key=setting)
print(result)
# 결과 : [('바나나', 2), ('당근', 3), ('사과', 5)]
정렬 라이브러리의 시간 복잡도 O(NlogN)
퀵 정렬보다 효과적
단순 정렬에서는 기본 정렬 라이브러리 사용
데이터 범위 한정된 경우, 빠르게 동작해야 하면 계수 정렬
정렬 알고리즘이 사용되는 경우
- 정렬 라이브러리로 풀 수 있는 문제 : 단순 정렬 기법, 라이브러리 숙지하면 됨
- 정렬 알고리즘의 원리에 대해서 물어보는 문제 : 선택, 삽입, 퀵 정렬 등의 원리 알아야 함
- 더 빠른 정렬이 필요한 문제 : 퀵 등의 기법으로는 풀 수 없으며, 계수 정렬 등의 다른 정렬 알고리즘 이용하거나, 구조적 개선을 요구
'Programming > 알고리즘 & 코테' 카테고리의 다른 글
[정렬] 성적이 낮은 순서로 학생 출력하기 (실전문제, 이것이 코딩테스트다 with 파이썬) (0) | 2023.12.26 |
---|---|
[정렬] 위에서 아래로 (실전문제, 이것이 코딩테스트다 with 파이썬) (1) | 2023.12.26 |
[DFS/BFS] 미로 탈출 (실전문제, 이것이 코딩테스트다 with 파이썬) (1) | 2023.12.18 |
[DFS/BFS] 음료수 얼려 먹기 (실전문제, 이것이 코딩테스트다 with 파이썬) (1) | 2023.12.18 |
[DFS/BFS] 알고리즘 개념 (0) | 2023.12.18 |