def selection_sort(seq: list)-> None:
"""선택 정렬"""
n = len(seq)
for i in range(n-1):
# i: 정렬 안 된 부분에서 맨 앞 원소 인덱스
# min_idx: # 정렬 안 된 부분에서 가장 작은 원소 인덱스
min_idx = i # 초기화
for j in range(i+1, n):
# j: 최소값을 찾기 위해서 탐색 중인 인덱스
if seq[j] < seq[min_idx]:
min_idx = j # min_idx 업데이트
# i와 min_idx의 원소를 교환
seq[i], seq[min_idx] = seq[min_idx], seq[i]
arr = [6,4,8,3,1,9,7]
selection_sort(arr)
print(arr)
최선의 경우: 이미 정렬되어있는 배열을 선택 정렬 시, 정렬 안 된 부분에서 최솟값 찾기 + 무조건 교환
O(N+(N-1)+(N-2)+...+1) + O(1+1+...+1) = O((1+N)N/2) + O(N) = O()
평균 경우: 무작위로 섞여있는 배열을 선택 정렬 시, 이미 정렬되어있는 배열을 선택 정렬 시, 정렬 안 된 부분에서 최솟값 찾기 무조건 교환
O(N+(N-1)+(N-2)+...+1) + O(1+1+...+1) = O((1+N)N/2) + O(N) = O()
최악의 경우: 역순으로 정렬되어있는 배열을 선택 정렬 시, 이미 정렬되어있는 배열을 선택 정렬 시, 정렬 안 된 부분에서 최솟값 찾기 무조건 교환
O(N+(N-1)+(N-2)+...+1) + O(1+1+...+1) = O((1+N)N/2) + O(N) = O()
아주 유용함
삽입 정렬(insertion sort)
선택정렬과의 차이점
삽입 정렬 과정
1) 아직 정렬되지 않은 부분의 맨 앞 원소를 정렬된 부분의 알맞은 위치에 삽입
def insertion_sort(seq: list)-> None:
"""삽입 정렬"""
n = len(seq)
for i in range(1,n): # 0일때는 고려할 필요 없음
# (첫 원소는 부분적으로는 이미 정렬되어있다 정의하기 때문)
# i: 정렬 안 된 부분에서 맨 앞 원소의 인덱스
for j in range(i, 0, -1):
# j: 삽입할 데이터의 현재 인덱스
if seq[j-1] > seq[j]:
seq[k-1], seq[j] = seq[j], seq[j-1]
else:
break
최선의 경우: 이미 정렬되어 있는 배열을 삽입 정렬할 때, 한 번씩만 비교하면 되므로 비교(O()) + 삽입 (O()) = O()이 된다.
평균 경우: 무작위로 섞여있는 배열을 삽입 정렬할 때,
최악 경우: 역순으로 정렬된 배열을 삽입 정렬할 때,
분할 시간복잡도가 O()인 이유
각 분할 단계에서 배열을 반으로 나누므로 분할 단계의 수는 로그에 비례한다. 이진 분할이기 때문에, 배열의 크기가 일 때 분할 단계의 수는 이 된다.
예를 들어, 배열의 크기가 8일 때 분할 단계는 3번입니다. (8 -> 4 -> 2 -> 1) 따라서 크기가 인 배열을 분할하려면 단계가 필요하며, 각 단계에서는 배열을 반으로 나누기 때문에 이 단계들의 합이 O()이 된다.
예를 들어, 배열의 크기가 8일 때 분할 단계는 3번입니다. (8 -> 4 -> 2 -> 1) 따라서 크기가 N인 배열을 분할하려면 log₂(N) 단계가 필요하며, 각 단계에서는 배열을 반으로 나누기 때문에 이 단계들의 합이 O(log N)이 됩니다.
left
: 정렬된 왼쪽 리스트right
: 정렬된 오른쪽 리스트result
: 합병된 결과def merge(left: list, right: list)-> list:
"""합병"""
result = [None] * (len(left) + len(right)) # 최종 결과 초기화
i = 0 # 왼쪽 리스트 인덱스 초기화
j = 0 # 오른쪽 리스트 인덱스 초기화
for k in range(len(result)): # 최종 결과에 값 채우기
if i < len(left) and j < len(right):
# 오/왼 리스트 모두 정렬할 원소가 남은 경우
if left[i] <= right[j]: # left[i]가 right[j]보다 작거나 같은 경우(안정 정렬)
result[k] = left[i] # result 리스트에 더 작은 쪽 채우기
i += 1 # 인덱스 하나 오른쪽으로 이동
else: # right[j]가 left[i]보다 작으면
result[k] = right[j] # result 리스트에 더 작은 쪽 채우기
j += 1 # 오른쪽리스트 인덱스 하나 오른쪽으로 이동
elif i >= len(left): # 왼쪽리스트 원소 모두 정렬(선택)됐다면
result[k] = right[j] # 무조건 오른쪽 리스트 원소 넣기
j += 1 # 오른쪽리스트 인덱스 하나 오른쪽으로 이동
elif j >= len(right): # 오른쪽리스트 원소 모두 정렬(선택)됐다면
result[k] = left[j] # 무조건 왼쪽 리스트 원소 넣기
i += 1 # 왼쪽리스트 인덱스 하나 오른쪽 이동
return result # 정렬된 리스트(오/왼 합쳐진) 리턴
print(merge([1,4,6],[2,5,7]))
배열의 원소 수가 1개 이하인 경우
1. 이미 정렬되어있음
배열의 원소 수가 2개 이상인 경우
1. 배열의 앞부분을 합병정렬로 정렬
2 .배열의 뒷부분을 합병정렬로 정렬
3. 배열의 앞부분과 뒷부분을 합병 후 복사
def merge_sort(seq: list)-> None:
"""합병 정렬"""
if len(seq) <= 1: # 원소가 하나 이하이면
return # 이미 정렬되어있다고 생각함, 리턴값 없고 동작만 수행
## 분할
mid = len(seq) // 2 몫(예: 총 길이가 9이면 4:5로 나뉨)
left = seq[:mid] # 0부터 mid-1까지
right = seq[mid:] # mid부터 끝까지
## 정복(재귀적 합병정렬)
merge_sort(left) # left를 합병정렬
merge_sort(right) # right를 합병정렬
## 합병
merged = merge(left, right) # left, right를 합병하여 result 리턴
for i in range(len(seq)):
seq[i] = merged[i] # 리턴 대신 seq자체 값을 정렬된 값으로 바꿈
# (얕은 복사기 때문에 원소 단위로 접근해서 변경 가능
# 단, 통으로는 불가능 seq = merged와 같이 한번에 못 바꿈)
arr = [1,5,2,6,7,2,4,8,9,4,5,3,1]
merge_sort(arr)
print(arr)
퀵 정렬 과정
1. 하나의 원소를 골라서 피벗이라고 한다.
2. 피벗을 기준으로 전체 데이터를 분할(partition)한다.
- 왼쪽에는 피벗보다 작거나 같은 원소가 오고,
- 오른쪽에는 피벗보다 큰 값이 오도록 한다.
3. 분할된 작은 리스트에 대해 재귀적으로 이 과정을 반복한다.
def quick_sort(seq: list)-> None:
"""퀵 정렬"""
def partition(start: int, end = int)-> int:
"""피벗의 인덱스 리턴"""
i = start + 1 # 피벗보다 작은 수를 지나치고, 큰 수에선 멈추는 애, 피벗 하나 뒤에서 시작한다.
j = end # 피벗보다 큰 수는 지나치고, 작은 수에선 멈추는 애. 배열 가장 끝에서 시작한다.
pivot = start # 여기서는 피벗으로 가장 왼쪽에 있는 노드를 선택했다.
while True: # 무한 루프
while i <= end and seq[i] <= seq[pivot]:
# i가 위치한 노드의 값이 피벗의 값보다 작거나 같으면
i += 1 # i가 한 칸 이동한다.
if i > j: # i와 j가 교차하면
seq[pivot], seq[j] = seq[j], seq[pivot] # j, 피벗 교환
break
seq[i], seq[j] = seq[j], seq[i] # i, j 교환
return j # 피벗 인덱스 리턴
def sort(start: int, end: int)-> None: # 실질적 퀵 정렬 구현
if start < end: # 정렬할 데이터가 남아있을 때
pivot = partition(start, end) # pivot 정하기
sort(start, pivot - 1) # 정렬할 데이터가 하나 줄어 빠른 검색 가능
sort(pivot + 1, end) # 재귀 호출
sort(0, len(seq) - 1) # 정렬 수행
시간복잡도
장점
단점
성능 향상 방법
[출처: 황용득 교수님]