Ruby에서 배열을 정렬하는 것은 아주 간단합니다!
원하는 결과를 얻기 위해 복잡한 알고리즘을 직접 작성할 필요가 전혀 없습니다.
그렇다면 무엇이 필요할까요?
바로 Ruby의 내장 정렬 메서드를 이해하는 것입니다.
대표적인 메서드는 다음과 같습니다:
- sort
- sort_by
- sort!
이 메서드들은 어떻게 동작하며, 왜 서로 다를까요?
이 글에서 그 답을 확인할 수 있습니다.
sort 메서드부터 시작해서, 여러 값을 기준으로 정렬할 수 있는 고급 기법인 sort_by까지, 배열을 정렬하는 다양한 방법을 배워보겠습니다.
그럼 시작해볼까요!
Ruby의 sort와 sort! 메서드 사용법
가장 기본적인 정렬 방식은 Enumerable 모듈에 정의된 Ruby의 sort 메서드가 제공합니다.
예제를 살펴보겠습니다:
numbers = [5,3,2,1] numbers.sort # [1,2,3,5]
sort는 결과물로 새로운 배열을 반환한다는 점에 주목하세요.
정렬된 요소들로 이루어진 새로운 배열이 만들어집니다!
또한 sort! 메서드를 사용하면 "제자리(in-place)" 정렬도 가능합니다.
이 경우 새 배열을 생성하는 대신 원본 배열이 직접 변경되므로, 성능 면에서 유리할 수 있습니다.
sort_by로 구현하는 커스텀 정렬
sort_by 메서드를 사용하면 더 고급스럽고 흥미로운 정렬이 가능합니다.
다음과 같은 작업을 할 수 있습니다:
- 문자열 길이 기준 정렬
- 문자열 내용 기준 정렬
- 숫자의 홀수/짝수 여부 기준 정렬
이 모든 것은 sort_by 메서드와 Ruby 블록을 조합하여 구현할 수 있습니다.
예를 들어:
strings = %w(foo test blog a) strings.sort_by(&:length) # ["a", "foo", "test", "blog"]
어떻게 동작하는 걸까요?
sort_by 메서드는 숫자 값을 기준으로 삼기 때문에, 문자열의 length(길이)가 정렬 기준으로 잘 동작합니다.
이 원리만 이해하면 훨씬 재미있는 활용도 가능합니다. 예를 들어 대문자로 시작하는 단어들을 앞쪽으로 몰아 정렬하고 나머지는 그대로 두는 식으로 말이죠.
다음과 같이요:
def sort_by_capital_word(text)
text
.split
.sort_by { |w| w[0].match?(/[A-Z]/) ? 0 : 1 }
.join(" ")
end
sort_by_capital_word("calendar Cat tap Lamp")
# "Cat Lamp calendar tap"
또한 일반 sort 메서드에 블록을 넘겨서 커스텀 정렬을 구현할 수도 있습니다.
예제입니다:
strings = %w(foo test blog a)
strings.sort { |a,b| a.length <=> b.length }
# ["a", "foo", "test", "blog"]
참고: 이
<=>기호는 "우주선 연산자(spaceship operator)"라고 불리며, 여러분의 클래스에 직접 구현할 수 있는 메서드입니다. 비교 결과에 따라 1(크다), 0(같다), -1(작다)을 반환해야 합니다.
일반적으로 저는 의도가 명확하고 가독성이 좋으며 속도도 약간 더 빠른 sort_by 메서드를 선호합니다.
역순으로 정렬하기
내림차순 정렬은 어떻게 할 수 있을까요?
정렬 후 reverse 메서드를 호출해도 되고, 블록 안에서 정렬 기준 앞에 마이너스(-) 기호를 붙여도 됩니다.
예제를 보여드릴게요:
strings = %w(foo test blog a)
strings.sort_by { |str| -str.length }
# ["blog", "test", "foo", "a"]
화려하지는 않지만 확실하게 동작합니다! 🙂
알파벳+숫자 혼합 정렬 (Alphanumeric Sorting)
숫자가 포함된 문자열 목록을 숫자 크기 기준으로 정렬하고 싶다고 가정해봅시다.
예를 들면:
music = %w(21.mp3 10.mp3 5.mp3 40.mp3)
기본 정렬을 사용하면 원하는 결과가 나오지 않습니다.
예제:
music.sort # ["10.mp3", "21.mp3", "40.mp3", "5.mp3"]
하지만 sort_by를 활용하면 이 문제를 해결할 수 있습니다:
music.sort_by { |s| s.scan(/\d+/).first.to_i }
# ["5.mp3", "10.mp3", "21.mp3", "40.mp3"]
여기서는 정규표현식(\d+)으로 숫자를 찾아낸 뒤, 첫 번째 숫자(first)를 가져와 정수 객체로 변환(to_i)했습니다.
Ruby에서 해시(Hash) 정렬하기
정렬 대상이 배열에 국한되지 않습니다. 해시도 정렬할 수 있습니다.
예제:
hash = {coconut: 200, orange: 50, bacon: 100}
hash.sort_by(&:last)
# [[:orange, 50], [:bacon, 100], [:coconut, 200]]
값(value)을 기준으로 정렬되는데, 여기서 흥미로운 점 하나를 짚고 넘어가겠습니다. 반환 결과가 해시가 아니라는 점입니다.
해시를 정렬하면 다차원 배열(multi-dimensional array)이 반환됩니다.
이를 다시 해시로 변환하려면 Array#to_h 메서드를 사용하면 됩니다.
여러 값 기준으로 정렬하기
여러 속성을 기준으로 정렬해야 하는 경우도 있습니다. 예를 들어 먼저 날짜순으로 정렬하는데, 같은 날짜를 가진 항목이 여러 개라면 순위가 겹치게 됩니다.
이런 동점 상황을 해소하려면 보조 속성을 활용하면 됩니다.
예제:
Event = Struct.new(:name, :date)
events = []
events << Event.new("book sale", Time.now)
events << Event.new("course sale", Time.now)
events << Event.new("new subscriber", Time.now)
events << Event.new("course sale", Time.now + 1.day)
events.sort_by { |event| [event.date, event.name] }
핵심은 sort_by 블록 안에 있는 배열입니다.
배열의 첫 번째 요소(event.date)에는 주 정렬 기준을, 두 번째 요소(event.name)에는 동점 처리용 보조 기준을 지정합니다.
퀵정렬(QuickSort) 직접 구현하기
재미삼아 우리만의 정렬 메서드를 직접 구현해보겠습니다. 내장 정렬 메서드보다는 느리겠지만, 컴퓨터 과학에 관심이 있다면 충분히 흥미로운 연습 과제입니다.
def quick_sort(list)
return [] if list.empty?
groups = list.group_by { |n| n <=> list.first }
less_than = groups[-1] || []
first = groups[0] || []
greater_than = groups[1] || []
quick_sort(less_than) + first + quick_sort(greater_than)
end
p quick_sort [3, 7, 2, 1, 8, 12]
# [1, 2, 3, 7, 8, 12]
퀵정렬의 아이디어는 임의의 숫자 하나를 선택한 뒤, 정렬 대상 리스트를 두 그룹으로 나누는 것입니다.
한 그룹은 선택한 숫자보다 작은 값들이고, 다른 그룹은 더 큰 값들입니다.
그리고 리스트가 정렬될 때까지 이 과정을 반복하면 됩니다.
벤치마크 성능 비교
이제 각 정렬 메서드의 성능을 서로 비교해보겠습니다.
Ruby 2.4.0 기준:
sort!: 1405.8 i/s sort: 1377.6 i/s - same-ish: difference falls within error sort_by reverse: 196.6 i/s - 7.15x slower sort_by: 183.7 i/s - 7.65x slower sort_by minus: 172.3 i/s - 8.16x slower sort with block: 164.1 i/s - 8.57x slower
결과에서 볼 수 있듯이, 일반 sort 메서드가 sort_by보다 훨씬 빠릅니다. 다만 블록을 함께 사용하지 않으면 유연성이 떨어진다는 단점이 있습니다.
영상 강의
마무리 정리
이번 글에서는 sort와 sort_by 메서드를 활용해 배열과 해시를 다양한 방식으로 정렬하는 방법을 배웠습니다. 또한 성능 차이와 퀵정렬 알고리즘 구현 방법도 함께 알아보았습니다.
꼭 기억해야 할 핵심 내용:
- 배열, 해시 또는 다른 Enumerable 객체에
sort메서드를 사용하면 기본 정렬 동작(<=>연산자 기반)이 적용됩니다. sort에 블록과 두 개의 블록 인자를 넘기면 객체 간 비교 기준을 직접 정의할 수 있습니다(블록은 1, 0, -1 중 하나를 반환해야 함).sort_by에 블록과 한 개의 인자를 넘기면 각 객체마다 정렬 기준이 될 하나의 속성(배열 길이, 객체 속성, 인덱스 등)을 지정할 수 있습니다. 블록은 정렬된 배열에서 해당 객체의 위치를 결정하는 정수 값을 반환해야 합니다.
더 많은 사람들이 배울 수 있도록 이 글을 공유하는 것도 잊지 마세요. 🙂