정렬은 이름, 학번, 키 등 핵심 항목(key)의 대소 관계에 따라 데이터 집합을 일정한 순서로 나열하는 작업을 말한다.
정렬 알고리즘의 안정성 동일한 키 값을 가진 요소들의 상대적인 순서가 정렬 전과 정렬 후에도 유지되는 정렬 특성을 나타낸다. 안정적인 정렬 알고리즘은 동일한 키 값을 가진 요소들 간의 순서를 보존하는 특성을 갖는다. 위 그림의 왼쪽과 같이 학번, 점수가 학번 순으로 나열되어있다. 이때 점수를 기준으로 오름차순 정렬을 하면 오른쪽 그림과 같다. 점수가 같을 때는 학번이 작은 사람을 앞쪽에 배치한다. 안정된 정렬이란 이렇게 키값이 같은 요소의 순서가 정렬 전후에도 유지되는 것을 말한다. 안정되지 않은 알고리즘은 점수가 같을 때 반드시 학번 순서대로 정렬되지는 않는다.
안정적인 정렬 알고리즘의 예로는 버블 정렬, 삽입 정렬, 병합 정렬이 있다. 반면에 선택 정렬과 퀵 정렬은 일반적으로 안정적이지 않는다.
내부 정렬과 외부 정렬 하나의 배열에서 작업할 수 있을 때에는 내부정렬을 사용하고, 하나의 배열에서 작업할 수 없을 때에는 외부 정렬을 사용한다.
-내부 정렬 : 정렬할 모든 데이터를 하나의 배열에 저장할 수 있을 때에 사용하는 알고리즘 -외부 정렬 : 정렬할 데이터가 너무 많아서 하나의 배열에 저장할 수 없을 때에 사용하는 알고리즘
셸 정렬(Shell Sort)은 단순 삽입 정렬의 장점을 살리고 단점을 보완하여 좀 더 빠르게 정렬하는 알고리즘이다.
단순 삽입 정렬의 장점과 단점 아래의 배열에서 단순 삽입 정렬을 수행한다고 가정해보자. 2, 3, 4, 5 순서로 선택하여 정렬한다. 여기까지는 이미 정렬이 되어 있는 상태이기 때문에 요소를 이동(값의 대입)하지 않는다. 그러므로 5까지는 빨리 정렬할 수 있다. 그러나 0을 삽입하려면, 아래와 같이 총 6회에 걸쳐 요소를 이동(대입)해야 한다.
장점 : 정렬이 되었거나 또는 그 상태에 가까우면 정렬 속도가 아주 빠르다. 단점 : 삽입할 곳이 멀리 떨어지면 이동(대입)하는 횟수가 많다.
이 알고리즘은 삽입 정렬의 장점을 살리면서 요소들 간의 더 멀리 떨어진 요소들도 비교하여 정렬하려는 시도에서 비롯되었다.
셸 정렬은 다음과 같은 방식으로 동작한다.
먼저 배열을 일정한 간격(셸 정렬에서는 "간격" 또는 "h"라고 함)에 따라 여러 부분 배열로 나눈다.
각 부분 배열에 대해 삽입 정렬을 수행한다. 이때 간격에 따라 떨어진 요소들끼리 삽입 정렬을 수행한다.
간격을 줄여가면서 위의 과정을 반복한다.
간격이 1이 될 때까지 반복. 이때는 일반적인 삽입 정렬이 수행된다.
간격의 선택은 여러 방식이 있으며, 셸 정렬의 성능에 영향을 미친다.
일반적으로는 간격을 반으로 나누어가는 방식이나 Hibbard 간격 등이 사용된다.
이 글에서는 간격을 반으로 나누어가는 방식을 다룬다.
위의 그림은 요솟수가 8개이기 때문에 요솟수의 절반인 4를 첫 번째 간격으로 정한 예제이다.
->이 때 4개의 그룹({8, 7}, {1, 6}, {4, 3}, {2, 5})이 생성되고 각 그룹별로 정렬한다.(4-정렬)
두 번째 간격은 이전의 간격의 절반인 2로 정한다.
->이 때 2개의 그룹({7, 3, 8, 4}, {1, 2, 6, 5})이 생성되고 각 그룹별로 정렬한다.(2-정렬)
이렇게 해서 얻은 배열은 좀 더 정렬된 상태에 가까워진다.
세 번째 간격은 이전의 간격의 절반인 1로 정한다.
마지막으로 간격이 1인 정렬을 적용하면 정렬을 마치게 된다.(1-정렬)
셸 정렬 과정에서 수행하는 각각의 정렬은 h-정렬이라고 한다.
아래의 그림은 h값을 4, 2, 1로 감소시키면서 7회로 정렬을 마치게 된다.
셸 정렬은 비교적 간단하면서도 효과적인 정렬 알고리즘이지만, 정확한 최적 간격을 찾는 것이 어려우며 여러 실험적인 연구가 진행되어왔다.
평균 시간 복잡도는 O(n log^2 n)이지만 최악의 경우에도 O(n^2)으로 다소 비효율적일 수 있다.
셸 정렬은 일반적으로 안정적이지 않다.
publicclassMain{
publicstaticvoidmain(String[] args){
int[] myArray = {64, 25, 12, 22, 11};
System.out.println("정렬 전 배열:");
printArray(myArray);
// 셸 정렬 수행
shellSort(myArray);
System.out.println("\n정렬 후 배열:");
printArray(myArray);
}
staticvoidshellSort(int[] arr){
// 간격(h) 초기화for (int gap = arr.length / 2; gap > 0; gap /= 2)
{
// 각 부분 배열에 대해 삽입 정렬 수행for (int i = gap; i < arr.length; i++)
{
int temp = arr[i];
int j;
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) arr[j] = arr[j - gap];
arr[j] = temp;
}
}
}
staticvoidprintArray(int[] arr){
for (int i : arr) System.out.print(i + " ");
System.out.println();
}
}
힙은 완전 이진 트리의 일종으로, 부모 노드의 값이 자식 노드의 값보다 항상 큰(또는 작은) 특성을 갖는 자료구조이다.
힙 정렬은 이러한 힙 자료구조의 특성을 이용하여 정렬을 수행한다.
힙 정렬의 주요 단계는 다음과 같다.
힙 구성(Build Heap): 주어진 배열을 힙으로 만든다. 이를 위해 배열의 요소들을 힙의 형태에 맞게 재배치한다. 주로 최대 힙(Max Heap)을 사용하며, 각 노드는 자식 노드보다 큰 값을 가지게 된다.
힙에서 최대값 추출 및 재배치(Extract Max): 최대 힙에서 루트 노드를 추출하여 배열의 뒷부분에 위치시킨다. 그 후 힙의 크기를 감소시키고, 루트 노드에 위치한 값이 힙 특성을 만족하도록 재배치한다. 이 과정을 반복하면서 최대값부터 차례대로 추출한다.
정렬된 배열 구성: 최대값을 추출하면서 배열의 뒷부분에 순서대로 채워나가므로, 최종적으로는 정렬된 배열이 형성된다.
아래의 그림은 힙의 요소를 배열에 저장하는 과정을 나타낸 것이다.
중요한 것은 주어진 배열(정렬할 배열)은 힙이 아닐 가능성이 크다. 우선 주어진 배열을 힙으로 만들어야 한다.
위와 같은 이진 트리가 있다고 가정해보자.
4를 루트로 하는 부분트리(a)는 힙이 아니다.
그러나 왼쪽 자식 8을 루트로 하는 부분트리(b)와 오른쪽 자식 5를 루트로 하는 부분트리(c)는 모두 힙이다.
루트 4를 알맞은 위치로 내려보내면서 부분트리(a)를 힙으로 만들 수 있다.
아래와 같은 방법을 사용하면 아래부분의 작은 부분트리부터 시작해 올라가는 방식(bottom-up)으로 전체 배열을 힙으로 만들 수 있다.
힙 정렬은 '가장 큰 값이 루트에 위치'하는 특징을 이용하는 정렬 알고리즘이다.
구체적으로 살펴보면 아래와 같은 작업을 반복한다.
힙에서 가장 큰 값인 루트를 꺼낸다.
루트 이외의 부분을 힙으로 만든다.
아래의 내용은 힙에서 가장 큰 값인 루트를 꺼내고 힙 상태를 유지하는 방법을 나타낸다.
아래의 그림은 힙 정렬 알고리즘의 흐름을 나타낸다.
힙 정렬은 평균 및 최악의 경우에도 O(n log n)의 시간 복잡도를 가진다.
이는 힙 구성 단계와 최대값 추출 단계 모두에서 log n의 시간 복잡도가 소요되기 때문이다.
힙 정렬은 추가적인 메모리를 필요로하지 않는 인플레이스(in-place) 정렬 알고리즘 중 하나다.
또한 힙 정렬은 일반적으로 불안정한 정렬 알고리즘이다.
publicclassMain{
publicstaticvoidmain(String[] args){
int[] myArray = {64, 25, 12, 22, 11};
System.out.println("정렬 전 배열:");
printArray(myArray);
// 힙 정렬 수행
heapSort(myArray);
System.out.println("\n정렬 후 배열:");
printArray(myArray);
}
staticvoidheapSort(int[] arr){
// 초기 힙 구성for (int i = arr.length / 2 - 1; i >= 0; i--) heapify(arr, arr.length, i);
// 최대값을 배열의 뒤로 이동하면서 정렬for (int i = arr.length - 1; i > 0; i--)
{
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 힙 속성 유지
heapify(arr, i, 0);
}
}
staticvoidheapify(int[] arr, int n, int i){
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) largest = left;
if (right < n && arr[right] > arr[largest]) largest = right;
if (largest != i)
{
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;
// 재귀적으로 힙 속성 유지
heapify(arr, n, largest);
}
}
staticvoidprintArray(int[] arr){
for (int i : arr) System.out.print(i + " ");
System.out.println();
}
}