컬렉션의 고차 함수들

1. map

  • 정의: 컬렉션의 각 요소를 변환하여 새로운 컬렉션을 반환합니다.
  • 용도: 리스트의 요소들을 다른 값으로 변환할 때 사용.
1
2
3
val numbers = listOf(1, 2, 3, 4)
val squares = numbers.map { it * it }
println(squares)  // 출력: [1, 4, 9, 16]

2. filter

  • 정의: 주어진 조건에 맞는 요소들만 필터링하여 새로운 컬렉션을 반환합니다.
  • 용도: 리스트에서 특정 조건을 만족하는 요소들만 남기고 싶을 때 사용.
1
2
3
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers)  // 출력: [2, 4, 6]

3. reduce

  • 정의: 컬렉션의 모든 요소를 누적하여 단일 값을 반환합니다.
  • 용도: 리스트에서 값을 축적하거나 합산할 때 사용.
1
2
3
val numbers = listOf(1, 2, 3, 4)
val sum = numbers.reduce { acc, number -> acc + number }
println(sum)  // 출력: 10
  • acc 는 누적된 값이고, number 는 리스트의 현재 요소입니다.

4. fold

  • 정의: reduce와 유사하지만, 초기값을 지정할 수 있습니다.
  • 용도: 초기값과 함께 누적 연산을 해야 할 때 사용.
1
2
3
val numbers = listOf(1, 2, 3, 4)
val product = numbers.fold(1) { acc, number -> acc * number }
println(product)  // 출력: 24
  • 초기값으로 1을 제공했고, 이를 기반으로 리스트의 각 요소를 곱하여 누적합니다.

5. forEach

  • 정의: 컬렉션의 각 요소에 대해 주어진 동작을 수행합니다. 반환값은 없습니다.
  • 용도: 컬렉션의 요소를 순회하면서 작업을 수행할 때 사용.
1
2
3
val numbers = listOf(1, 2, 3, 4)
numbers.forEach { println(it) }
// 출력: 1, 2, 3, 4

6. flatMap

  • 정의: 각 요소를 변환하고, 변환된 결과들을 평탄화하여 하나의 리스트로 반환합니다.
  • 용도: 리스트의 리스트를 하나의 리스트로 합치고 싶을 때 사용.
1
2
3
val listOfLists = listOf(listOf(1, 2), listOf(3, 4))
val flatList = listOfLists.flatMap { it }
println(flatList)  // 출력: [1, 2, 3, 4]

7. groupBy

  • 정의: 컬렉션의 요소를 주어진 키로 그룹화하여 Map을 반환합니다.
  • 용도: 리스트의 요소를 특정 조건에 따라 그룹화할 때 사용.
1
2
3
val words = listOf("apple", "banana", "cherry", "avocado")
val groupedWords = words.groupBy { it.first() }
println(groupedWords)  // 출력: {a=[apple, avocado], b=[banana], c=[cherry]}

8. partition

  • 정의: 조건에 따라 컬렉션을 두 개의 리스트로 나눕니다. 조건을 만족하는 요소는 첫 번째 리스트에, 그렇지 않은 요소는 두 번째 리스트에 담깁니다.
  • 용도: 리스트를 조건에 맞게 둘로 나눌 때 사용.
1
2
3
4
val numbers = listOf(1, 2, 3, 4, 5, 6)
val (even, odd) = numbers.partition { it % 2 == 0 }
println(even)  // 출력: [2, 4, 6]
println(odd)   // 출력: [1, 3, 5]

9. take / takeWhile

  • take: 처음부터 주어진 숫자만큼의 요소를 가져옵니다.
  • takeWhile: 조건을 만족하는 동안만 요소를 가져옵니다.
1
2
3
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.take(3))  // 출력: [1, 2, 3]
println(numbers.takeWhile { it < 4 })  // 출력: [1, 2, 3]

10. zip

  • 정의: 두 개의 컬렉션을 병합하여 쌍(pair)의 리스트를 만듭니다.
  • 용도: 두 리스트를 쌍으로 묶고 싶을 때 사용.
