필자는 C++로 알고리즘을 공부하고 코딩테스트를 준비했었다. 하지만, 과거 한글과 기타 기호로 이루어진 문자열을 다루는 문제에서 좌절했던 경험이 있고 특정 기업에서는 Swift만 지원하기 때문에 Swift로 코딩테스트를 준비하려고 한다.
이 게시글은 새로운 지식을 얻을 때마다 정리하여 나중에 보기 편하기 위해 꾸준히 업데이트할 예정이다.
최종 수정일: 2023-01-16
고차함수를 2개 이상 써야하는 경우 예를 들어 filter 함수를 2개를 사용하는 것보다 for문을 사용하는 것이 더 빠르다. filter의 시간 복잡도는 O(n)이긴 하지만 여러 개를 사용해야 한다면 for문으로 한번에 돌리자.
C++에서는 이를 크게 신경쓰지 않았다. 하지만 Swift에서는 값이 바뀌지 않을 경우엔 var를 지양하고 let을 사용하자. 어떤 문제를 풀 때 var을 let으로 바꾸었더니 통과했던 경험이 있다.
아직까지는 extension이 익숙치 않지만 Swift에서는 Int형으로 원하는 위치의 문자열 요소에 접근할 수 없으므로 extension을 사용하는 것이 편하다.
즉,
var answer: [String] = ["A", "B", "C"]
이라고 했을 때 answer[1] 이런 식으로 접근할 수 없으므로 Int형으로 원하는 위치의 문자열 요소에 쉽게 접근할 수 있게 subscript을 오버로딩한다. 코드에 아래 코드를 추가하면 된다.
extension String {
subscript(_ index: Int) -> Character {
return self[self.index(self.startIndex, offsetBy: index)]
}
}
C++에서는 Int형으로 선언된 값의 자릿수를 더하려면 10으로 나누고... 10으로 나눈 나머지 계산하고... 그런 번거러움은 잊어라. Swift에서는 단 한줄이면 된다.
아래 코드는 Int형 상수 n의 각 자릿수를 배열에 넣는 코드이다. n을 String으로 형변환을 하고 각 요소를 Int형으로 바꿔서 배열에 넣음을 의미한다.
var arr:[Int] = String(n).map { Int(String($0))! }
그리고나서 배열의 모든 요소를 더하면 된다. 이것 또한 단 한줄이다.
let sum: Int = arr.reduce(0, +)
문제를 풀면서 심심치 않게 나오는 개념인데 혹시라도 까먹을까봐 기록한다..
func GCD(_ min: Int, _ max: Int) -> Int {
let res = min % max
if res == 0 {
return max
}
return GCD(max, res)
}
func LCM(_ a: Int, _ b: Int) -> Int {
return (a * b) / GCD(a, b)
}
반복문은 문제 대다수에서 쓰이는데 유의할 점이 있다. 바로 범위를 조심해야 하는데 아래 예시를 보자.
var idx: Int = 2
for i in 3...idx {
}
코드에서는 idx 값을 2로 초기화하여 한눈에 봐도 에러 발생을 알 수 있지만 idx 값이 유동적으로 초기화된다면 범위의 시작인 3보다 작은지 큰지 알기 쉽지 않다. 필자도 문제를 풀면서 이를 조심하지 않아 틀린 경험이 있어 기록한다.
Swift는 아시다시피 데이터 타입에 대해 엄격한 문법을 적용하였기 때문에 다른 타입의 값을 할당할 경우 오류를 내뱉는다. 심지어 Int64형 변수의 값을 Int형 변수에 할당할 경우에도 그러하다.
var num64: Int64 = 123
var num: Int = n
결과
Cannot convert value of type 'Int64' to specified type 'Int'
for문에서 와일드카드 식별자를 사용하였는데 에러를 내뱉었던 경험이 있다. 구글링하여도 마땅한 답을 얻을 수 없어서 이유는 모르겠지만 와일드카드 식별자를 피하는 것이 좋겠다.
하나의 key에 여러개의 value를 할당하고 싶을 때 사용한다. 처음으로 어떤 key에 대한 value가 있는지 확인하고 없으면 updateValue
메소드를 사용하고, 있으면 append
메소드를 사용한다.
let clothes: [[String]] = [["yellow_hat", "headgear"], ["blue_sunglasses", "eyewear"], ["green_turban", "headgear"]]
var dic: [String:[String]] = [:]
for i in clothes {
if dic[i[1]] == nil {
dic.updateValue([i[0]], forKey: i[1])
}
else {
dic[i[1]]!.append(i[0])
}
}
코드 로직상 배열이 비었는지 확인을 안하고 removeLast()
메소드를 사용해도 된다고 생각했지만 어느 한 케이스에서 런타임 에러가 발생하였다.
isEmpty
로 배열이 비었는지의 여부를 확인하였더니 런타임 에러가 발생하지 않아 isEmpty를 사용하는 것이 좋겠다.
둘 다 정렬한다는 점은 동일하지만, sorted()는 정렬된 배열을 새롭게 리턴해주므로 기존 배열에 영향을 끼치지 않고, sort()는 배열 자체를 정렬하므로 기존 배열에 영향을 끼친다.
sorted()는 값을 복제하여 새로운 배열을 생성해서 리턴하므로 메모리를 두배로 사용한다. 따라서 sort()를 사용하자.
C++에서는 당연하게 가능했지만 Swift는 문자열에 포함된 문자의 인덱스를 정수 값으로 변환하는 것이 불가능하다. 따라서 다음 extension을 활용하자.
extension StringProtocol {
func distance(of element: Element) -> Int? { firstIndex(of: element)?.distance(in: self) }
func distance<S: StringProtocol>(of string: S) -> Int? { range(of: string)?.lowerBound.distance(in: self) }
}
extension Collection {
func distance(to index: Index) -> Int { distance(from: startIndex, to: index) }
}
extension String.Index {
func distance<S: StringProtocol>(in string: S) -> Int { string.distance(to: self) }
}
예제
let letters = "abcdefg"
let char: Character = "c"
if let distance = letters.distance(of: char) {
print("character \(char) was found at position #\(distance)") // "character c was found at position #2\n"
} else {
print("character \(char) was not found")
}
extension StringProtocol {
var firstUppercased: String {
prefix(1).uppercased() + dropFirst()
}
}
회전하기 위해서 removeFirst()
, append()
메소드를 적절히 활용하여 구현할 수 있지만 회전하는 그 정도가 크다면 구현하기 힘들기 때문에 extension을 활용한다.
왼쪽 방향으로 회전
extension RangeReplaceableCollection {
func rotatingLeft(positions: Int) -> SubSequence {
let index = self.index(startIndex, offsetBy: positions, limitedBy: endIndex) ?? endIndex
return self[index...] + self[..<index]
}
mutating func rotateLeft(positions: Int) {
let index = self.index(startIndex, offsetBy: positions, limitedBy: endIndex) ?? endIndex
let slice = self[..<index]
removeSubrange(..<index)
insert(contentsOf: slice, at: endIndex)
}
}
오른쪽 방향으로 회전
extension RangeReplaceableCollection {
func rotatingRight(positions: Int) -> SubSequence {
let index = self.index(endIndex, offsetBy: -positions, limitedBy: startIndex) ?? startIndex
return self[index...] + self[..<index]
}
mutating func rotateRight(positions: Int) {
let index = self.index(endIndex, offsetBy: -positions, limitedBy: startIndex) ?? startIndex
let slice = self[index...]
removeSubrange(index...)
insert(contentsOf: slice, at: startIndex)
}
}
예제
var test = [1,2,3,4,5,6,7,8,9,10]
test.rotateLeft(positions: 3) // [4, 5, 6, 7, 8, 9, 10, 1, 2, 3]
var test2 = "1234567890"
test2.rotateRight(positions: 3) // "8901234567"
심심치 않게 아스키코드를 사용해야하는 경우가 있다.
아래는 모든 대문자를 저장하는 배열을 만드는데 아스키코드를 활용하여 만든 경우다.
대문자 A의 아스키코드 값은 65이고 알파벳의 개수는 26개이므로 아래와 같이 구현할 수 있다.
var dict: [String] = (65..<65+26).map { String(UnicodeScalar($0)!) }
문자열 배열의 경우에는 자주 정렬해보았지만, String 안에 있는 Character를 정렬하는 경우가 적어 정리한다.
let string = "foobar"
let sortedString = String(string.sorted())
Swift의 sort는 기본적으로 stable sort이다. 하지만, 클로저 등으로 조건을 주어 정렬할 경우 stable하지 않을 수 있으므로 맹신하지 않는 것이 좋겠다.
let arr = readLine()!.components(separatedBy: " ").map { Int(String($0))! }
let arr2 = readLine()!.split(separator: " ").map { Int(String($0))! }
arr와 arr2는 같은 결과값을 가진다. 하지만, 구조상 성능차이가 있어 split을 사용한 arr2가 더 빠르다.
components는 리턴 타입이 [String]이고 split은 리턴 타입이 [SubString]임을 주의하고 map과 같은 고차 함수를 사용한다면 components보다는 split을 사용하자. 어차피 같은 결과값을 보이니까.
그래야 필자가 겪은 시간 초과
결과를 초래하지 않을 것이다.
replacingOccurrences는 String에서 특정 문자열을 내가 원하는 문자열로 모두 바꿀 수 있다. 하지만, 때에 따라서 전부다 대치하는 것이 아닌 하나만 대치하고 싶을 때가 있다. 다음과 같이 extension을 활용하자.
extension String {
func replacingFirstOccurrence(of target: String, with replacement: String) -> String {
guard let range = self.range(of: target) else { return self }
return self.replacingCharacters(in: range, with: replacement)
}
}
정수형에 대해 나머지를 구하는 모듈러 연산은 가능하지만 실수형은 % 대신에 truncatingRemainder를 사용하자.
let x = 12.345
let y = 11.111
print(x.truncatingRemainder(dividingBy: y))
잘 읽었습니다.