Swift로 코딩 테스트 준비하면서 도움 될만한 잡다한 지식

silverCastle·2022년 7월 31일
8

필자는 C++로 알고리즘을 공부하고 코딩테스트를 준비했었다. 하지만, 과거 한글과 기타 기호로 이루어진 문자열을 다루는 문제에서 좌절했던 경험이 있고 특정 기업에서는 Swift만 지원하기 때문에 Swift로 코딩테스트를 준비하려고 한다.
이 게시글은 새로운 지식을 얻을 때마다 정리하여 나중에 보기 편하기 위해 꾸준히 업데이트할 예정이다.

최종 수정일: 2023-01-16

🔍 고차함수의 시간적 비용

고차함수를 2개 이상 써야하는 경우 예를 들어 filter 함수를 2개를 사용하는 것보다 for문을 사용하는 것이 더 빠르다. filter의 시간 복잡도는 O(n)이긴 하지만 여러 개를 사용해야 한다면 for문으로 한번에 돌리자.

🔍 let을 사용하자

C++에서는 이를 크게 신경쓰지 않았다. 하지만 Swift에서는 값이 바뀌지 않을 경우엔 var를 지양하고 let을 사용하자. 어떤 문제를 풀 때 var을 let으로 바꾸었더니 통과했던 경험이 있다.

🔍 extension을 사용하자

아직까지는 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문에서 와일드카드 식별자를 사용하였는데 에러를 내뱉었던 경험이 있다. 구글링하여도 마땅한 답을 얻을 수 없어서 이유는 모르겠지만 와일드카드 식별자를 피하는 것이 좋겠다.

🔍 Dictionary에 Array 타입 생성

하나의 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])
        }        
    }

🔍 isEmpty를 사용하자

코드 로직상 배열이 비었는지 확인을 안하고 removeLast() 메소드를 사용해도 된다고 생각했지만 어느 한 케이스에서 런타임 에러가 발생하였다.
isEmpty로 배열이 비었는지의 여부를 확인하였더니 런타임 에러가 발생하지 않아 isEmpty를 사용하는 것이 좋겠다.

🔍 sorted()를 지양하고 sort()를 지향하자

둘 다 정렬한다는 점은 동일하지만, sorted()는 정렬된 배열을 새롭게 리턴해주므로 기존 배열에 영향을 끼치지 않고, sort()는 배열 자체를 정렬하므로 기존 배열에 영향을 끼친다.
sorted()는 값을 복제하여 새로운 배열을 생성해서 리턴하므로 메모리를 두배로 사용한다. 따라서 sort()를 사용하자.

🔍 Index 타입을 Int로 변환

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()
    }
}

🔍 Array 회전하기

회전하기 위해서 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"

🔍 아스키코드 값에 해당하는 Character로 변환

심심치 않게 아스키코드를 사용해야하는 경우가 있다.
아래는 모든 대문자를 저장하는 배열을 만드는데 아스키코드를 활용하여 만든 경우다.
대문자 A의 아스키코드 값은 65이고 알파벳의 개수는 26개이므로 아래와 같이 구현할 수 있다.

    var dict: [String] = (65..<65+26).map { String(UnicodeScalar($0)!) }

🔍 String 내부의 Character 정렬

문자열 배열의 경우에는 자주 정렬해보았지만, String 안에 있는 Character를 정렬하는 경우가 적어 정리한다.

let string = "foobar"
let sortedString = String(string.sorted())

🔍 sort가 stable sort라고 맹신하지 말자

Swift의 sort는 기본적으로 stable sort이다. 하지만, 클로저 등으로 조건을 주어 정렬할 경우 stable하지 않을 수 있으므로 맹신하지 않는 것이 좋겠다.

🔍 components보다는 split을 사용하자

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에서 첫번째꺼만 대치하기

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))

8개의 댓글

comment-user-thumbnail
2022년 8월 8일

잘 읽었습니다.

1개의 답글
comment-user-thumbnail
2023년 1월 11일

도움되는 정보 잘 알아갑니다 :)

1개의 답글
comment-user-thumbnail
2023년 6월 1일

와 짱이네요. 도움받고갑니당.

1개의 답글