1
2
3
4
val names = listOf("Alice", "Bob", "Charlie")
val ages = listOf(25, 30, 35)
val people = names.zip(ages)
println(people)  // 출력: [(Alice, 25), (Bob, 30), (Charlie, 35)]

11. any / all / none

  • any: 하나라도 조건을 만족하는 요소가 있으면 true를 반환합니다.
  • all: 모든 요소가 조건을 만족하면 true를 반환합니다.
  • none: 하나도 조건을 만족하지 않으면 true를 반환합니다.
1
2
3
4
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.any { it > 3 })  // 출력: true
println(numbers.all { it < 6 })  // 출력: true
println(numbers.none { it == 0 })  // 출력: true

12. find / firstOrNull

  • find: 조건을 만족하는 첫 번째 요소를 반환합니다.
  • firstOrNull: 조건을 만족하는 요소가 없으면 null 을 반환합니다.
1
2
3
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.find { it > 3 })  // 출력: 4
println(numbers.firstOrNull { it > 5 })  // 출력: null

13. groupingBy / eachCount

  • Python의 Counter 기능
1
2
3
val list = listOf("apple", "banana", "apple", "orange", "banana", "apple")
val counter = list.groupingBy { it }.eachCount()
println(counter)  // {apple=3, banana=2, orange=1}

14. groupBygroupingBy의 차이점

groupBygroupingBy
결과즉시 그룹화된 맵을 반환 (Map<K, List<V>>)지연 계산을 위한 Grouping 객체 (Grouping<K, T>)
동작 방식그룹화와 동시에 결과를 반환추가 처리가 필요, eachCount, fold, reduce
사용 시기그룹화된 결과가 즉시 필요한 경우그룹화 후 추가적인 처리나 지연 계산이 필요한 경우
예시list.groupBy { it }list.groupingBy { it }.eachCount()

정렬 기준이 여러 개일 때 정렬

Kotlin에서 여러 기준으로 정렬하려면 sortedWith를 사용하고, 각 기준에 맞는 Comparator를 정의할 수 있습니다.

정렬 기준을 여러 개 지정할 때는 주로 compareBythenBy를 사용해 우선순위를 설정합니다.

문제 예시:

만약 리스트가 List<Triple<Int, Int, Int>> 형식이고,

각 요소에 대해 2번 요소(세 번째 값)를 우선적으로 비교하고, 그 다음으로 0번 요소(첫 번째 값), 마지막으로 1번 요소(두 번째 값)를 기준으로 정렬한다고 가정해 보겠습니다.

코드 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fun main() {
    // 예시 데이터: Triple(0번 요소, 1번 요소, 2번 요소)
    val list = listOf(
        Triple(1, 2, 3),
        Triple(1, 3, 2),
        Triple(2, 1, 3),
        Triple(0, 0, 2)
    )

    // 2번 요소를 우선으로, 0번 요소, 그 다음 1번 요소 기준으로 정렬
    val sortedList = list.sortedWith(
        compareByDescending<Triple<Int, Int, Int>> { it.third }  // 2번 요소 내림차순
            .thenBy { it.first }  // 0번 요소 오름차순
            .thenBy { it.second } // 1번 요소 오름차순
    )

    // 결과 출력
    for (item in sortedList) {
        println(item)
    }
}

[출력 결과]
(2, 1, 3)
(1, 2, 3)
(1, 3, 2)
(0, 0, 2)
  • Kotlin에서 thenBy다중 정렬 기준을 사용할 때, 첫 번째 기준이 동일한 경우 두 번째 정렬 기준을 적용하기 위해 사용됩니다.
    • 즉, 첫 번째 기준에서 비교가 끝나지 않았을 때, 추가적인 비교 기준을 설정하는 역할을 합니다.
  • thenByDescending 도 가능

heap: PriorityQueue

PriorityQueue는 기본적으로 최소 힙(min-heap)으로 동작하며, 최대 힙(max-heap)을 구현하려면 커스텀 비교 함수를 제공해줘야 한다.

