[iOS 4주차] Algorithm: 가장 많이 받은 선물 - 인라인 변수 선언 / Dictionary - default 값, uniqueKeysWithvalues: 생성자, grouping:by: 생성자

DoyleHWorks·2024년 11월 15일
2

가장 많이 받은 선물

문제 링크 / Github 링크
import Foundation

func solution(_ friends:[String], _ gifts:[String]) -> Int {
    return 0
}

처음 짠 코드

import Foundation

func solution(_ friends: [String], _ gifts: [String]) -> Int {
    
    // 선물지수 딕셔너리
    var friendGiftPowers = friends.reduce(into: [String: Int]()) { dict, friend in
        dict[friend] = 0
    }
    
    // 주고받음 딕셔너리
    var friendGifts: [[String]:Int] = [:]
    for i in 0..<friends.count {
        for j in (i + 1)..<friends.count {
            friendGifts[ [friends[i], friends[j]] ] = 0
        }
    }
    
    // 받을선물 딕셔너리
    var friendGiftsComing = friends.reduce(into: [String: Int]()) { dict, friend in
        dict[friend] = 0
    }
    
    // 가장많이받는 수
    var result = 0
    
    // 선물지수 & 주고받음 측정
    for gift in gifts {
        let giftArray = gift.components(separatedBy: .whitespaces)
        for key in friendGiftPowers.keys { // 선물지수 측정
            if key == giftArray[0] { friendGiftPowers[key]! += 1 }
            if key == giftArray[1] { friendGiftPowers[key]! -= 1 }
        }
        for key in friendGifts.keys { // 주고받음 측정
            if key[0] == giftArray[0] && key[1] == giftArray[1] { friendGifts[key]! += 1 }
            if key[0] == giftArray[1] && key[1] == giftArray[0] { friendGifts[key]! -= 1 }
        }
    }
    
    print(friendGiftPowers)
    print(friendGifts)
    
    // 받을선물 측정
    for key in friendGifts.keys {
        if friendGifts[key] == 0 {
            if friendGiftPowers[key[0]]! > friendGiftPowers[key[1]]! {
                friendGiftsComing[key[0]]! += 1
            } else if friendGiftPowers[key[0]]! < friendGiftPowers[key[1]]! {
                friendGiftsComing[key[1]]! += 1
            }
        } else if friendGifts[key]! > 0 {
            friendGiftsComing[key[0]]! += 1
        } else if friendGifts[key]! < 0 {
            friendGiftsComing[key[1]]! += 1
        }
    }
    
    print(friendGiftsComing)

    // 가장많이받는수 측정
    result = friendGiftsComing.values.max()!
    return result
}

개선할 수 있는 점

  1. 불필요한 반복 제거 및 간결화
    friendGiftPowersfriendGifts를 동시에 순회하며 값을 갱신하는 부분에서 불필요한 반복이 있음.

  2. 딕셔너리 접근의 강제 언래핑 제거
    friendGiftPowers[key]!와 같은 강제 언래핑(!)은 안전하지 않음.

  3. 코드의 가독성 향상
    가독성을 위해 변수를 좀 더 명확하게 이름짓고, 중복되는 코드를 함수로 추출할 수 있음.

개선한 코드

import Foundation

