셸 정렬 (Shell Sort)

셸 정렬은 정렬 방법의 원리로만 보자면 “분할 증분 정렬”(diminishing incremental sort)이라고도 불리는 방법이며, 굉장히 오래된 정렬 알고리듬 중 하나이다. 이 정렬 방법은 삽입 정렬을 근간으로 하면서 삽입 정렬의 성능을 간단한 아이디어로부터 크게 증가시키는 방법이다.

삽입 정렬은 왼쪽에 있는 원소부터, 그 원소가 이동할 수 있는 ‘가장 왼쪽 자리’에 그 값을 삽입하여 정렬해 나가는 방식이다. 이 때 ‘삽입’을 위한 공간을 만들기 위해 원래자리부터 삽입될 자리 사이의 값들이 하나씩 오른쪽으로 이동해야 한다. 따라서 삽입 정렬은 어떤 원소가 이동해야 할 거리가 크면 클수록 효율이 떨어지며 배열이 반대로 정렬되어 있는 경우가 이에 해당할 것이다.

만약 먼거리를 이동할 때 한 번에 N칸 씩 비교하고 이동한다면 전체 이동 횟수는 1/N만큼 줄어들 수 있을 것이다. 셸 정렬은 이 원리를 이용한다. 개괄적인 방식은 다음과 같다.

  • 한 번에 이동할 칸 수 N을 정한다. 여기서는 일단 임의로 4라고 하자.
  • 한 번에 이동하는 칸 수는 정렬의 간격이 된다. 즉 12번 위치의 값은 8번, 4번, 0번 위치의 값과 비교하여 이동하고, 이 때 값들은 0 > 4, 4 > 8, 8 > 12, 12 > 0 로 이동하게 된다.
  • 0, 4, 8, 12, .. 의 원소들을 대략적으로 정렬했으면, 1, 5, 9, 13, … 의 원소들도 같은 식으로 정렬한다.
  • 마찬가지의 방법으로 2와 3으로 시작하는 간격 4의 원소들도 정렬한다.

전체 길이가 배열의 길이가 L일 때, L/4인 배열 4개를 삽입정렬했다. 이 상태에서 간격을 다시 2로 줄여서 0, 1을 시작점으로 하는 부분 배열을 다시 삽입정렬한다. 이 때, 이 배열들은 앞단계에서 부분적으로 보다 더 정렬된 상태에 가깝기에 (작은 값들이 평균적으로 왼쪽으로 더 이동했으므로) 좀 더 적은 교환으로 완료할 수 있을 것이다. 그리고 더 촘촘한 간격에 의해 더 많은 작은 값들이 평균적으로 왼쪽에 와 있다고 볼 수 있다. 간격을 더 줄여서 1이 되면 일반 삽입정렬과 동일하다. 최종적으로 삽입정렬로 배열 전체를 정렬한다.

삽입 정렬을 여러번 수행하는 것이 실제 전체 배열에 대해 한 번 정렬하는 것보다 빨라진다는 것이 직관적으로는 와 닿지 않지만, 삽입정렬의 최악의 시간복잡도는 O_{(n^2)}이므로 배열을 쪼개어 각각 정렬하는 시간이 전체 배열을 정렬하는 시간보다는 무조건 이익이다. 이후 같은 정렬을 반복해야 하는 부담이 있지만, 이 이후부터는 교환이 발생할 확률이 낮아지므로 전체 성능을 (의외로 크게) 오르게 된다.

구현

일반적인 삽입 정렬과 달리 이동거리(gap) 만큼씩 움직이면서 부분적인 삽입 정렬을 할 수 있는 헬퍼 함수가 있다면 셸 정렬을 쉽게 구현할 수 있다. 하나의 gap 값에 대해서 0, 1, .. gap-1 을 시작 위치로 하여 부분 정렬을 수행하고, gap을 절반이나 1/3 등으로 줄여가면 되겠다.

먼저 헬퍼 함수로 사용될 gi_sort()를 구현하고 셸 소트에서 이 헬퍼 함수를 호출 하는 식으로 구현하는 것이 이해하기도 쉽고 편리하다. 우선 헬퍼함수는 한 번에 1씩 움직이는 것이 아닌 gap만큼씩 움직이는 삽입 정렬 코드와 거의 동일할 것이며, shell 소트는 gap을 점점 줄여나가면서 매번 range(gap)에 대해서 gi_sort()를 호출하면 된다.