최소 힙 (Min-Heap) 사용

PriorityQueue는 기본적으로 최소 힙으로 구현되어 있으며, 값을 삽입할 때 자동으로 우선 순위에 맞게 정렬되어 가장 작은 값이 우선 처리된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.PriorityQueue

fun main() {
    // 최소 힙 (기본적으로 작을수록 우선순위가 높음)
    val minHeap = PriorityQueue<Int>()

    // 값 삽입
    minHeap.add(10)
    minHeap.add(4)
    minHeap.add(15)
    minHeap.add(1)

    // 최소값부터 출력
    while (minHeap.isNotEmpty()) {
        println(minHeap.poll())  // poll()은 가장 작은 값부터 제거하며 반환
    }
}

출력 결과
1
4
10
15

최대 힙 (Max-Heap) 사용

최대 힙은 기본적으로 지원하지 않기 때문에, 커스텀 비교자를 설정해 값을 큰 순서대로 처리하도록 할 수 있다.

Comparator를 이용해 값을 반대로 비교하면 최대 힙을 구현할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.PriorityQueue

fun main() {
    // 최대 힙 (큰 값이 우선순위가 높음)
    val maxHeap = PriorityQueue<Int>(compareByDescending { it })

    // 값 삽입
    maxHeap.add(10)
    maxHeap.add(4)
    maxHeap.add(15)
    maxHeap.add(1)

    // 최대값부터 출력
    while (maxHeap.isNotEmpty()) {
        println(maxHeap.poll())  // poll()은 가장 큰 값부터 제거하며 반환
    }
}

출력 결과
15
10
4
1

사용자 정의 객체를 힙에 사용

만약 힙에 사용자 정의 객체를 저장하고 싶다면, 해당 객체의 우선순위를 지정할 수 있도록 Comparator를 정의해야 한다.

예를 들어, 객체에 포함된 특정 속성 값을 기준으로 힙을 구성할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.PriorityQueue

data class Person(val name: String, val age: Int)

fun main() {
    // 나이를 기준으로 최소 힙 (나이가 적을수록 우선순위 높음)
    val personHeap = PriorityQueue<Person>(compareBy { it.age })

    personHeap.add(Person("Alice", 30))
    personHeap.add(Person("Bob", 25))
    personHeap.add(Person("Charlie", 35))

    // 나이가 적은 사람부터 출력
    while (personHeap.isNotEmpty()) {
        println(personHeap.poll())  // 나이가 적은 사람부터 반환
    }
}
[출력 결괴]
Person(name=Bob, age=25)
Person(name=Alice, age=30)
Person(name=Charlie, age=35)

PriorityQueue의 기능들

1. 생성자

  • PriorityQueue() : 기본 생성자로 빈 우선순위 큐를 생성한다.

  • PriorityQueue(initialCapacity: Int) : 지정한 초기 용량을 가진 우선순위 큐를 생성한다.

  • PriorityQueue(comparator: Comparator<in E>) : 커스텀 Comparator를 제공해 우선순위를 정의할 수 있다. 이를 통해 최대 힙을 구현할 수 있다.

2. 주요 함수

2.1. 삽입과 삭제

  • add(element: E): Boolean : 우선순위 큐에 요소를 추가하고, 성공하면 true를 반환한다. 큐가 가득 찬 경우 예외를 던질 수 있다.

    1
    2
    3
    4
    
    val pq = PriorityQueue<Int>()
    pq.add(10)
    pq.add(5)
    pq.add(20)
    
  • offer(element: E): Boolean : add()와 동일하게 요소를 큐에 추가하지만 예외를 던지지 않는다. 성공 여부를 Boolean으로 반환한다.

    1
    
    pq.offer(15)
    
  • poll(): E? : 우선순위가 가장 높은(가장 작은) 요소를 제거하고 반환한다. 큐가 비어있으면 null을 반환한다.

    1
    
    val minValue = pq.poll()
    
  • remove(element: E): Boolean : 특정 요소를 큐에서 제거하고, 성공하면 true를 반환한다. 큐에 해당 요소가 없으면 false를 반환한다.

    1
    
    val isRemoved = pq.remove(5)
    

