Shell Sort (쉘 정렬)
01. 요약
- 삽입 정렬을 보완한 알고리즘이다.
- Donald L. Shell이라는 사람이 제안한 방법으로, 삽입 정렬이 어느정도 정렬된 배열에 대해서는 대단히 빠른 것에 착안한 방법이다.
- 삽입 정렬의 최대 문제점은 요소들이 삽입될 때, 이웃한 위치로만 이동이 가능하다는 것이다.
- 만약 삽입되어야 할 위치가 현재 위치에서 상당히 멀리 떨어진 곳이라면 많은 이동을 해야 만이 제자리로 갈 수 있다.
- 쉘 정렬에서는 요소들이 멀리 떨어진 위치로도 이동할 수 있다.
- 삽입 정렬과는 다르게 쉘 정렬은 전체 리스트를 한 번에 정렬하지 않는다.
02. 원리 (작동 방식)
- 동작 과정
- 먼저 정렬해야 할 리스트를 일정한 기준에 따라서 분류하여 연속적이지 않은 여러 개의 부분 리스트를 만든다.
- 각 부분 리스트를 삽입 정렬을 이용하여 정렬한다.
- 모든 부분 리스트가 정렬되면 쉘 정렬은 다시 전체 리스트를 더 작은 개수의 부분 리스트로 만든 후에 알고리즘을 되풀이한다.
- 위의 과정을 부분 리스트의 개수가 1이 될 때까지 되풀이 한다.
- 구현 과정
- ‘간격(gap)’을 나타내는 k값을 정하고, 정렬해야 할 리스트의 각 k번째 요소를 추출해서 부분리스트를 만든다.
- 간격의 초깃값 : (정렬할 값의 개수)/2
- 간격의 값 k는 홀수인 게 좋다고 분석되었다. 짝수일 경우 1을 더한다.
- 생성된 부분 리스트의 개수는 gap과 같다.
- 각 부분 리스트를 삽입 정렬을 이용해 정렬한다.
- 각 스텝마다 간격 k를 줄여가므로, 수행과정이 반복될 때마다 하나의 부분리스트에 속하는 레크드들의 개수는 증가한다.
- 간격 k의 값이 1일 될때까지 반복한다.
- ‘간격(gap)’을 나타내는 k값을 정하고, 정렬해야 할 리스트의 각 k번째 요소를 추출해서 부분리스트를 만든다.
- 이해 돕기
- 처음 간격 k = 5 (10/2), 다섯 번째 요소를 추출해 부분리스트를 만든다.
- 첫 번째 부분 리스트 : [10, 3, 16]
- 두 번째 부분 리스트 : [8, 22]
- … (이런 식으로 진행)
- 각 부분 리스트에 대하여 삽입 정렬
- 다음 간격 k = 3(5/2 + 1)로 한다. 짝수일 경우 +1을 해서 홀수로
- 이런 방식으로 진행한다.
- 처음 간격 k = 5 (10/2), 다섯 번째 요소를 추출해 부분리스트를 만든다.
03. 순서도 (알고리즘)
// 기존의 삽입 정렬 알고리즘과 거의 동일하나, gap 단위로 삽입 정렬하는 알고리즘
const ShellInsertionSort = (array, first, gap) => {
let i;
let j;
let key;
let last = array.length - 1;
// 삽입 정렬은 원래 n + 1부터 시작
for (i = first + gap; i <= last; i += gap) {
key = array[i];
// 삽입할 위치를 찾기 위해서 선택한 key의 앞에서부터 비교해가면서 한칸씩 오른쪽으로 민다.
for (j = i - gap; j >= first && array[j] > key; j -= gap) {
array[j + gap] = array[j];
}
// 삽입할 위치를 찾았으니 삽입한다.
array[j + gap] = key;
}
};
const ShellSort = (array) => {
let i;
let gap;
let length = array.length;
// gap의 초기값은 보통 반의 크기로 한다.
for (gap = parseInt(length / 2); gap > 0; gap = parseInt(gap / 2)) {
// gap은 짝수보다 홀수가 더 좋은 것으로 분석되었다. 짝수일 경우 홀수로 만들어준다.
if (gap % 2 === 0) gap++;
// 각 부분리스트에 대해서 삽입 정렬을 수행한다
// index를 이용해 처리하기 때문에 추가적인 배열 공간은 필요 없다.
for (i = 0; i < gap; i++) {
ShellInsertionSort(array, i, gap); // 6.
}
}
};
const array = [3, 2, 6, 1, 5, 4];
ShellSort(array);
console.log(array);
04. 특징
- 장점 (삽입 정렬에 비하여)
- 연속적이지 않은 부분 리스트에서 자료의 교환이 일어나면 더 큰 거리를 이동한다. 반면 삽입 정렬에서는 한 번에 한 칸씩만 이동된다. 따라서 교환되는 아이템들이 삽입 정렬보다는 최종 위치에 더 가까이 있을 가능성이 높아진다.
- 부분 리스트는 어느 정도 정렬이 된 상태이기 때문에 부분 리스트의 개수가 1이 되게 되면, 쉘 정렬은 기본적으로 삽입 정렬을 수행하는 것이지만 빠르게 수행된다. (이는 삽입정렬이 거의 정렬된 리스트에 대해서는 빠르게 수행되기 때문이다.)
- 단점
- 생략
05. 복잡도
- 시간복잡도
- 증명은 되어있지 않고, 실험적인 연구를 통한 자료이다.
- 최악의 경우 O(n^2)
- 평균의 경우 O(n^1.5)
- 최선의 경우 O(n)
- 쉘 정렬은 나누는 간격의 값에 따라 시간에 크게 영향을 받는다고 한다. 이 나누는 간격이 최악의 경우 O(n^2)의 시간 복잡도를 갖는다. 그렇다면 이게 삽입정렬 보다 더 빠른게 맞을까?
- 쉘 정렬 자체는 그리 빠르지는 않다. 다만, 삽입 정렬보다 평균적인 경우 보다 더 빠르다는 것이다.
- 삽입 정렬은 최선의 경우 O(n), 평균의 경우 O(n^2), 최악의 경우 O(n^2)이다.
- 쉘 정렬은 평균의 경우 O(n^1.5)로 대체적으로 빠르다고 보면 될 것 같다.
06. 참고
- C언어로 쉽게 풀어쓴 자료구조 (천인국, 공용해, 하상호 지음)
- https://gmlwjd9405.github.io/2018/05/08/algorithm-shell-sort.html