1편에 이어서, 사용할 수 있는 자료구조와 Collections 확장 함수에 대해서 소개한다.
- 1편에 이어 새로 업데이트 하였다. (2023.08.07 1.0.2v)
리스트 : List
리스트는 여러개의 값을 불연속적인 공간에 저장하는 동적인 자료구조이다. 포인터로 불연속적인 메모리에 접근하기 때문에 메모리 관리에 용이하다.
코틀린은 불변 리스트와, 가변 리스트를 구분하고 있다.
가변 리스트는 요소를 추가하거나 삭제가 가능한 리스트를 의미한다. listOf() 메소드를 통해 생성하는 리스트는 불변 리스트이며, 가변 리스트는 mutable 키워드를 앞에 붙여서 생성할 수 있다.
fun main(){
val immutableList: List<Int> = List(size = 3){ init -> init } // 불변 리스트 생성
println(immutableList) // > [1, 2, 3]
val immutableListOf: List<Int> = listOf(1, 2, 3) // listOf() 메소드를 통해 리스트 생성
println(immutableListOf) // > [1, 2, 3]
val mutableList: MutableList<Int> = MutableList(size = 3){ init -> init } // 가변 리스트 생성
println(mutableList) // > [1, 2, 3]
val mutableListOf: MutableList<Int> = mutableListOf(1, 2, 3)
println(mutableListOf) // > [1, 2, 3]
}
이러한 List는 아래와 같은 다양한 유틸리티 함수를 제공한다.
fun main() {
val iList = listOf("spotlight", "파노라마", "정자이노", "헬로") // 불변 리스트 Immutable
println(iList.size) // 리스트 사이즈
println(iList.indexOf("정자이노")) // 정자이노 index
println(iList.get(2)) // 정자이노 3번째 값
val mList = mutableListOf("hello", "Qwe", "bang", "아발론") // 가변 리스트 Mutable
mList.add("찰리푸스") // '찰리푸스' 값 추가
mList.remove("아발론") // '아발론' 값 제거
mList.removeAt(0) // 1번 인덱스 제거
mList.addAll(iList) // 'iList' 리스트 추가
}
Set : 중복된 요소가 존재하지 않는 자료구조
Set은 중복을 허용하지 않는 자료구조이다. 여러개의 값이 순서가 정해져 있지 않은 채로 저장된 자료구조이다.
다른 Collection과 마찬가지로 Immutable과 Mutable로 구분되어진다.
fun main(){
val iSet = setOf(3, 2, 1, 4, 5, 2, 1, 3) // Immutable Set
println(iSet) // [3, 2, 1, 4, 5]
println(iSet.contains(1)) // true
val mSet = mutableSetOf(3, 1, 5, 4, 2, 1) // Mutable Set
mSet.add(0) // 0 더하기
mSet.remove(3) // 3 제거
mSet.removeIf{ it < 3 } // 3 보다 작은 수 모두 제거
println(mSet) // [5, 4]
}
Set의 자료구조
Set의 자료구조는 HashSet, SortedSet, LinkedSet으로 나누어진다.
- HashSet의 경우
- 자바의 HashSet 컬렉션을 기반으로 순서와 중복된 요소를 무시하며, 정렬을 지원하지 않는다.
- HashSet은 가변성이며, 해시값을 통해 값을 찾아내어 검색속도가 빠르다는 장점이 있다.
- SortedSet의 경우는
- 자바의 TreeSet을 기반으로 오름차순으로 정렬된 상태로 반환한다.
- 가변성을 가지며, 정렬에 있어서 강점이 있는 자료구조이다.
- LinkedSet의 경우는
- 각 노드는 다음 연결 노드 포인터를 가지고 있어, 비어있는 공간을 참조하는 방법으로 효율적으로 메모리를 관리할 수 있다.
- 자료의 추가와 삭제에 있어 강점이 있는 자료구조이다.
fun main(){
val hashSet : HashSet<Int> = hashSetOf(3, 5, 8, 1)
val sortedSet : TreeSet<Int> = sortedSetOf(1, 9, 3, 2)
val linkedSet : LinkedHashSet<Int> = linkedSetOf(13, 12, 4, 6)
println(hashSet) // [8, 1, 3, 5]
println(sortedSet) // [1, 2, 3, 9]
println(linkedSet) // [13, 12, 4, 6]
}
Map : 값이 키와 묶여져 있는 자료구조
Map은 각 요소가 Key와 Value로 구성되며, Key는 중복될 수 없다.
기존에 저장된 키와 동일한 키로 값을 저장하면 기존의 값은 없어지고, 새로운 값으로 대체된다. Map 역시 Immutable과 Mutable로 구분되어진다.
fun main(){
val jainoMap : MutableMap<String, String> = mutableMapOf("jinho" to "jaino", "simon" to "jiho")
println(jainoMap.values) // [jaino, jiho]
println(jainoMap.keys) // [jinho, simon]
jainoMap.put("age", "23") // put(key, value)
jainoMap.remove("simon")
println(jainoMap) // {jinho=jaino, age=23}
}
Map의 사용
Map의 자료구조는 HashMap, SortedMap, LinkedHashMap으로 나누어진다. 일반적으로 HashMap을 코딩테스트에서 빈번하게 사용할 수 있다.
아래에서는 HashMap의 사용 방법에 대해서 소개한다.
fun main(){
val hashMap = HashMap<Int, String>()
repeat(3){
hashMap.put(key = it, value = (it + 1).toString()) // 해시 맵 초기화 {1 to "1"}, ...
}
println(hashMap.keys) // > [0, 1, 2]
println(hashMap.values) // > [1, 2, 3]
println(hashMap.containsKey(2)) // true
println(hashMap.containsValue("0")) // false
println(hashMap.toSortedMap()) // > {0=1, 1=2, 2=3}
// 필자가 많이 사용하는 방법
val goal = "2"
hashMap.keys.forEach{
if(hashMap.get(it) == goal){ // value에 접근하여 다른 조건식 적용
return
}
}
}
Collection 함수 정리
코틀린은 확장함수를 통해, 다른 자료구조라도 비슷한 확장함수를 사용할 수 있다. 아래에서는 유용하게 사용할 수 있는 Collection 함수에 대해서 소개한다.
정렬 함수
Collection을 정렬하기 위해 다양한 함수가 사용될 수 있다. 아래에서 가장 많이 사용되는 정렬 함수를 소개한다.
가변 Collection인 경우, 자신을 정렬하며, 반환 값이 없다. 그러나 불변 Collection의 경우 자신을 정렬하지 않고, 정렬한 Collection을 반환한다.
따라서 가변 Collection과 다르게 불변 Collection인 경우(List, Map) 메소드에 ed 키워드를 붙여야 한다. (ex : sorted())
아래에서는 가변 Collection의 예시를 소개한다.
fun main(){
val list = mutableListOf(1, 2, 3, 4, 5)
list.sort() // 오름차순 정렬 > [1, 2, 3, 4, 5]
list.sortDescending() // 내림차순 정렬 > [5, 4, 3, 2, 1]
list.reverse() // 리스트 뒤집기 > [1, 2, 3, 4, 5]
list.sortBy{ int -> int % 3 } // > 정렬 기준을 입력하여, 정렬한다.
println(list) // > [3, 1, 4, 2, 5] > 3으로 나눈 나머지의 오름차순
list.sortWith(compareBy { int -> int % 2 }) // > Comparator 에 정의한 정렬 기준으로 정렬한다.
println(list) // > 4, 2, 3, 1, 5 > 2로 나눈 나머지의 오름차순
list.sortWith(compareBy<Int> { init -> init % 2 }.then(compareBy { int -> -int }))
println(list) // > [4, 2, 5, 3, 1] > 2로 나눈 나머지의 오름차순, 나머지가 같다면 내림차순 정렬
}
- sort(), sortDescending()
- 오름차순으로 정렬한다. 수가 아닌 문자열도 알파벳 순서대로 정렬한다.
- reverse()
- 현재 리스트를 뒤집는다.
- sortBy()
- 정렬 기준을 함수로 전달한다.
- 해당 예시외에 Data Class나 Pair, Triple 객체에서 내부의 프로퍼티만을 비교하는 용도로 사용된다. (ex : Pair의 경우 second 값만으로 정렬할 수 있다.)
- sortWith()
- 매개변수로 Comparator를 전달한다. Comparator는 CompareBy의 람다를 통해 구현할 수 있으며, 원하는 정렬 기준을 입력할 수 있다.
- Comparator의 then() 메소드를 통해, 비교 값이 같은 경우 2차적인 처리를 할 수 있다.
순회 함수
순회 함수는 각 요소에 접근할 수 있으며, 모든 원소에 적용할 함수를 전달하고 적용된 Collection을 반환 받을 수 있다.
순회 함수에는 Indexed라는 키워드를 뒤에 붙여, 각 요소의 값뿐만 아니라 Index에도 접근할 수 있다.
fun main(){
val list = listOf(1, 2, 3, 4, 5)
list.forEach{ // 각 요소의 value에 접근
println(it * 2)
} // [2, 4, 6, 8, 10]
list.forEachIndexed{ index, value -> // 각 요소의 index, value에 접근
println("index[$index] = $value")
} // index[0] = 1 index[1] = 2 index[2] = 3 index[3] = 4 index[4] = 5
val onEachList = list.onEach { // 모든 요소의 value에 적용할 함수 전달
print(it * 2)
} // > 246810
}
- forEach(), forEachIndexed()
- 각 요소에 접근하여 value를 반환 받아 작업을 수행할 수 있다.
- forEachIndexed()는 index와 함께 value를 반환 받는다.
- onEach(), onEachIndexed()
- 각 요소에 접근하여, value를 반환 받아 작업을 수행할 수 있다. 수행한 list를 다시 반환한다. (컬렉션은 변하지 않는다.)
- onEachIndexed()는 index와 함께 value를 반환 받는다.
매핑 함수
Collection에 사용할 수 있는 함수로, 요소에 특정 식을 수행하고 새로운 컬렉션을 반환하는 함수이다.
fun main(){
val list = listOf(1, 2, 3, 4, 5)
val listWithNull = listOf(1, null, 2, null, 3)
val twiceList = list.map{ it * 2 } // [2, 4, 6, 8, 10]
val listWithNotNull = listWithNull.mapNotNull { it?.times(2) } // [2, 4, 6]
val listWithJ = list.flatMap { listOf(it * 3, it * 4, it * 5) }
print(listWithJ) // [3, 4, 5, 6, 8, 10, 9, 12, 15, 12, 16, 20, 15, 20, 25]
val groupMap = list.groupBy { it % 2 == 0 } // {false=[1, 3, 5], true=[2, 4]}
}
- map()
- 모든 요소에 적용할 함수를 전달한다. 이때 함수의 반환값과 각 요소의 value는 같은 자료형이여야 한다.
- 인덱스를 포함하는 mapIndexed(), null을 제외하고 특정 식을 수행하는 mapNotNull()이 있다.
- flatMap()
- 모든 요소에 적용할 함수를 전달한다. 이때 함수의 반환값은 iterable(Collections을 포함하는 상위 객체)이여야 한다.
- 각 요소에 함수를 적용한 후 iterable를 합쳐 새로운 컬렉션을 반환한다.
- groupBy()
- 입력한 식에따라 요소를 그룹화하여 Map으로 반환한다.
필터링 함수
특정 식에 부합하는 요소를 골라내어 새로운 컬렉션을 반환하는 함수이다. 특정 식은 Boolean 값을 반환해야 한다.
fun main(){
val list = listOf(1, 2, 3, 4, 5)
val listWithNull = listOf(1, null, 2, null, 3)
val twiceList = list.filter{ it % 2 == 0} // [2, 4, 6]
val notTwiceList = list.filterNot { it % 2 == 0 } // [1, 3, 5]
val notNullList = listWithNull.filterNotNull() // [1, 2, 3]
}
- filter()
- 식에 부합하는 요소를 골라내어 컬렉션을 만들어 반환한다.
- filterNot()
- 식에 부합하지 않는 요소를 골라내어 컬렉션을 만들어 반환한다.
- fiterNotNull()
- null을 걸러내어 컬렉션을 만들어 반환한다.
이외에도 원하는 자료형을 골라내는 filterIsInstance(), 인덱스를 함께 추출하는 filterIndexed(), 반환하는filterIndexedTo()가 있다.
검사 함수
컬렉션의 요소를 하나씩 검사하면서 참/거짓을 반환하는 함수이다.
fun main(){
val list = listOf(1, 2, 3, 4, 5)
if (list.any { it % 2 == 0 }) {
println("적어도 하나의 짝수가 존재합니다.")
}else{
println("모든 수가 홀수입니다.")
}
if (list.all { it % 2 == 0 }) {
println("모든 수가 짝수입니다.")
}else{
println("적어도 하나의 홀수가 존재합니다.")
}
if (list.none { it > 120 }) {
println("모든 수가 120보다 작습니다.")
}else{
println("적어도 하나의 수가 120보다 큽니다.")
}
}
- any()
- 조건식을 만족하는 요소가 하나라도 있다면 참을 반환하며, 모두 만족하지 않으면 거짓을 반환한다.
- all()
- 모든 요소가 조건식을 만족해야 참을 반환하며, 그렇지 않다면 거짓을 반환한다.
- none()
- 모든 요소가 조건을 만족하지 않을 때 참을 반환한다. (any()의 반대)
중첩 함수
컬렉션 내의 데이터를 모두 모아 계산하는 함수로, 모은 데이터의 연산을 설정할 수 있다.
fun main(){
val list = listOf(3, 1, 6, 2, 12)
println(list.reduce{ total, num -> // 첫번 째 요소부터 시작한다.
total + num
}) // 24
println(list.fold(12){ total, num -> // 지정한 초기값부터 시작한다.
total + num
}) // 초기값 12, 총 36
}
- reduce()
- 첫 번째 요소부터 차례대로 설정한 연산을 수행하며, 연산의 결과를 반환한다.
- fold()
- 지정해 준 초기 값(매개변수) 으로 시작하여, 모든 데이터의 연산을 마치고, 연산의 결과를 반환한다.
fold, reduce외에 foldRight, reduceRight가 있다. foldRight(), reduceRight()는 오른쪽부터(맨 끝에서부터) 처리하는 함수이다.
집합 함수
교집합 혹은 합집합을 계산할 때 사용할 수 있는 함수이다.
fun main(){
val list = listOf(1, 2, 2, 3, 3)
val list1 = listOf(1, 2, 3, 4, 5)
val list2 = listOf(3, 4, 5, 6, 7)
println(list.distinct()) // [1, 2, 3] 중복되는 값 제외 list
println(list1.intersect(list2)) // [3, 4, 5] 두 컬렉션 간 중첩되는 값 list
println(list1.union(list2)) // [1, 2, 3, 4, 5, 6, 7] 중첩되는 값 소거
println(list1.plus(list2)) // [1, 2, 3, 4, 5, 3, 4, 5, 6, 7] 중첩되는 값 소거 하지 않음
}
- distinct()
- 중복된 요소를 소거하여 list로 반환하는 함수이다. (합집합)
- intersect()
- 두 컬렉션간의 겹치는 요소를 골라내어 list로 반환하는 함수이다. (교집합).
- union()
- 두 컬렉션을 병합하여 Set로 반환하는 함수로, 중복되는 값이 없다.
- plus()
- 중복되는 값을 그대로 합쳐 List를 반환하는 함수이다.
마지막으로
Kotlin은 다양한 유틸리티 확장함수나 문자열 처리 용이성과 같은 이유로 코딩테스트에 도입할 때 큰 장점이 있는 것 같다! 실제 개발을 Kotlin 언어로 하고 있다면, 코딩테스트도 Kotlin으로 준비하는 것도 나쁘지 않은 선택인 것 같다😀
참고 문헌
https://junyoung-developer.tistory.com/98