2.2. 조회

  • peek(): E? : 큐에서 우선순위가 가장 높은 요소를 제거하지 않고 반환한다. 큐가 비어 있으면 null을 반환한다.

    1
    
    val topValue = pq.peek()
    
  • element(): E :peek()와 동일하게 가장 높은 우선순위의 요소를 반환하지만, 큐가 비어있으면 예외(NoSuchElementException)를 던진다.

    1
    
    val firstElement = pq.element()
    

2.3. 상태 확인

  • isEmpty(): Boolean : 큐가 비어 있는지 여부를 반환한다.

  • size(): Int

2.4. 기타 함수

  • clear() : 큐의 모든 요소를 제거한다.

  • contains(element: E): Boolean : 특정 요소가 큐에 포함되어 있는지 확인하고 truefalse를 반환한다

  • toArray(): Array<Any?> : 큐의 모든 요소를 배열로 반환한다.

    1
    
    val array = pq.toArray()
    

deque: ArrayDeque, LinkedList

Kotlin에서는 직접적으로 Deque(양방향 큐) 에 대한 클래스를 제공하지 않습니다.

하지만 Java의 Deque 인터페이스와 그 구현체인 ArrayDeque 또는 LinkedList 를 Kotlin에서 사용할 수 있습니다.

Deque양쪽에서 삽입과 삭제가 가능한 자료구조로, 스택이나 큐와 같은 동작을 모두 수행할 수 있습니다.

1. ArrayDeque 사용

Java의 ArrayDeque 은 크기 조정이 가능한 배열 기반의 Deque 구현체로, 가변 크기의 양방향 큐입니다.

기본 사용법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayDeque

fun main() {
    // ArrayDeque 생성
    val deque: ArrayDeque<Int> = ArrayDeque()

    // 양쪽에 요소 추가
    deque.addFirst(1)  // 앞쪽에 추가
    deque.addLast(2)   // 뒤쪽에 추가
    deque.addLast(3)

    println(deque)  // 출력: [1, 2, 3]

    // 양쪽에서 요소 제거
    println(deque.removeFirst())  // 출력: 1
    println(deque.removeLast())   // 출력: 3

    println(deque)  // 출력: [2]
}

2. LinkedList 사용

또 다른 Deque 구현체는 LinkedList 입니다. LinkedList노드 기반의 자료구조로, Deque와 같은 양방향 큐로 사용할 수 있습니다.

기본 사용법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.util.LinkedList

fun main() {
    // LinkedList 생성 (Deque로 사용)
    val deque: LinkedList<Int> = LinkedList()

    // 양쪽에 요소 추가
    deque.addFirst(1)  // 앞쪽에 추가
    deque.addLast(2)   // 뒤쪽에 추가
    deque.addLast(3)

    println(deque)  // 출력: [1, 2, 3]

    // 양쪽에서 요소 제거
    println(deque.removeFirst())  // 출력: 1
    println(deque.removeLast())   // 출력: 3

    println(deque)  // 출력: [2]
}

3. Deque의 사용 예시 (스택과 큐로 사용)

  • 스택으로 사용 (addFirst, removeFirst):

    1
    2
    3
    4
    
    val stack: ArrayDeque<Int> = ArrayDeque()
    stack.addFirst(1)  // push
    stack.addFirst(2)
    println(stack.removeFirst())  // pop, 출력: 2
    
  • 로 사용 (addLast, removeFirst):

    1
    2
    3
    4
    
    val queue: ArrayDeque<Int> = ArrayDeque()
    queue.addLast(1)  // enqueue
    queue.addLast(2)
    println(queue.removeFirst())  // dequeue, 출력: 1
    

