이번에는 구현 난이도는 어렵지만 정렬의 속도가 빠른 정렬 방법 중에 합병 정렬, 힙 정렬, 퀵 정렬에 대해 알아본다. 이 세가지 정렬 방법은 평균적으로 의 시간 복잡도를 가진다.
public class Main {
// 합병 정렬
public static void mergeSort(int[] arr, int[] tmp, int left, int right) {
if (left < right) { // left가 right보다 작은동안 배열이 하나가 될때 까지 반으로 나눠가며 쪼갠다.
int mid = (left + right) / 2; // 중간 인덱스 구하기
mergeSort(arr, tmp, left, mid); // 중간을 기준으로 왼쪽
mergeSort(arr, tmp, mid + 1, right); // 중간을 기준으로 오른쪽
merge(arr, tmp, left, right, mid); // 나누어진 배열 합치기
}
}
public static void merge(int[] arr, int[] tmp, int left, int right, int mid) {
int p = left; // 왼쪽 배열의 첫번째 인덱스
int q = mid + 1; // 오른쪽 배열의 첫번째 인덱스
int idx = p;
// p, q 둘 중 하나는 해당 배열 범위 안에 있는 동안
while (p <= mid || q <= right) {
if (p <= mid && q <= right) { // 둘 다 범위 안인 경우
if (arr[p] <= arr[q]) { // 값 비교하여 정렬
tmp[idx++] = arr[p++];
} else {
tmp[idx++] = arr[q++];
}
} else if (p <= mid && q > right) { // 왼쪽만 남은 경우 이어주기
tmp[idx++] = arr[p++];
} else { // 오른쪽만 남은 경우 이어주기
tmp[idx++] = arr[q++];
}
}
for (int i = left; i <= right; i++) { // 정렬된 부분 데이터 arr 쪽으로 다시 저장해주기
arr[i] = tmp[i];
}
}
}
// 힙 정렬
// 힙 정렬
public static void heapSort(int[] arr) {
// 배열을 힙으로 재구성 (마지막 부모 노드부터 maxHeap으로 구성한다)
for(int i = arr.length / 2 - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
// maxHeap으로 만들었으므로 root 쪽을 끝 쪽으로 보내고 나머지 부분을 다시 힙으로 만드는 과정을 반복한다. -> 오름차순 정렬
for(int i = arr.length - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, 0, i);
}
}
public static void heapify(int[] arr, int parentIdx, int size) {
// 자식 노드 위치
int leftIdx = 2 * parentIdx + 1;
int rightIdx = 2 * parentIdx + 2;
int maxIdx = parentIdx;
// 왼쪽 자식 노드가 크면 maxIdx 를 해당 인덱스로 교체
if(leftIdx < size && arr[maxIdx] < arr[leftIdx]) {
maxIdx = leftIdx;
}
// 오른쪽 자식 노드가 크면 maxIdx 를 해당 인덱스로 교체
if(rightIdx < size && arr[maxIdx] < arr[rightIdx]) {
maxIdx = rightIdx;
}
// maxIdx 가 바뀐 경우, 부모 노드가 교체되는 것을 의미한다.
// 교체하고 또 연쇄적으로 교체가 일어날 수 있으므로 서브 트리를 검사 하도록 재귀로 호출한다.
if(parentIdx != maxIdx) {
swap(arr, maxIdx, parentIdx);
heapify(arr, maxIdx, size);
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
기준 값인 pivot을 고르는 방법엔 여러가지가 있지만 대표적으로 3가지가 있다.
1. 배열의 맨 앞 값을 pivot으로
2. 배열의 맨 뒤 값을 pivot으로
3. 임의로 3개의 값을 고르고 그 중 중간값을 pivot으로
세번째 방법은 최대나 최소 값이 pivot이 되는 경우를 방지해 worst case( )를 피할 수 있다.
여기서 i는 처음으로 pivot보다 큰 값이고, j는 처음으로 pivot보다 작은 값이다.
// 퀵 정렬
public static void quickSort(int[] arr, int left, int right) {
if(left >= right) { // 만약 left가 right보다 크거나 같게 되면 멈춘다.
return;
}
// 분할
int pivot = partition(arr, left, right); // 피봇 값을 구한다.
// 기준값 중심으로 좌우 재귀적으로 호출한다.
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot + 1, right);
}
public static int partition(int[] arr, int left, int right) {
// 가장 좌측 값(배열의 가장 앞쪽 값)을 기준값으로 설정하는 경우
int pivot = arr[left];
int i = left;
int j = right;
while(i < j) {
while (arr[j] > pivot && i < j) { // 처음으로 피봇 보다 작은 값을 찾아야 하므로 피봇값보다 크고 i < j인 동안은 j를 왼쪽으로 이동시킨다.
j--;
}
while(arr[i] <= pivot && i < j) { // 처음으로 피봇보다 큰 값을 찾아야 하므로 피봇 값보다 작고 i < j인 동안은 i를 오른쪽으로 이동 시킨다.
i++;
}
swap(arr, i, j); // i와 j의 값을 swap한다.
}
swap(arr, left, i); // i < j일 때까지 i,j의 값을 swap하고 나서 i=j일때 i와 left인덱스의 값인 pivot값을 swap한다.
return i; // 피봇 값의 인덱스를 반환한다.
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}