[iOS 2주차] Swift: String.Index / String(repeating:count:) / 튜플 / 컬렉션의 명시적 선언 / reduce(into:) / 대소문자 정렬

DoyleHWorks·2024년 11월 1일
1

하루 회고

  • 오늘도 알고리즘을 풀면서 낯선 문법은 발견하고, 배운 문법에는 익숙해지는 시간을 가졌다. ChatGPT에게는 알고리즘 문제를 어떻게 접근할지 고민하고, 그 접근방법에 알맞는 메서드나 수학 공식 같은 게 있는지 물어보았다. 테스트를 통과하는 해답을 찾고나면 코드 평가를 부탁하기도 했다. 그 과정에서 최근에 알게 된 시간복잡도와 공간복잡도 개념을 활용해 여러 방법을 평가하는 시간을 갖기도 했다. 그 과정에서 여러 접근법들을 주석 처리하면서 같은 문제를 다르게 풀어보는게 나름 유익하다고 느꼈다.
  • Xcode에서 문법 자동 완성 기능을 받으면서 코딩하는 것과, 알고리즘 문제 사이트에서 코딩하는 것은 너무나도 느낌이 다르다. Xcode에서 새로운 문법들을 발견해나가며 코딩하는 것도 무척 유익하고 재밌을 것 같다.
  • 더 많이 다른 사람들의 답안을 살펴보면 좋겠다. 미려하게 정리된 코드를 보는 것도 물론 즐겁지만, 더 중요하게는 내가 생각치도 않거나 몰랐던 접근법들이 종종 보이기 때문이다.
  • 오늘은 흥미로운 이야기를 튜터님에게 많이 들었다. 파고들게 너무 많다.. 너무 많아서 여러 키워드들을 간략하게 정리해뒀다. (본문 최하단)

오늘 푼 알고리즘


ChatGPT 4o with canvas prompt:
(생략: 알고리즘 문제에 대한 질답)
지금까지 한 얘기를 반영해서, 처음부터 String.Index에 대해서 설명해줘

String.Index

Swift에서 String.Index란?

Swift에서 String.Index는 문자열 내 특정 위치를 나타내는 타입이다. Swift의 문자열은 단순 배열처럼 인덱스를 Int로 접근할 수 없으며, 대신 String.Index를 통해 문자열 내의 위치를 안전하게 다룬다. 문자열은 유니코드 스칼라(Unicode Scalar)로 구성되어 있으며, 각 문자는 가변 길이일 수 있기 때문에 단순히 정수형 인덱스를 사용하면 잘못된 위치에 접근할 위험이 있다.

String.Index는 문자열의 시작부터 특정 문자의 위치까지 직접 계산하여 다루기 때문에, 복잡한 유니코드 문자나 이모지 같은 여러 유니코드 스칼라로 이루어진 문자들을 다룰 때 정확하게 처리할 수 있다. Swift는 문자열의 정확한 인덱싱을 보장하기 위해 이런 방식을 채택하고 있다.

String.Index와 정수형 인덱스의 차이점

  • 정수형 인덱스(Int)는 배열처럼 각 요소에 대해 사용하지만, String.Index는 문자열의 복잡한 구조를 다루기 위해 고안된 타입이다.
  • 문자열에서 각 문자의 길이는 유니코드 특성상 다를 수 있기 때문에, 정수 인덱스를 사용하면 잘못된 위치를 참조하거나 문자열의 경계를 벗어나는 문제가 발생할 수 있다.

String.Index와 Int 상호작용 예시

만약 문자열 내에서 특정 문자에 접근하기 위해 Int를 사용하고 싶다면, String.Index로 변환하여 접근할 수 있다. 예를 들어, 문자열의 두 번째 문자에 접근하려면 다음과 같이 작성할 수 있다:

let text = "안녕하세요"
let intIndex = 2
if let index = text.index(text.startIndex, offsetBy: intIndex, limitedBy: text.endIndex) {
    print(text[index]) // "녕"
}