결론:

  • Kotlin에서는 Deque를 직접 제공하지 않지만, Java의 ArrayDeque 또는 LinkedList 를 사용하여 Deque와 같은 기능을 구현할 수 있습니다.
  • ArrayDeque 는 배열 기반으로, 빠른 삽입 및 삭제를 지원하는 반면, LinkedList 는 노드 기반으로 동작합니다.
  • 이 두 클래스는 Deque 인터페이스를 구현하고 있으며, 이를 통해 양방향 큐를 편리하게 사용할 수 있습니다.

ArrayDeque의 기능들

1. 삽입 메서드

  • addFirst(element: E): 큐의 앞쪽에 요소를 추가합니다.

    1
    2
    3
    4
    
    val deque = ArrayDeque<Int>()
    deque.addFirst(1)
    deque.addFirst(2)
    println(deque)  // 출력: [2, 1]
    
  • addLast(element: E): 큐의 뒤쪽에 요소를 추가합니다.

    1
    2
    
    deque.addLast(3)
    println(deque)  // 출력: [2, 1, 3]
    
  • offerFirst(element: E): 큐의 앞쪽에 요소를 추가하고, 성공하면 true, 실패하면 false를 반환합니다.

  • offerLast(element: E): 큐의 뒤쪽에 요소를 추가하고, 성공하면 true, 실패하면 false를 반환합니다.

2. 삭제 메서드

  • removeFirst(): 큐의 앞쪽에서 요소를 제거하고 반환합니다. 만약 큐가 비어 있으면 NoSuchElementException 을 발생시킵니다.

    1
    2
    
    println(deque.removeFirst())  // 출력: 2
    println(deque)  // 출력: [1, 3]
    
  • removeLast(): 큐의 뒤쪽에서 요소를 제거하고 반환합니다. 큐가 비어 있으면 NoSuchElementException 을 발생시킵니다.

    1
    2
    
    println(deque.removeLast())  // 출력: 3
    println(deque)  // 출력: [1]
    
  • pollFirst(): 큐의 앞쪽에서 요소를 제거하고 반환합니다. 비어 있으면 null을 반환합니다.

  • pollLast(): 큐의 뒤쪽에서 요소를 제거하고 반환합니다. 비어 있으면 null을 반환합니다.

3. 조회 메서드

  • getFirst(): 큐의 앞쪽 요소를 반환하지만, 제거하지 않습니다. 큐가 비어 있으면 NoSuchElementException 을 발생시킵니다.

    1
    
    println(deque.getFirst())  // 출력: 1
    
  • getLast(): 큐의 뒤쪽 요소를 반환하지만, 제거하지 않습니다. 큐가 비어 있으면 NoSuchElementException 을 발생시킵니다.

    1
    2
    
    deque.addLast(5)
    println(deque.getLast())  // 출력: 5
    
  • peekFirst(): 큐의 앞쪽 요소를 반환하지만, 제거하지 않습니다. 비어 있으면 null을 반환합니다.

  • peekLast(): 큐의 뒤쪽 요소를 반환하지만, 제거하지 않습니다. 비어 있으면 null을 반환합니다.

4. 기타 메서드

  • size: Deque의 크기를 반환합니다.

    1
    
    println(deque.size)  // 출력: 2
    
  • isEmpty(): Deque가 비어 있으면 true, 비어 있지 않으면 false를 반환합니다.

    1
    
    println(deque.isEmpty())  // 출력: false
    
  • clear(): Deque의 모든 요소를 제거합니다.

    1
    2
    
    deque.clear()
    println(deque)  // 출력: []
    

5. 스택 기능으로 사용

  • push(element: E): 스택의 맨 위에 요소를 추가합니다. (addFirst와 동일한 동작)

    1
    2
    3
    4
    
    val stack = ArrayDeque<Int>()
    stack.push(10)
    stack.push(20)
    println(stack)  // 출력: [20, 10]
    
  • pop(): 스택의 맨 위에 있는 요소를 제거하고 반환합니다. (removeFirst와 동일한 동작)

    1
    2
    
    println(stack.pop())  // 출력: 20
    println(stack)  // 출력: [10]
    