func solution(_ friends: [String], _ gifts: [String]) -> Int {
    var friendGiftPowers = Dictionary(uniqueKeysWithValues: friends.map { ($0, 0) })
    var friendGifts = Dictionary(uniqueKeysWithValues: combinations(friends, 2).map { ($0, 0) })
    var friendGiftsComing = Dictionary(uniqueKeysWithValues: friends.map { ($0, 0) })
    
    // 선물지수와 주고받은 선물 처리
    for gift in gifts {
        let giftArray = gift.components(separatedBy: .whitespaces)
        // guard giftArray.count == 2 else { continue } // 입력 데이터 유효성 체크
         
        let giver = giftArray[0], receiver = giftArray[1]
        
        friendGiftPowers[giver, default: 0] += 1
        friendGiftPowers[receiver, default: 0] -= 1
        
        let pair1 = [giver, receiver], pair2 = [receiver, giver]
        if let _ = friendGifts[pair1] {
            friendGifts[pair1, default: 0] += 1
        } else if let _ = friendGifts[pair2] {
            friendGifts[pair2, default: 0] -= 1
        }
    }
    
    // 받을 선물 수 계산
    for (pair, value) in friendGifts {
        let (friend1, friend2) = (pair[0], pair[1])
        if value == 0 {
            if friendGiftPowers[friend1, default: 0] > friendGiftPowers[friend2, default: 0] {
                friendGiftsComing[friend1, default: 0] += 1
            } else if friendGiftPowers[friend1, default: 0] < friendGiftPowers[friend2, default: 0] {
                friendGiftsComing[friend2, default: 0] += 1
            }
        } else if value > 0 {
            friendGiftsComing[friend1, default: 0] += 1
        } else {
            friendGiftsComing[friend2, default: 0] += 1
        }
    }
    
    // 가장 많이 받는 선물 수 반환
    return friendGiftsComing.values.max() ?? 0
}

// 친구 리스트에서 조합을 만드는 재귀함수
func combinations<T>(_ array: [T], _ n: Int) -> [[T]] {
    guard array.count >= n else { return [] }
    if n == 0 { return [[]] }
    guard let first = array.first else { return [] }
    let rest = Array(array.dropFirst())
    return combinations(rest, n - 1).map { [first] + $0 } + combinations(rest, n)
}


인라인 변수 선언

Swift에서는 let 뒤에 여러 개의 변수를 한 줄에 선언할 수 있다. 쉼표(,)로 구분하여 한 번에 여러 값을 바인딩할 수 있으며, 이는 주로 배열, 튜플 등의 데이터를 해체할 때 유용하다.


예시 1: 배열 해체

  • 쉼표로 구분하여 여러 변수를 선언할 수 있다.
let giftArray = ["Alice", "Bob"]
let giver = giftArray[0], receiver = giftArray[1]
// `giftArray[0]`의 값은 `giver`에, `giftArray[1]`의 값은 `receiver`에 할당된다.

print(giver)    // Alice
print(receiver) // Bob

예시 2: 튜플 해체

  • 한 줄에 여러 변수를 선언하면서 간결하게 사용할 수 있다.
let (x, y) = (10, 20)
let a = x, b = y
// `(x, y)` 튜플을 해체하여 각각 `x`와 `y`에 값을 할당한 후, 이를 다시 `a`, `b`에 할당한다.

print(a) // 10
print(b) // 20

예시 3: 조건문과 함께 사용

if let 또는 guard let에서도 동일한 방식으로 여러 변수를 한 번에 선언할 수 있다.

let optional1: Int? = 5
let optional2: Int? = 10

if let value1 = optional1, let value2 = optional2 {
// `optional1`과 `optional2`가 모두 `nil`이 아니면 두 값을 한 번에 언래핑한다.
    print("Both values are unwrapped: \(value1), \(value2)")
}

주의사항

  1. 한 줄에 여러 변수 선언은 가독성에 영향을 줄 수 있음:
    • 너무 많은 변수를 한 줄에 선언하면 코드가 복잡해 보일 수 있으므로, 상황에 따라 적절히 사용해야 한다.
  2. varlet 혼용 불가:
    • 한 줄에 선언할 때 모두 let이거나 모두 var이어야 한다.
    let x = 5, y = 10  // OK
    var a = 5, b = 10  // OK
    let x = 5, var y = 10  // Error!


Dictionary와 default 값

1. 기본적인 딕셔너리 사용법

딕셔너리는 키와 값의 쌍으로 이루어진 데이터 구조이다.

딕셔너리 선언

var dictionary: [String: Int] = ["apple": 3, "banana": 5]
  • String은 키의 타입
  • Int는 값의 타입

2. 키에 접근하기

딕셔너리에서 값을 가져오거나 설정할 때, 키가 존재하지 않을 수 있기 때문에 옵셔널 값이 반환된다.

let value = dictionary["apple"] // Optional(3)
let nonExistentValue = dictionary["orange"] // nil
  • 존재하지 않는 키를 접근하면 nil이 반환됨