이 예시에서 text.index(_:offsetBy:limitedBy:) 메서드는 문자열의 시작 인덱스에서 Int만큼 이동한 String.Index를 반환한다. limitedBy:는 인덱스가 문자열의 끝을 초과하지 않도록 하여 안전하게 접근할 수 있게 한다.

String.Index 사용의 장점

  1. 안전한 접근: 문자열의 범위를 벗어나지 않도록 제한할 수 있다. 예를 들어 limitedBy:를 사용하여 인덱스가 문자열의 끝을 초과하지 않도록 안전장치를 제공한다.
  2. 유니코드 지원: 유니코드 스칼라와 결합 문자를 포함한 모든 문자에 대해 정확하게 접근할 수 있다.
  3. 복잡한 문자 처리: 여러 유니코드 스칼라로 이루어진 문자(예: 이모지나 결합형 문자)를 제대로 다룰 수 있어 잘못된 인덱스 접근을 방지한다.

String.Index와 Character 배열 변환 방법 비교

String.Index 대신 문자열을 Character 배열로 변환하여 중간 문자에 접근하는 방법도 있다. 이 방법의 시간 복잡도는 O(n)이며, 직관적으로 사용할 수 있지만 유니코드 문자가 복잡할 경우 의도한 대로 동작하지 않을 수 있다.

let word = "école" // 'é'는 'e' + 결합형 악센트 마크로 구성된 조합 문자
let characters = word.map { $0 }
print(characters) // 출력: ["e", "́", "c", "o", "l", "e"]

let middleIndex = word.count / 2
let middleString = word.count.isMultiple(of: 2)
    ? (String(characters[middleIndex - 1]) + String(characters[middleIndex]))
    : String(characters[middleIndex])

print(middleString) // 조합 문자가 분리되어 예상과 다른 결과가 나올 수 있음

이처럼 Character 배열로 변환할 경우, 여러 유니코드 스칼라로 이루어진 조합 문자가 분리될 수 있어 정확한 처리가 어렵다. 반면 String.Index를 사용하면 유니코드 문자를 안전하게 다룰 수 있어 문자열의 정확한 위치를 계산하고 접근할 수 있다.

결론

  • String.Index는 Swift에서 문자열의 위치를 안전하게 다루기 위한 타입으로, 복잡한 유니코드 문자를 처리할 때 특히 유용하다.
  • Int 인덱스와는 다르게, 문자열 내의 정확한 위치를 보장하며, limitedBy 등을 사용해 범위를 벗어나지 않도록 안전하게 처리한다.
  • 단순히 Character 배열로 변환하는 방법은 이해하기 쉽지만, 복잡한 유니코드 문자 처리가 필요할 때는 적합하지 않을 수 있다.

따라서 복잡한 문자열이나 유니코드 문자를 다룰 때는 String.Index를 사용하는 것이 안전하고 올바른 접근 방식이다.


String(repeating:count:)

String(repeating:count:) 메서드는 Swift에서 문자열을 반복하여 원하는 개수만큼 생성할 때 사용하는 메서드이다. 이 메서드를 사용하면 지정한 문자열을 주어진 횟수만큼 반복하여 새 문자열을 생성할 수 있다.

구문 (Syntax):

String(repeating: <문자열>, count: <반복 횟수>)
  • repeating: 반복할 문자열을 지정한다.
  • count: 해당 문자열을 몇 번 반복할지 지정한다.

예제:

let repeatedString = String(repeating: "수박", count: 3)
print(repeatedString) // "수박수박수박"

위 코드에서는 "수박"이라는 문자열을 3번 반복하여 "수박수박수박"이라는 결과를 얻을 수 있다.

이 메서드를 활용하면 문자열의 반복 작업을 간편하고 효율적으로 수행할 수 있다. 또한, 반복 작업을 메모리 효율적으로 처리하기 때문에 일반적인 문자열 추가(+=) 방식보다 성능이 더 좋다.


// ChatGPT 4o prompt:
...
(lowercases, uppercases)는 (변수명, 변수명) 이잖아.
튜플은 변수와 변수를 담을수도 있는거야?