6. 큐 기능으로 사용

  • offer(element: E): 큐의 뒤쪽에 요소를 추가합니다. (offerLast와 동일)

    1
    2
    3
    4
    
    val queue = ArrayDeque<Int>()
    queue.offer(100)
    queue.offer(200)
    println(queue)  // 출력: [100, 200]
    
  • poll(): 큐의 앞쪽에서 요소를 제거하고 반환합니다. (pollFirst와 동일)

    1
    2
    
    println(queue.poll())  // 출력: 100
    println(queue)  // 출력: [200]
    

ArrayDeque의 특징:

  • 스택 모두 지원: ArrayDeque는 양방향 큐로서 스택이나 로 유연하게 사용할 수 있습니다.

  • Null 허용 안 함: ArrayDequenull 요소를 허용하지 않습니다.

  • 크기 조정 가능: 배열 기반이지만 동적으로 크기가 조정됩니다.

  • 성능: ArrayDeque가변 배열로 구현되어 있으며, 삽입과 삭제가 매우 빠릅니다. LinkedList에 비해 메모리 사용량이 적고 성능이 뛰어납니다.

Python의 defaultdict을 Kotlin으로 구현하는 법

defaultdict를 Kotlin에서 구현하는 방법

getOrPut 함수는 주어진 키가 없으면 기본 값을 생성하고, 해당 값을 맵에 넣어주는 역할을 한다.

defaultdict(lambda: list)와 동일한 기능을 구현하려면 MutableMap을 사용해보자.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main() {
    // MutableMap을 사용하여 defaultdict와 같은 기능을 구현
    val map = mutableMapOf<String, MutableList<Int>>()

    // 특정 키가 없을 경우 기본값으로 빈 리스트를 생성하고, 있으면 기존 값을 가져옴
    map.getOrPut("key1") { mutableListOf() }.add(1)
    map.getOrPut("key1") { mutableListOf() }.add(2)

    map.getOrPut("key2") { mutableListOf() }.add(10)

    println(map) // 출력: {key1=[1, 2], key2=[10]}
}

코드 설명

  • mutableMapOf(): 변경 가능한 맵을 생성한다.
  • getOrPut(): 주어진 키에 해당하는 값을 반환하고, 키가 없으면 기본 값을 넣은 후 반환한다.
    • 첫 번째 매개변수는 키(key1, key2)다.
    • 두 번째 매개변수는 값이 없을 때 제공할 기본 값으로, mutableListOf()를 사용하여 빈 리스트를 생성한다.

더 간단한 함수로 감싸기

defaultdict처럼 쓰기 위해 특정 함수로 감싸는 것도 가능하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fun <K, V> defaultMap(defaultValue: () -> V): MutableMap<K, V> {
    return mutableMapOf<K, V>().withDefault { defaultValue() }
}

fun main() {
    // 기본값이 빈 리스트인 defaultdict와 유사한 맵 생성
    val map = defaultMap<String, MutableList<Int>> { mutableListOf() }

    map.getValue("key1").add(1)  // key1에 1 추가
    map.getValue("key1").add(2)  // key1에 2 추가
    map.getValue("key2").add(10) // key2에 10 추가

    println(map) // 출력: {key1=[1, 2], key2=[10]}
}

코드 설명

  • withDefault: 기본 값을 설정할 수 있는 Map의 확장 함수다. 지정된 기본 값이 없을 때 호출된다.
  • defaultMap 함수: Python의 defaultdict처럼 기본값을 설정할 수 있는 MutableMap을 반환한다.

참고

  • map[key]: 키가 없으면 null을 반환한다.

  • map.getValue(key): 키가 없으면 지정된 기본 값을 반환한다. (그러나 맵에 추가되지는 않는다.)

  • withDefault: 키가 없을 때 기본 값을 제공하지만, 맵에는 값을 추가하지 않는다.

    • map[key]로 접근하면 null이 반환된다.
  • getOrPut: 키가 없으면 기본 값을 추가하고, 값을 반환한다.