3. Default 값을 사용한 안전한 접근

Dictionarysubscript(_:default:)를 활용하여 기본값을 제공할 수 있다.

사용법

let value = dictionary["orange", default: 0] // 0
  • "orange"가 딕셔너리에 없으면 기본값 0을 반환함
  • 키가 존재하면 해당 값을 반환함

장점

  • 옵셔널 언래핑이 필요 없음
  • 존재하지 않는 키에 접근할 때도 안전하게 처리할 수 있음

4. 값 업데이트 시 Default 활용

딕셔너리의 특정 값을 업데이트하거나 누적할 때도 default를 사용하면 유용하다.

예시: 값 누적

var wordCount: [String: Int] = [:]

let words = ["apple", "banana", "apple", "orange", "banana", "apple"]

for word in words {
    wordCount[word, default: 0] += 1
    // 단어가 딕셔너리에 없으면 기본값 0으로 시작.
    // 이미 존재하면 기존 값에 1을 추가.
}

print(wordCount)
// ["apple": 3, "banana": 2, "orange": 1]

5. Default 값으로 복잡한 데이터 구조 관리

기본값은 딕셔너리 안에 또 다른 컬렉션(배열, 딕셔너리)을 포함할 때도 유용하다.

예시: 배열을 값으로 사용하는 딕셔너리

var friendsGifts: [String: [String]] = [:]

friendsGifts["Alice", default: []].append("Book")
friendsGifts["Bob", default: []].append("Pen")
friendsGifts["Alice", default: []].append("Notebook")
// 키 "Alice"나 "Bob"이 없으면 빈 배열 []로 초기화한 후 값 추가.

print(friendsGifts)
// ["Alice": ["Book", "Notebook"], "Bob": ["Pen"]]

6. Default 값을 활용한 카운팅 문제

딕셔너리로 간단한 카운팅 문제를 해결할 때 default는 필수적이다.

예시: 문자열에서 각 문자의 빈도 수 계산

let text = "hello"
var charCount: [Character: Int] = [:]

for char in text {
    charCount[char, default: 0] += 1
}

print(charCount)
// ["h": 1, "e": 1, "l": 2, "o": 1]

요약

  • 옵셔널 값 처리: 기본값을 사용하면 키가 없을 때도 안전하게 값을 반환하거나 업데이트할 수 있다.
  • 코드 간결화: 강제 언래핑(!)이나 옵셔널 바인딩(if let, guard let) 없이 값을 안전하게 다룰 수 있다.
  • 유연성: 누적, 배열 추가, 카운팅 등 다양한 작업을 쉽게 처리할 수 있다.


Dictionary(uniqueKeysWithValues:) 생성자

Swift의 uniqueKeysWithValues는 배열을 딕셔너리로 변환할 때 사용하는 생성자이다. 주어진 배열의 각 요소를 키-값 쌍으로 변환하여 딕셔너리를 생성한다. 키는 유일해야 하며, 중복된 키가 있을 경우 런타임 에러가 발생한다.

정의

uniqueKeysWithValuesDictionary의 이니셜라이저 중 하나이다.

구문

Dictionary(uniqueKeysWithValues: sequence)
  • sequence: 키-값 쌍을 포함하는 배열 또는 시퀀스.

사용 예시

1. 배열에서 딕셔너리 생성

let pairs = [("Alice", 25), ("Bob", 30), ("Charlie", 28)]
let dictionary = Dictionary(uniqueKeysWithValues: pairs)
// `pairs` 배열의 각 튜플에서 첫 번째 요소가 키, 두 번째 요소가 값으로 사용됨

print(dictionary)
// ["Alice": 25, "Bob": 30, "Charlie": 28]

2. 문자열 배열로 딕셔너리 생성

let words = ["apple", "banana", "cherry"]
let wordLengths = Dictionary(uniqueKeysWithValues: words.map { ($0, $0.count) })
// `words` 배열에서 각 문자열을 키로, 문자열의 길이를 값으로 하는 딕셔너리가 생생됨

print(wordLengths)
// ["apple": 5, "banana": 6, "cherry": 6]