튜플

튜플은 변수 자체를 담는 것이 아니라 변수의 값을 담는 구조이다. 즉, (lowercases, uppercases)와 같은 튜플은 lowercasesuppercases라는 변수명을 사용하여 각 값을 담고 있는 튜플이다.

튜플의 역할

  • 튜플은 여러 개의 값을 하나로 묶는 데 사용된다.
  • 각 요소는 다른 타입일 수도 있고, 특정 이름이 없는 경우도 있으며, 변수의 값을 담는 컨테이너 역할을 한다.

예시: 튜플의 사용

let lowercases = ["a", "b", "c"]
let uppercases = ["A", "B", "C"]

// 튜플로 두 배열을 묶을 수 있음
let combined = (lowercases, uppercases)

// combined는 두 배열을 담고 있는 튜플
print(combined.0)  // ["a", "b", "c"]
print(combined.1)  // ["A", "B", "C"]

변수와 튜플

튜플은 변수 자체가 아닌 변수에 담긴 값을 담는다. 따라서 let tuple = (lowercases, uppercases)를 하면 tuplelowercasesuppercases값을 포함하는 새로운 튜플이다. 이후 tuple.0tuple.1을 사용하면 각각의 값에 접근할 수 있다.

튜플을 변수에 풀어서 할당

튜플을 사용하면 그 값을 변수로 쉽게 나눌 수 있다:

let (lc, uc) = combined
// lc는 ["a", "b", "c"]
// uc는 ["A", "B", "C"]

이처럼 let (lc, uc)combined 튜플의 첫 번째와 두 번째 요소를 lcuc 변수에 할당한다.


컬렉션의 명시적 선언

Swift에서 컬렉션은 명시적으로 선언할 때 다음과 같은 방식으로 작성된다:

  1. 배열(Array):

    • 빈 배열을 생성할 때 [] 또는 생성자 Array()를 사용한다.
    • 타입을 명시적으로 선언할 때는 [Int](), [String](), [Double](), Array<Int>() 등의 형식을 사용한다.
    let intArray: [Int] = []        // 빈 배열
    let stringArray = [String]()    // 빈 배열
    let doubleArray: Array<Double> = Array()  // 빈 배열
  2. 딕셔너리(Dictionary):

    • 빈 딕셔너리를 생성할 때 [:] 또는 Dictionary()를 사용한다.
    • 타입을 명시적으로 선언할 때는 [String: Int]() 또는 Dictionary<String, Int>() 등의 형식을 사용한다.
    let intDictionary: [String: Int] = [:]       // 빈 딕셔너리
    let stringDictionary = [String: String]()    // 빈 딕셔너리
    let boolDictionary: Dictionary<String, Bool> = Dictionary()  // 빈 딕셔너리
  3. 집합(Set):

    • 빈 집합을 생성할 때 Set()을 사용한다.
    • 타입을 명시적으로 선언할 때는 Set<Int>(), Set<String>() 등의 형식을 사용한다.
    let intSet: Set<Int> = Set()        // 빈 집합
    let stringSet = Set<String>()       // 빈 집합

각 컬렉션을 명시적으로 선언할 때, 타입을 [Int], [String: Int], Set<String> 등으로 지정하고 빈 컬렉션을 만들기 위해 (), [:], 또는 []를 사용한다.


reduce()와 reduce(:into)의 차이

reduce()reduce(into:)는 Swift에서 컬렉션의 요소를 순회하며 값을 누적하는 데 사용되는 메서드이다. 두 메서드는 목적은 비슷하지만, 작동 방식메모리 효율성에서 차이가 있다.

0. reduce() 기본 구문

let result = collection.reduce(initialValue) { (accumulator, element) in
    // 새로운 누적값을 반환
}
  • initialValue: 누적값의 초기값이다.
  • accumulator: 현재까지의 누적값이다.
  • element: 컬렉션의 현재 요소이다.
  • 클로저는 각 단계에서 새로운 누적값을 반환하여 다음 단계의 accumulator로 사용된다.

