정렬 알고리즘 정리
정렬 알고리즘의 종류
- 정렬 알고리즘은 버블 정렬, 선택 정렬, 삽입 정렬, 퀵 정렬, 병합 정렬, 기수 정렬 등의 알고리즘으로 구성되어 있다.
- 퀵 정렬의 경우는 평균 nlogn의 시간복잡도를 갖지만,최악의 경우에는 n2 시간 복잡도를 갖는다.
-
정렬시 피벗(pivot)이라는 기준값을 사용하여 배열을 분할하는데, 피벗 선택에 따라 성능이 크게 달라진다. 만약 항상 최솟값 또는 최댓값을 피벗으로 선택하는 경우, 퀵 정렬은 최악의 성능을 보인다. 즉, 분할된 부분 배열의 크기가 매우 작아지기 때문에 분할 정복의 장점을 잃게 된다.
-
피벗을 기준으로 배열을 분할할 때, 피벗보다 작은 값들과 큰 값들을 나누는데, 만약 배열이 이미 정렬되어 있는 경우나 거의 정렬되어 있는 경우에는 피벗의 선택에 따라 한 쪽 부분 배열의 크기가 크게 줄어들 수 있다. 이로 인해 분할이 불균형하게 이루어져 재귀적인 호출이 깊게 이루어지는 상황이 발생할 수 있다.
버블 정렬
- 두 인접한 데이터를 비교해 swap하는 방식으로 정렬하는 방법
- 비교할 루프 범위를 지정한다.
- 인접한 데이터를 비교한다.
- swap 조건에 부합하면, swap연산을 수행한다.
- 2~3번 과정을 루프 범위 동안 반복한다.
- 정렬된 영역을 제외하고 1~4번 과정을 반복한다.
private void bubbleSort(int[] input) {
for (int i = 0; i < input.length; i++) {
// i : 비교할 루프 범위, 즉 i번쨰 원소까지는 정렬됨을 보장한다.
for (int j = input.length - 1; j > i; j--) {
if (input[j-1] > input[j]) {
swap(input, j-1, j);
}
}
}
}
private void swap(int[] input, int x, int y) {
int tmp = input[x];
input[x] = input[y];
input[y] = tmp;
}
- 구현은 간단하지만 시간 복잡도 O(n2)으로 왠만한 알고리즘 문제에서는 타임아웃 에러가 발생한다.
선택 정렬
- 최솟값을 선택해, 정렬되지 않은 범위의 제일 앞 위치로 swap 한다.
- 남은 정렬 부분에서 최솟값을 찾는다.
- 남은 정렬 부분에서 가장 앞의 원소와 swap한다.
- 남은 정렬 부분의 위치를 변경한다. (한칸줄인다.)
- 1~3을 반복하여 남은 정렬부분이 사라지도록 한다.
private void selectionSort(int[] input) {
for (int i = 0; i < input.length; i++) {
int min = Integer.MAX_VALUE;
int minIndex = 0;
/**
* i번쨰 이후를 남은 정렬 부분이라고 하였을때, i~마지막원소까지 최솟값을 찾고,
* i번쨰 원소와 swap한다.
*/
for (int j = i; j < input.length; j++) {
if (input[j] < min) {
min = input[j];
minIndex = j;
}
}
swap(input, minIndex, i);
}
}
삽입 정렬
- 이미 정렬된 데이터 범위에 정렬되지 않은 데이터를 삽입시켜 정렬하는 방식
- 현재 index에 있는 데이터 값을 선택한다.
- 현재 선택한 데이터가 정렬된 데이터 범위에 삽입될 위치를 탐색한다.
- 삽입 위치부터 index까지 shift연산을 수행한다.
- index를 증가시킨다.
- 전체 데이터 크기만큼 index가 커질떄까지 반복한다.
private void insertionSort(int[] input) {
for (int i = 1; i < input.length; i++) {
int current = input[i];
for (int j = i; j - 1 >= 0; j--) {
// 작거나 같다면 shift 연산을 수행하지 않아도됨.
if (input[j-1] <= current){
break;
}
// shift연산 수행
input[j] = input[j - 1];
// 삽입위치를 찾았다면 값을 넣어준다.
if (input[j - 1] > current || j - 1 == 0) {
input[j - 1] = current;
}
}
}
}
상기 3개의 알고리즘은 모두 O(n2))의 시간복잡도를 가지고 있다. 말그대로 “빠른” 정렬 알고리즘인 퀵 정렬은 평균 시간복잡도 O(nlogn)을 가진다.
퀵 정렬
pivot(기준값)
을 중심으로 계속 데이터를 2개의 집합으로 나누면서 정렬하는 분할-정복 알고리즘이다.
퀵 정렬은 앞선 n2 알고리즘들보다 상대적으로 직관적으로 이해하기는 어렵다.
-
데이터를 분할하는 pivot을 설정한다.
-
pivot을 기준으로 다음 과정을 거쳐 데이터를 2개의 집합으로 분리한다.
-
start 포인터가 가르키는 데이터가 pivot이 가르키는 데이터보다 작으면 start를 오른쪽으로 1칸 이동한다.
-
end 포인터가 가르키는 데이터가 pivot이 가르키는 데이터보다 크면 end를 왼쪽으로 1칸 이동한다.
-
start가 가르키는 데이터가 pivot이 가르키는 데이터보다 크고, end가 가르키는 데이터가 pivot보다 작으면 start,end 가 각각 가르키는 데이터를 swap하고 start는 오른쪽, end는 왼쪽으로 1칸씩 이동한다.
-
start와 end가 만날떄까지 3~5과정을 반복한다.
-
start와 end가 만나면 만난 지점에서 가르키는 데이터와 pivot이 가르키는 데이터를 비교하여 pivot이 가르키는 데이터가 크면 만난 지점의 오른쪽에, 작으면 만난 지점의 왼쪽에 pivot이 가르키는 데이터를 삽입한다.
-
pivot을 기준으로 왼/오른쪽 집합을 나누고, 분리 집합에서 다시끔 pivot을 선정한다. 1~7번 과정을 반복한다.
public void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
public int partition(int[] arr, int low, int high) {
int pivot = arr[low]; // 피벗을 가장 왼쪽 값으로 선택
int start = low;
int end = high;
while (start < end) {
while (arr[start] <= pivot && start < high) {
start++;
}
while (arr[end] > pivot) {
end--;
}
if (start < end) {
swap(arr, start, end);
}
}
// 피벗과 end 지점의 값을 교환
swap(arr, low, end);
// 피벗이 위치한 인덱스 반환
return end;
}
Reference
[1] https://upload.wikimedia.org/wikipedia/commons/9/9c/Quicksort-example.gif