주의사항: 키의 고유성

uniqueKeysWithValues키가 고유해야 한다는 점에서 이름에 unique가 포함된다. 만약 중복된 키가 있는 경우, 런타임 에러가 발생한다.

에러 예시

let pairs = [("Alice", 25), ("Bob", 30), ("Alice", 28)]
let dictionary = Dictionary(uniqueKeysWithValues: pairs)
// Fatal error: Duplicate keys ("Alice") found when initializing Dictionary

유용한 활용 방법

  1. 데이터 변환:

    • 배열이나 튜플 시퀀스를 딕셔너리로 변환할 때 유용하다.
  2. 고유한 키-값 매핑:

    • 데이터가 고유한 키를 기반으로 매핑될 때 간결하게 딕셔너리를 생성할 수 있다.

관련 생성자/메서드

  • Dictionary(grouping:by:): 값들을 그룹화하여 딕셔너리를 생성할 때 사용.
  • zip: 두 배열을 키와 값으로 묶어 사용할 때 유용.

예시: zip과 함께 사용

let keys = ["Alice", "Bob", "Charlie"]
let values = [25, 30, 28]

let dictionary = Dictionary(uniqueKeysWithValues: zip(keys, values))
print(dictionary)
// ["Alice": 25, "Bob": 30, "Charlie": 28]


Dictionary(grouping:by:) 생성자

Dictionary(grouping:by:)컬렉션의 요소를 특정 기준으로 그룹화하여 딕셔너리를 생성하는 이니셜라이저이다. 그룹화된 딕셔너리의 키는 기준 함수(by)의 결과값이고, 값은 해당 키에 매핑된 요소들의 배열이다.

구문

Dictionary(grouping: sequence, by: keyForValue)
  • sequence: 그룹화할 대상 시퀀스 (배열, 문자열 등).
  • keyForValue: 각 요소에 적용할 그룹화 기준을 제공하는 함수.

사용 예시

1. 문자열 길이를 기준으로 그룹화

let words = ["apple", "banana", "cherry", "apricot", "blueberry", "grape"]

let groupedByLength = Dictionary(grouping: words, by: { $0.count })
// 단어의 길이(`$0.count`)를 기준으로 단어들을 그룹화함

print(groupedByLength)
// [5: ["apple", "grape"], 6: ["banana", "cherry"], 7: ["apricot"], 9: ["blueberry"]]
// 키는 단어의 길이, 값은 해당 길이의 단어 배열임

2. 문자열 첫 글자를 기준으로 그룹화

let names = ["Alice", "Bob", "Charlie", "Anna", "Brian", "Catherine"]

let groupedByFirstLetter = Dictionary(grouping: names, by: { $0.first! })
// 이름의 첫 글자(`$0.first!`)를 기준으로 그룹화함

print(groupedByFirstLetter)
// ["A": ["Alice", "Anna"], "B": ["Bob", "Brian"], "C": ["Charlie", "Catherine"]]
// 키는 첫 글자, 값은 해당 글자로 시작하는 이름 배열임

3. 숫자를 짝수와 홀수로 그룹화

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let groupedByEvenOdd = Dictionary(grouping: numbers, by: { $0 % 2 == 0 ? "Even" : "Odd" })
// 숫자를 짝수와 홀수로 나누어 그룹화함

print(groupedByEvenOdd)
// ["Odd": [1, 3, 5, 7, 9], "Even": [2, 4, 6, 8, 10]]
// 키는 "Even" 또는 "Odd"이며, 값은 해당 그룹의 숫자 배열임

활용 사례

  • 데이터 카테고리화:
    • 데이터를 특정 기준으로 그룹화하여 분석하거나 필터링에 사용.
  • 섹션 기반 UI:
    • 그룹화된 데이터를 기반으로 테이블뷰나 콜렉션뷰의 섹션을 구성할 때 활용.
profile
Reciprocity lies in knowing enough

2개의 댓글

comment-user-thumbnail
2024년 11월 15일

누워서 보다가 집중하다보면 앉아서 읽게 되는 마성의 글이네요

1개의 답글