배열의 합을 구하는 예시:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { $0 + $1 }  // 초기값은 0, 각 요소를 더해나감
print(sum)  // 출력: 15
  • $0: 현재 누적값 (accumulator).
  • $1: 현재 배열의 요소 (element).
  • 각 단계에서 accumulatorelement를 더한 결과가 다음 단계의 누적값으로 사용된다.

이처럼 reduce()는 반환되는 값을 통해 누적 결과를 갱신하는 방식으로 최종 결과를 생성한다.

1. 작동 방식의 차이

  • reduce():

    • 클로저가 반환하는 값을 통해 누적 결과를 생성한다.
    • 각 단계에서 반환된 값을 새로운 누적값으로 사용하므로, result는 매번 새로운 값을 반환한다.
    let array = [1, 2, 3, 4, 5]
    let sum1 = array.reduce(0) { (result, element) in
        result + element
    }
    // 결과: sum은 15
    let sum2 = array.reduce(0) { $0 + $1 }
    // 이렇게도 표현 가능 (sum1 == sum2)
  • reduce(into:):

    • 초기값을 인자로 받아 클로저 내에서 직접 변경한다.
    • 반환 없이 초기값 자체를 수정하여 누적하므로, 메모리 효율성이 더 높을 수 있다.
    let array = [1, 2, 3, 4, 5]
    let sum1 = array.reduce(into: 0) { (result, element) in
        result += element  // 초기값(result)을 직접 수정
    }
    // 결과: sum은 15
    let sum2 = array.reduce(int: 0) { $0 *= $1 }
    // 이렇게도 표현 가능 (sum1 == sum2)

2. 메모리 효율성

  • reduce()는 새로운 값을 반환하므로, 각 단계에서 새로운 값이 만들어지고, 메모리 사용이 더 높을 수 있다.
  • reduce(into:)는 초기값을 수정하며 작업을 수행하므로, 기존 값을 재사용하여 메모리 사용이 더 효율적이다.

3. 반환값의 차이

  • reduce()는 클로저가 반환한 값을 통해 누적값을 업데이트한다.
  • reduce(into:)는 반환값이 필요하지 않으며, 초기값을 직접 수정하여 최종 결과를 생성한다.

sorted()의 문자열 정렬 (대소문자)

sorted() 메서드는 문자열의 정렬을 수행할 때 유니코드 스칼라 값을 기준으로 각 문자를 비교하여 정렬한다. 유니코드 스칼라 값은 각 문자가 유니코드 표준에서 가지는 고유한 숫자이다. 이를 통해 문자열의 정렬 순서를 결정한다.

유니코드 스칼라 값이란?

  • 모든 문자는 유니코드 표준에서 고유한 숫자로 매핑된다.
  • 예를 들어:
    • 'A'의 유니코드 스칼라 값은 65
    • 'a'의 유니코드 스칼라 값은 97
    • 'B'는 66, 'b'는 98 등등
let str = "aAbBcC"
let sortedStr = String(str.sorted())
print(sortedStr)  // 출력: "ABCabc"

대소문자 정렬 결과

대문자의 유니코드 값은 소문자보다 작기 때문에, 기본 오름차순 정렬에서는 대문자가 소문자보다 앞에 위치하게 된다. 내림차순 정렬 (sorted(by: >))을 사용할 경우, 큰 유니코드 값이 먼저 오므로 소문자가 대문자보다 앞에 위치하게 된다.


Interesting Keywords

  • Singleton 패턴
  • MVC: Model-View-Controller 패턴
  • Shallow Copy vs Deep Copy
  • 결합도(Coupling) & 응집도(Cohesion)
  • POP (Protocol-Oriented Programming) vs OOP (Object-Oriented Programming)
  • 일급 객체 (First-Class Citizen)
  • RxSwift, ReactiveX
  • Endpoint
  • RESTful: REST(Representational State Transfer) 아키텍처 스타일
  • Trade-off
profile
Reciprocity lies in knowing enough

0개의 댓글