from typing import TypeVar, List
T = TypeVar('T')
def gi_sort(arr: List[T], gap: int=0, start: int=0):
	''' gap_interval_sort: start 부터 시작해서 gap씩 건너뛴 값들에 대해서
	    삽입 정렬을 수행한다. '''
	for i in range(start, len(arr), gap):
		# c : 현재 위치 인덱스의 커서
		# v : 비교하는 기준값
		c, v = i, arr[i]
		while c > gap and arr[c-gap] > v:
			arr[c] = arr[c-gap]
			c -= gap
		arr[c] = v
def shell_sort(arr: List[T]):
	''' 배열의 길이 L의 절반으로 gap을 정하고
	    gap을 점차 줄여 나간다.'''
	gap = len(arr) // 2
	while gap > 0:
		# 각 gap에 대해서 1~gap 범위를 start로 주고 gi정렬
		for start in range(gap):
			gap_sort(arr, gap, start)
		# gap은 반으로 줄여나간다.
		gap //= 2

그럼, 삽입 정렬과의 성능을 비교해보면 어떨까? 셸 정렬이 제자리 정렬이다보니, 배열을 복사하는데 드는 시간도 생각해야 겠지만 동일한 배열을 정렬하는 성능의 비교는 충분히 할 수 있을 듯 하다.

%%timeit
xs = ns[:]
insert_sort(xs)
264 ms ± 3.44 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
xs = ns[:]
shell_sort(xs)
15.6 ms ± 317 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

위에서 작성한 코드는 별도의 헬퍼 함수를 사용하여 작성했다. 하지만 실제로는 for 문내에서 동일한 규칙대로 함수를 호출하므로 함수는 하나로 합쳐질 수 있다. 또한 삽입 정렬의 구현에서와 같이 슬라이스를 사용하여 값 이동의 횟수를 더 줄일 수 있을 것이다.

참고로 gap의 길이를 전체 배열의 절반 길이로부터 2로 나눠가는 것 보다는 N / 3 + 1 의 형태로 감소시켜 가는 것이 더 좋은 성능을 보인다고 알려져 있다.

Read more

워드프레스에서 고스트로 이전

워드프레스에서 고스트로 이전

이 글을 쓰면서도 믿기 힘든 사실인데, 블로그라는 걸 처음 시작한지가 20년이 되었습니다. 이글루스에서 처음 시작했다가, SK컴즈가 인수한다고 발표함과 동시에 워드프레스로 플랫폼을 옮겼죠. 워드프레스오 옮긴 이후에는 호스팅 환경을 이리 저리 옮기긴 했지만 거의 18년 가까이 워드프레스를 사용해온 것 같습니다. 그 동안 워드프레스는 블로깅 툴에서 명실상부한 범용CMS로 발전했습니다. 사실 웬만한 홈페이지들은 이제

By sooop
띄어쓰기에 대한 생각

띄어쓰기에 대한 생각

업무 메일을 쓸 때 가장 많이 쓰는 말 중에 하나가 메일 말미에 ‘업무에 참고 부탁 드립니다.‘인데요, 어느 날부터 아웃룩에서 이 ‘부탁 드립니다’가 틀렸다고 맞춤법 지적을 하기 시작했습니다. 맞는 말은 ‘부탁드립니다’라고 붙여 쓰는 거라고. 사실 아래아한글 시절부터 이전의 MS워드까지, 워드프로세서들의 한국어 맞춤법 검사 실력은 거의 있으나 마나 한

By sooop

구글 포토에서 아이클라우드로 탈출한 후기

한 때 구글 포토가 백업 용량을 무제한으로 제공해 주겠다고해서, 구글 포토를 사용해서 사진을 백업해왔습니다. 물론 이 이야기의 결말은 저나 이 글을 읽고 있는 여러분이나 모두 알고 있습니다. 사실 AI에게 학습 시킬 이미지 데이터를 모으기 위한 것일 뿐이라거나 하는 이야기는 그 당시에도 있었습니다만, 에이 그래도 구글인데 용량은 넉넉하게 주겠지…하는 순진한

By sooop

Julia의 함수 사용팁

연산자의 함수적 표기 Julia의 연산자는 기본적으로 함수이며, 함수 호출 표기와 같은 방식으로 호출하는 것이 가능합니다. 또한 그 자체로 함수이기 때문에 filter(), map() 과 같이 함수를 인자로 받는 함수에도 연산자를 그대로 적용하는 것이 가능합니다. 특히 + 연산자는 sum() 함수와 같이 여러 인자를 받아 인자들의 합을 구할 수 있습니다. 2 + 3 # = 5 +(2,

By sooop