[Go] 정렬
Updated:
개요
Go의 sort 패키지는 다양한 타입의 슬라이스를 정렬하고 검색하는 기능을 제공합니다.
주요 특징:
- 기본 타입: Ints, Float64s, Strings 정렬
- 커스텀 정렬: Interface 구현으로 모든 타입 정렬
- Slice 정렬: sort.Slice로 간편한 정렬
- 안정 정렬: Stable sort 지원
- 검색: 이진 탐색 제공
- 역순: Reverse로 내림차순 정렬
- 성능: O(n log n) 복잡도
기본 타입 정렬
1. 정수 정렬
import (
"fmt"
"sort"
)
func main() {
nums := []int{5, 2, 8, 1, 9, 3}
// 오름차순 정렬
sort.Ints(nums)
fmt.Println(nums) // [1 2 3 5 8 9]
// 정렬 확인
sorted := sort.IntsAreSorted(nums)
fmt.Println(sorted) // true
// 내림차순 정렬
sort.Sort(sort.Reverse(sort.IntSlice(nums)))
fmt.Println(nums) // [9 8 5 3 2 1]
}
2. 실수 정렬
func main() {
nums := []float64{3.14, 1.41, 2.71, 0.58}
// 오름차순
sort.Float64s(nums)
fmt.Println(nums) // [0.58 1.41 2.71 3.14]
// 정렬 확인
sorted := sort.Float64sAreSorted(nums)
fmt.Println(sorted) // true
// 내림차순
sort.Sort(sort.Reverse(sort.Float64Slice(nums)))
fmt.Println(nums) // [3.14 2.71 1.41 0.58]
}
3. 문자열 정렬
func main() {
words := []string{"banana", "apple", "cherry", "date"}
// 사전순 정렬
sort.Strings(words)
fmt.Println(words) // [apple banana cherry date]
// 정렬 확인
sorted := sort.StringsAreSorted(words)
fmt.Println(sorted) // true
// 역순
sort.Sort(sort.Reverse(sort.StringSlice(words)))
fmt.Println(words) // [date cherry banana apple]
}
4. Search - 이진 탐색
func main() {
nums := []int{1, 3, 5, 7, 9, 11, 13}
// 값 찾기
idx := sort.SearchInts(nums, 7)
fmt.Println(idx) // 3
// 없는 값 (삽입 위치 반환)
idx = sort.SearchInts(nums, 6)
fmt.Println(idx) // 3 (6이 들어갈 위치)
// 문자열 검색
words := []string{"apple", "banana", "cherry", "date"}
idx = sort.SearchStrings(words, "cherry")
fmt.Println(idx) // 2
}
Slice 정렬
1. sort.Slice - 간단한 정렬
func main() {
nums := []int{5, 2, 8, 1, 9}
// 오름차순
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
fmt.Println(nums) // [1 2 5 8 9]
// 내림차순
sort.Slice(nums, func(i, j int) bool {
return nums[i] > nums[j]
})
fmt.Println(nums) // [9 8 5 2 1]
}
2. 구조체 정렬
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
{"Dave", 25},
}
// 나이순 정렬
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
fmt.Println(people)
// [{Bob 25} {Dave 25} {Alice 30} {Charlie 35}]
// 이름순 정렬
sort.Slice(people, func(i, j int) bool {
return people[i].Name < people[j].Name
})
fmt.Println(people)
// [{Alice 30} {Bob 25} {Charlie 35} {Dave 25}]
}
3. 다중 키 정렬
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 25},
{"Dave", 30},
}
// 나이순, 같으면 이름순
sort.Slice(people, func(i, j int) bool {
if people[i].Age != people[j].Age {
return people[i].Age < people[j].Age
}
return people[i].Name < people[j].Name
})
fmt.Println(people)
// [{Bob 25} {Charlie 25} {Alice 30} {Dave 30}]
}
4. SliceStable - 안정 정렬
type Item struct {
Value int
Index int
}
func main() {
items := []Item{
{3, 1},
{1, 2},
{3, 3},
{2, 4},
{1, 5},
}
// 안정 정렬 (같은 값의 순서 유지)
sort.SliceStable(items, func(i, j int) bool {
return items[i].Value < items[j].Value
})
fmt.Println(items)
// [{1 2} {1 5} {2 4} {3 1} {3 3}]
// Value 1인 항목들의 Index 순서가 유지됨 (2, 5)
}
Interface 구현
1. sort.Interface
type Person struct {
Name string
Age int
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sort.Sort(ByAge(people))
fmt.Println(people)
// [{Bob 25} {Alice 30} {Charlie 35}]
}
2. 여러 정렬 기준
type ByName []Person
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
type ByAgeAndName []Person
func (a ByAgeAndName) Len() int { return len(a) }
func (a ByAgeAndName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAgeAndName) Less(i, j int) bool {
if a[i].Age != a[j].Age {
return a[i].Age < a[j].Age
}
return a[i].Name < a[j].Name
}
func main() {
people := []Person{
{"Charlie", 25},
{"Alice", 30},
{"Bob", 25},
}
// 이름순
sort.Sort(ByName(people))
fmt.Println(people)
// [{Alice 30} {Bob 25} {Charlie 25}]
// 나이순, 같으면 이름순
sort.Sort(ByAgeAndName(people))
fmt.Println(people)
// [{Bob 25} {Charlie 25} {Alice 30}]
}
3. Reverse
func main() {
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// 나이 역순
sort.Sort(sort.Reverse(ByAge(people)))
fmt.Println(people)
// [{Charlie 35} {Alice 30} {Bob 25}]
}
고급 기능
1. IsSorted - 정렬 확인
func main() {
nums := []int{1, 2, 3, 4, 5}
sorted := sort.SliceIsSorted(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
fmt.Println(sorted) // true
people := []Person{
{"Alice", 30},
{"Bob", 25},
}
sorted = sort.IsSorted(ByAge(people))
fmt.Println(sorted) // false (Bob이 더 작음)
}
2. Search - 일반 검색
func main() {
nums := []int{1, 3, 5, 7, 9, 11, 13}
// 조건 함수로 검색
idx := sort.Search(len(nums), func(i int) bool {
return nums[i] >= 7
})
if idx < len(nums) && nums[idx] == 7 {
fmt.Printf("Found at index %d\n", idx) // Found at index 3
}
// 첫 번째 짝수 찾기
idx = sort.Search(len(nums), func(i int) bool {
return nums[i]%2 == 0
})
fmt.Println(idx) // 7 (없으므로 len 반환)
}
3. 구조체 검색
func main() {
people := []Person{
{"Alice", 25},
{"Bob", 30},
{"Charlie", 35},
{"Dave", 40},
}
// 나이순 정렬 필요
sort.Sort(ByAge(people))
// 나이 >= 35인 첫 번째 사람 찾기
idx := sort.Search(len(people), func(i int) bool {
return people[i].Age >= 35
})
if idx < len(people) {
fmt.Println(people[idx]) // {Charlie 35}
}
}
실전 예제
1. 점수 순위 매기기
type Student struct {
Name string
Score int
Rank int
}
func AssignRanks(students []Student) {
// 점수 내림차순 정렬
sort.Slice(students, func(i, j int) bool {
return students[i].Score > students[j].Score
})
// 순위 매기기
for i := range students {
if i == 0 {
students[i].Rank = 1
} else if students[i].Score == students[i-1].Score {
// 동점자는 같은 순위
students[i].Rank = students[i-1].Rank
} else {
students[i].Rank = i + 1
}
}
}
func main() {
students := []Student{
{"Alice", 85, 0},
{"Bob", 92, 0},
{"Charlie", 85, 0},
{"Dave", 78, 0},
}
AssignRanks(students)
for _, s := range students {
fmt.Printf("%s: %d점 (순위: %d)\n", s.Name, s.Score, s.Rank)
}
// Bob: 92점 (순위: 1)
// Alice: 85점 (순위: 2)
// Charlie: 85점 (순위: 2)
// Dave: 78점 (순위: 4)
}
2. 중복 제거 (정렬 후)
func RemoveDuplicates(nums []int) []int {
if len(nums) == 0 {
return nums
}
// 정렬
sort.Ints(nums)
// 중복 제거
j := 0
for i := 1; i < len(nums); i++ {
if nums[i] != nums[j] {
j++
nums[j] = nums[i]
}
}
return nums[:j+1]
}
func main() {
nums := []int{5, 2, 8, 2, 1, 5, 9, 1}
unique := RemoveDuplicates(nums)
fmt.Println(unique) // [1 2 5 8 9]
}
3. 병합 정렬된 배열
func MergeSorted(a, b []int) []int {
result := make([]int, 0, len(a)+len(b))
i, j := 0, 0
for i < len(a) && j < len(b) {
if a[i] <= b[j] {
result = append(result, a[i])
i++
} else {
result = append(result, b[j])
j++
}
}
// 남은 요소 추가
result = append(result, a[i:]...)
result = append(result, b[j:]...)
return result
}
func main() {
a := []int{1, 3, 5, 7}
b := []int{2, 4, 6, 8, 10}
merged := MergeSorted(a, b)
fmt.Println(merged) // [1 2 3 4 5 6 7 8 10]
}
4. K번째로 큰 값 찾기
func FindKthLargest(nums []int, k int) int {
// 내림차순 정렬
sort.Slice(nums, func(i, j int) bool {
return nums[i] > nums[j]
})
return nums[k-1]
}
func main() {
nums := []int{3, 2, 1, 5, 6, 4}
fmt.Println(FindKthLargest(nums, 2)) // 5 (두 번째로 큰 값)
fmt.Println(FindKthLargest(nums, 4)) // 3 (네 번째로 큰 값)
}
5. 구간 병합
type Interval struct {
Start int
End int
}
func MergeIntervals(intervals []Interval) []Interval {
if len(intervals) == 0 {
return intervals
}
// 시작점 기준 정렬
sort.Slice(intervals, func(i, j int) bool {
return intervals[i].Start < intervals[j].Start
})
result := []Interval{intervals[0]}
for i := 1; i < len(intervals); i++ {
last := &result[len(result)-1]
current := intervals[i]
if current.Start <= last.End {
// 겹치는 경우 병합
if current.End > last.End {
last.End = current.End
}
} else {
// 안 겹치면 추가
result = append(result, current)
}
}
return result
}
func main() {
intervals := []Interval{
{1, 3},
{2, 6},
{8, 10},
{15, 18},
}
merged := MergeIntervals(intervals)
fmt.Println(merged) // [{1 6} {8 10} {15 18}]
}
6. 커스텀 비교자
import "strings"
type File struct {
Name string
Size int64
Time time.Time
}
type FileSorter struct {
files []File
by func(f1, f2 *File) bool
}
func (s FileSorter) Len() int { return len(s.files) }
func (s FileSorter) Swap(i, j int) { s.files[i], s.files[j] = s.files[j], s.files[i] }
func (s FileSorter) Less(i, j int) bool {
return s.by(&s.files[i], &s.files[j])
}
func SortFiles(files []File, by func(f1, f2 *File) bool) {
sort.Sort(FileSorter{files: files, by: by})
}
func main() {
files := []File{
{"readme.txt", 1024, time.Now()},
{"config.json", 512, time.Now().Add(-1 * time.Hour)},
{"data.csv", 2048, time.Now().Add(-2 * time.Hour)},
}
// 크기순 정렬
SortFiles(files, func(f1, f2 *File) bool {
return f1.Size < f2.Size
})
fmt.Println("크기순:")
for _, f := range files {
fmt.Printf(" %s (%d bytes)\n", f.Name, f.Size)
}
// 시간순 정렬
SortFiles(files, func(f1, f2 *File) bool {
return f1.Time.Before(f2.Time)
})
fmt.Println("\n시간순:")
for _, f := range files {
fmt.Printf(" %s (%v)\n", f.Name, f.Time.Format("15:04:05"))
}
// 이름순 (대소문자 무시)
SortFiles(files, func(f1, f2 *File) bool {
return strings.ToLower(f1.Name) < strings.ToLower(f2.Name)
})
fmt.Println("\n이름순:")
for _, f := range files {
fmt.Printf(" %s\n", f.Name)
}
}
7. 우선순위 큐
import "container/heap"
type PriorityQueue []int
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool { return pq[i] < pq[j] }
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
func (pq *PriorityQueue) Push(x interface{}) {
*pq = append(*pq, x.(int))
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
x := old[n-1]
*pq = old[0 : n-1]
return x
}
func main() {
pq := &PriorityQueue{5, 3, 7, 1, 9}
heap.Init(pq)
// 최솟값 추출
fmt.Println(heap.Pop(pq)) // 1
// 새 값 추가
heap.Push(pq, 2)
// 정렬된 순서로 출력
for pq.Len() > 0 {
fmt.Println(heap.Pop(pq)) // 2, 3, 5, 7, 9
}
}
8. 정렬 성능 비교
import (
"math/rand"
"testing"
"time"
)
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000}
for _, size := range sizes {
// 랜덤 데이터 생성
data := make([]int, size)
for i := range data {
data[i] = rand.Intn(size * 10)
}
b.Run(fmt.Sprintf("Ints-%d", size), func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
nums := make([]int, len(data))
copy(nums, data)
b.StartTimer()
sort.Ints(nums)
}
})
b.Run(fmt.Sprintf("Slice-%d", size), func(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
nums := make([]int, len(data))
copy(nums, data)
b.StartTimer()
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
}
})
}
}
func main() {
// 벤치마크 실행: go test -bench=.
fmt.Println("Run: go test -bench=.")
}
일반적인 실수
1. 정렬 후 원본 변경
// ❌ 나쁜 예 (원본이 변경됨)
func main() {
nums := []int{5, 2, 8, 1}
sort.Ints(nums)
// nums가 변경됨
fmt.Println(nums) // [1 2 5 8]
}
// ✅ 좋은 예 (복사 후 정렬)
func main() {
nums := []int{5, 2, 8, 1}
// 복사
sorted := make([]int, len(nums))
copy(sorted, nums)
sort.Ints(sorted)
fmt.Println(nums) // [5 2 8 1] (원본 유지)
fmt.Println(sorted) // [1 2 5 8]
}
2. 정렬 안 된 데이터 검색
// ❌ 나쁜 예 (정렬 안 된 데이터에 Search)
func main() {
nums := []int{5, 2, 8, 1, 9}
// 정렬 안 된 상태로 검색
idx := sort.SearchInts(nums, 5)
fmt.Println(idx) // 잘못된 결과
}
// ✅ 좋은 예 (정렬 후 검색)
func main() {
nums := []int{5, 2, 8, 1, 9}
sort.Ints(nums)
idx := sort.SearchInts(nums, 5)
if idx < len(nums) && nums[idx] == 5 {
fmt.Printf("Found at %d\n", idx)
}
}
3. Less 함수 오류
// ❌ 나쁜 예 (잘못된 비교)
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool {
// 같을 때 true 반환 (잘못됨)
return a[i].Age <= a[j].Age
}
// ✅ 좋은 예 (올바른 비교)
func (a ByAge) Less(i, j int) bool {
return a[i].Age < a[j].Age
}
4. Stable vs Unstable 혼동
// ❌ 나쁜 예 (순서 보장 필요한데 일반 정렬)
type Item struct {
Category string
Order int
}
func main() {
items := []Item{
{"A", 1},
{"B", 2},
{"A", 3},
{"B", 4},
}
// 일반 정렬 (Order 순서 보장 안 됨)
sort.Slice(items, func(i, j int) bool {
return items[i].Category < items[j].Category
})
}
// ✅ 좋은 예 (안정 정렬)
func main() {
items := []Item{
{"A", 1},
{"B", 2},
{"A", 3},
{"B", 4},
}
// 안정 정렬 (Order 순서 유지)
sort.SliceStable(items, func(i, j int) bool {
return items[i].Category < items[j].Category
})
fmt.Println(items)
// [{A 1} {A 3} {B 2} {B 4}]
// 같은 Category 내에서 Order 순서 유지
}
5. nil 슬라이스 정렬
// ❌ 나쁜 예 (nil 체크 없음)
func SortNums(nums []int) {
sort.Ints(nums) // nums가 nil이면 패닉은 안 나지만 의미 없음
}
// ✅ 좋은 예 (nil 체크)
func SortNums(nums []int) {
if nums == nil || len(nums) == 0 {
return
}
sort.Ints(nums)
}
6. 포인터 슬라이스 정렬
// ❌ 나쁜 예 (포인터 주소로 정렬됨)
func main() {
nums := []*int{
ptrInt(5),
ptrInt(2),
ptrInt(8),
}
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j] // 포인터 주소 비교!
})
}
func ptrInt(n int) *int { return &n }
// ✅ 좋은 예 (값으로 비교)
func main() {
nums := []*int{
ptrInt(5),
ptrInt(2),
ptrInt(8),
}
sort.Slice(nums, func(i, j int) bool {
return *nums[i] < *nums[j] // 값 비교
})
}
7. 동시성 문제
// ❌ 나쁜 예 (동시 정렬)
func main() {
nums := []int{5, 2, 8, 1, 9}
go sort.Ints(nums)
go sort.Ints(nums) // race condition!
time.Sleep(100 * time.Millisecond)
}
// ✅ 좋은 예 (동기화)
import "sync"
func main() {
nums := []int{5, 2, 8, 1, 9}
var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
sort.Ints(nums)
}()
wg.Wait()
}
베스트 프랙티스
1. 적절한 정렬 함수 선택
// ✅ 기본 타입은 전용 함수
func main() {
nums := []int{5, 2, 8, 1}
sort.Ints(nums) // sort.Slice보다 빠름
// 구조체는 sort.Slice
people := []Person{{"Alice", 30}, {"Bob", 25}}
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
}
2. 순서 유지 필요 시 Stable
// ✅ 순서가 중요할 때는 SliceStable
func main() {
type Task struct {
Priority int
ID int
}
tasks := []Task{
{1, 100},
{2, 200},
{1, 101},
{2, 201},
}
// Priority로 정렬하되 같은 Priority 내에서 ID 순서 유지
sort.SliceStable(tasks, func(i, j int) bool {
return tasks[i].Priority < tasks[j].Priority
})
}
3. 검색 전 정렬 확인
// ✅ 검색 전 정렬 여부 확인
func BinarySearch(nums []int, target int) int {
if !sort.IntsAreSorted(nums) {
sort.Ints(nums)
}
idx := sort.SearchInts(nums, target)
if idx < len(nums) && nums[idx] == target {
return idx
}
return -1
}
4. 커스텀 타입은 Interface 구현
// ✅ 재사용 가능한 타입 정의
type ByLength []string
func (s ByLength) Len() int { return len(s) }
func (s ByLength) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s ByLength) Less(i, j int) bool { return len(s[i]) < len(s[j]) }
func main() {
words := []string{"apple", "pie", "banana"}
sort.Sort(ByLength(words))
fmt.Println(words) // [pie apple banana]
}
5. 복잡한 정렬은 함수로 분리
// ✅ 정렬 로직을 함수로 분리
func SortPeople(people []Person, by string) {
switch by {
case "age":
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
case "name":
sort.Slice(people, func(i, j int) bool {
return people[i].Name < people[j].Name
})
case "age_name":
sort.Slice(people, func(i, j int) bool {
if people[i].Age != people[j].Age {
return people[i].Age < people[j].Age
}
return people[i].Name < people[j].Name
})
}
}
6. 성능 측정
// ✅ 큰 데이터셋은 벤치마크
func BenchmarkSorting(b *testing.B) {
data := generateData(10000)
b.Run("Ints", func(b *testing.B) {
for i := 0; i < b.N; i++ {
nums := make([]int, len(data))
copy(nums, data)
sort.Ints(nums)
}
})
b.Run("Slice", func(b *testing.B) {
for i := 0; i < b.N; i++ {
nums := make([]int, len(data))
copy(nums, data)
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
}
})
}
func generateData(n int) []int {
data := make([]int, n)
for i := range data {
data[i] = rand.Intn(n * 10)
}
return data
}
7. 테스트 작성
// ✅ 정렬 함수 테스트
func TestSort(t *testing.T) {
tests := []struct {
name string
input []int
expected []int
}{
{"empty", []int{}, []int{}},
{"single", []int{1}, []int{1}},
{"sorted", []int{1, 2, 3}, []int{1, 2, 3}},
{"reverse", []int{3, 2, 1}, []int{1, 2, 3}},
{"duplicates", []int{3, 1, 2, 1}, []int{1, 1, 2, 3}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
nums := make([]int, len(tt.input))
copy(nums, tt.input)
sort.Ints(nums)
if !reflect.DeepEqual(nums, tt.expected) {
t.Errorf("got %v, want %v", nums, tt.expected)
}
})
}
}
8. 문서화
// ✅ 정렬 기준 문서화
// SortByPriority sorts tasks by priority (ascending) and then by
// creation time (descending) for tasks with the same priority.
// This ensures high-priority tasks are processed first, and among
// tasks with the same priority, newer tasks are processed first.
func SortByPriority(tasks []Task) {
sort.Slice(tasks, func(i, j int) bool {
if tasks[i].Priority != tasks[j].Priority {
return tasks[i].Priority < tasks[j].Priority
}
return tasks[i].CreatedAt.After(tasks[j].CreatedAt)
})
}
정리
- 기본: Ints, Float64s, Strings로 기본 타입 정렬, Search로 이진 탐색
- Slice: sort.Slice로 간편한 정렬, 구조체와 다중 키 지원
- Interface: Len, Swap, Less 구현으로 커스텀 정렬
- 고급: SliceStable (안정 정렬), IsSorted (확인), Reverse (역순)
- 실전: 순위 매기기, 중복 제거, 병합, K번째 값, 구간 병합, 커스텀 비교자, 우선순위 큐, 성능 비교
- 실수: 원본 변경, 정렬 없이 검색, Less 오류, Stable 혼동, nil 체크, 포인터 정렬, 동시성
- 베스트: 적절한 함수 선택, Stable 활용, 정렬 확인, Interface 구현, 함수 분리, 성능 측정, 테스트, 문서화