-
[이코테] Ch6. 정렬알고리즘 2022. 11. 28. 19:38728x90
Ch6. 정렬
- 정렬 : 데이터를 특정한 기준에 따라서 순서대로 나열
- 정렬 알고리즘으로 데이터를 정렬하면 이진 탐색이 가능하다. 정렬 알고리즘은 이진 탐색의 전처리 과정이기도 하다.
- 면접의 단골주제이기도 하다.
- 어느 정도 정해진 답이 있는, 즉 외워서 잘 풀어낼 수 있는 문제
1. 선택 정렬
- 가장 작은 데이터를 선택해 맨 앞의 데이터와 바꾸고, 그 다음 작은 데이터를 선택해 앞에서 두 번째 데이터와 바꾸는 과정을 반복
- ‘가장 작은 것을 선택’한다는 의미에서 선택 정렬이라고 한다.
- 시간복잡도
- 연산 횟수가 N+(N−1)+⋯+2≈N(N+1)/2 이다.
- 빅오 표기법으로 간단히 O(N^2)으로 표현 가능하다.
- 다른 정렬 알고리즘보다 비효율적이다.
- 특정한 리스트에서 가장 작은 데이터를 찾는 일이 코딩 테스트에서 많으므로 선택 정렬 소스코드 형태에 익숙해질 필요가 있다.
- 선택 정렬 소스코드
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] print(array) # 결과 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2. 삽입정렬
- 데이터를 하나씩 확인하며 각 데이터를 적절한 위치에 삽입
- 선택 정렬에 비해 구현 난이도가 높은 편이지만 선택 정렬에 비해 실행 시간이 더 효율적이다.
- 필요할 때만 위치를 바꾸기 때문에 ‘데이터가 거의 정렬되어 있을 때’ 훨씬 효율적인 알고리즘
- 정렬되어 있는 데이터 리스트에서 적절한 위치를 찾은 뒤에 그 위치에 삽입
- 특정 데이터의 왼쪽에 있는 데이터들은 이미 정렬이 된 상태이므로 자기보다 작은 데이터를 만났다면 더 이상 데이터를 살펴볼 필요 없이 그 자리에 삽입
- 시간 복잡도 : O(N^2)
- 삽입정렬 소스코드
array = [7, 5, 9, 0, 3, 1, 6, 2, 4, 8] for i in range(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) # 결과 : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3. 퀵 정렬
- 기준 데이터를 설정하고 그 기준보다 큰 데이터와 작은 데이터의 위치를 바꿈
- 가장 많이 사용되는 알고리즘
- 이코테 책에서는 다루지 않지만 ‘병합 정렬’ 알고리즘도 빠르기 때문에 많이 사용된다.
- 기준을 설정한 다음 큰 수와 작은 수를 교환한 후 리스트를 반으로 나누는 방식으로 동작
- 피벗 사용
- 피벗 : 교환하기 위한 기준
- 퀵 정렬을 수행하기 전에 피벗을 어떻게 설정할 것인지 미리 명시해야 한다. 피벗을 설정하고 리스트를 분할하는 방법에 따라서 여러 가지 방식으로 퀵 정렬을 구분한다.
- 이코테 책에서는 가장 대표적인 분할 방식인 호어 분할 방식을 기준으로 설명
- 호버 분할 방식 : 리스트에서 첫번째 데이터를 피벗으로 정한다.
- 피벗 설정한 뒤 왼쪽부터 피벗보다 큰 데이터를 찾고, 오른쪽에서부터 피벗보다 작은 데이터를 찾는다.
- 만약 찾는 값의 위치가 엇갈린 경우 작은 데이터와 피벗의 위치를 변경한다.
- 큰 데이터와 작은 데이터의 위치를 교환한다.
- 피벗의 왼쪽 리스트는 작은 데이터, 피벗의 오른쪽 리스트는 큰 데이터로 나누어지도록 수행하는 작업을 분할 혹은 파티션이라고 한다.
- 1-2번 과정 반복하면 정렬이 수행된다.
- 피벗 설정한 뒤 왼쪽부터 피벗보다 큰 데이터를 찾고, 오른쪽에서부터 피벗보다 작은 데이터를 찾는다.
- 재귀 함수 형태로 작성했을 때 구현이 매우 간결해진다.
- 종료조건은 현재 리스트의 데이터 개수가 1개인 경우이다. 리스트의 원소가 1개라면 이미 정렬되어 있다고 간주할 수 있으며 분할이 불가능하다.
- 시간 복잡도 :O(NlogN)
- 최악의 경우 시간 복잡도 :O(N2)
- 정렬 라이브러리를 제공하는 프로그래밍 언어들은 최악의 경우에도 시간 복잡도가O(NlogN)이 되는 것을 보장할 수 있도록 피벗 값을 설정할 때 추가적인 로직을 더해준다.
- 퀵 정렬 소스코드
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: # 피벗보다 큰 데이터를 찾을 때까지 반복 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]
- 파이썬의 장점을 살린 퀵 정렬 소스코드
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]
→ 피벗과 데이터를 비교하는 비교 연산 횟수가 증가하므로 시간 면에서는 비효율적이다. 하지만 더 직관적이고 기억하기 쉽다는 장점이 있다.
4. 계수 정렬
- 별도의 리스트를 선언하고 그 안에 정렬에 대한 정보(데이터의 개수)를 담는 방식 사용
- 특정한 조건이 부합할 때만 사용할 수 있지만 매우 빠른 알고리즘
- 조건 : 데이터의 크기 범위가 제한되어 정수 형태로 표현할 수 있을 때
- 모든 범위를 담을 수 있는 크기의 리스트(배열)을 선언해야 하므로 가장 큰 데이터와 가장 작은 데이터의 차이가 너무 크다면 계수 정렬은 사용할 수 없다.
- 데이터의 특성을 파악하기 어렵다면 퀵 정렬을 이용하는 것이 유리
- 데이터의 크기가 한정되어 있고, 데이터의 크기가 많이 중복되어 있을수록 유리
- 계수 정렬 소스코드
# 모든 원소의 값이 0보다 크거나 같다고 가정 array = [7, 5, 9, 0, 3, 1, 6, 2, 9, 1, 4, 8, 0, 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)O(N+K)
- N : 데이터의 개수
- K : 데이터 중 최대값의 크기
- 공간 복잡도 : O(N+K)
파이썬의 정렬 라이브러리
sorted()
- 병합 정렬을 기반으로 생성
- 퀵 정렬보다 느리지만 최악의 경우에도 시간복잡도O(NlogN) 보장
- 별도의 정렬된 리스트 반환
sort()
- 별도의 정렬된 리스트 반환하지 않고 내부 원소 정렬
sorted()
,sort()
이용 시 key 매개변수를 입력으로 받아 정렬 기준으로 사용할 수 있다.- key를 활용하여 정렬한 예시 코드
array = [('바나나', 2), ('사과', 5), ('당근', 3)] def setting(data): return data[1] result = sorted(array, key = setting) print(result) # 결과 : [('바나나', 2), ('당근', 3), ('사과', 5)]
코딩 테스트에서 정렬 알고리즘이 사용되는 경우
- 정렬 라이브러리로 풀 수 있는 문제
- 단순히 정렬 기법을 알고 있는지 물어보는 문제
- 기본 정렬 라이브러리의 사용 방법을 숙지하고 있으면 풀 수 있다.
- 정렬 알고리즘의 원리에 대해서 물어보는 문제
- 선택 정렬, 삽입 정렬, 퀵 정렬 등의 원리를 알고 있어야 문제를 풀 수 있다
- 더 빠른 정렬이 필요한 문제
- 퀵 정렬 기반의 정렬 기법으로는 풀 수 없으며 계수 정렬 등의 다른 정렬 알고리즘을 이용하거나 문제에서 기존에 알려진 알고리즘의 구조적인 개선을 거쳐야 풀 수 있다.
728x90'알고리즘' 카테고리의 다른 글
[백준] 1758번 알바생 강호 (0) 2022.12.02 [백준] 20920번 영단어 암기는 괴로워 (1) 2022.12.02 [이코테] Ch5. DFS/BFS (0) 2022.11.24 [이코테] ch4. 구현 (0) 2022.11.24 [이코테] ch3. 당장 좋은 것만 선택하는 그리디 (0) 2022.11.24