문제

 

풀이 코드

var result = [Int]()
let inputs = readLine()!.split(separator: " ")
var num: [Int] = Array(1...Int(inputs[0])!)
var idx = Int(inputs[1])! - 1

while true {
    result.append(num.remove(at: idx))
    if num.isEmpty { break }
    idx = (idx + Int(inputs[1])! - 1) % num.count
}
print("<\(result.map({ String($0)}).joined(separator: ", "))>")

 

풀이 과정

문제를 이해하는데 그림을 그려야 했다.. 그래서 발로 그린듯한 그림을..

이렇게 원형으로 앉아있을때 K번 순으로 제거하는 문제였다

K번째를 셀때, 제거된 자리는 건너뛰고 세어야 한다!! 

 

코드에서 inputs[0] 에는 N 이,  inputs[1] 에는 K 가 들어간다.

 

그리고 나서 규칙을 찾기위해 열심히.. 그림을 그렸다

처음 idx 는 K - 1 ( 인덱스는 0부터 시작하기 때문!! ) 가 되고,

이후로 제거할때 마다 idx 를 바꿔줘야 하는데, 어떻게 바꿀지를 그리면서 고민한 결과 규칙을 찾았다!

 

1. 우선 idx 인덱스 요소를 제거한다

2. 현재 idx 의 값에 K-1 을 더한후, 제거후 남은 배열의 갯수로 나눈 나머지로 재설정한다 ( idx = (idx + (k-1)) % Array.count )

3. 무한 루프로 돌되, 제거후 배열이 비어 있는 상태가 되면 루프를 종료시킨다!

 

알고나면 간단한 문제였다..

이래서 그리면서 생각하는게 중요한가봄..

문제

 

풀이 코드

struct Queue<T> {
    private var elements: [T] = []
    
    public var size: Int {
        return elements.count
    }
    
    public var empty: Int {
        return elements.isEmpty ? 1 : 0
    }
    
    public func front() -> T? {
        return self.elements.first
    }
    
    public func back() -> T? {
        return self.elements.last
    }
    
    public mutating func push(_ element: T) {
        return elements.append(element)
    }
    
    public mutating func pop() -> T? {
        return elements.isEmpty ? nil : elements.removeFirst()
    }
}

let N = Int(readLine()!)!
var intQueue = Queue<Int>()

for _ in 0..<N {
    let inputs = readLine()!.split(separator: " ")
    switch inputs[0] {
    case "push":
        intQueue.push(Int(inputs[1])!)
    case "pop":
        print(intQueue.pop() ?? -1)
    case "size":
        print(intQueue.size)
    case "empty":
        print(intQueue.empty)
    case "front":
        print(intQueue.front() ?? -1)
    default:
        print(intQueue.back() ?? -1)
    }
}

 

풀이 과정

Queue에 대한 개념을 학습하고, 이를 Swift 로 구현한 후 문제를 보니 쉽게 다가왔다,

Queue를 정의한 struct를 이용해서 문제를 풀 수 있었다.

 

그런데 풀이코드와 같은 Queue의 구조를 쓰게되면 메모리 효율이 떨어지기 때문에 다른 방법으로 큐를 구현하여 풀었더니

분명 TastCase 모두 정답이 나왔고, 문제없이 잘 출력 되었는데 틀렸다는 채점결과를 얻게되었다.

아직까지 뭐가 문제인지 모르겠다,, 이것저것 테스트를 해보았지만 정상적으로 값이 출력되었는데 도저히 모르겠다.

 

혹시나 해서 메모리 효율을 무시하고 그냥 원래의 방식대로 풀었더니 맞았다고 나왔다..

출력 결과는 같은데 도대체 뭐가 다른건지.. 도저히 모르겠다..

 

문제의 코드

struct Queue<T> {
    private var elements: [T?] = []
    private var head = 0
    
    public var size: Int {
        var numberOfNonNilValues = 0
        for case .some in elements { numberOfNonNilValues += 1 }
        return numberOfNonNilValues
    }
    
    public var empty: Int {
        return elements.isEmpty ? 1 : 0
    }
    
    public func front() -> T? {
        return self.elements.first ?? nil
    }
    
    public func back() -> T? {
        return self.elements.last ?? nil
    }
    
    public mutating func push(_ element: T) {
        return elements.append(element)
    }
    
    public mutating func pop() -> T? {
        guard head < elements.count, let element = elements[head] else { return nil }
        elements[head] = nil
        head += 1
        
        if head > (elements.count / 3) {
            elements.removeFirst(head)
            head = 0
        }
        return element
    }
}

let N = Int(readLine()!)!
var intQueue = Queue<Int>()
var element: Int?

for _ in 0..<N {
    let inputs = readLine()!.split(separator: " ")
    switch inputs[0] {
    case "push":
        intQueue.push(Int(inputs[1])!)
    case "pop":
        print(intQueue.pop() ?? -1)
    case "size":
        print(intQueue.size)
    case "empty":
        print(intQueue.empty)
    case "front":
        print(intQueue.front() ?? -1)
    default:
        print(intQueue.back() ?? -1)
    }
}

Queue 란?

  • Queue 자료구조를 그림을 통해 이해해 보자
  • 각 아이템들이 들어간 순서대로 나오는 구조를 이야기한다
  • First In First Out ( FIFO )

Swift 에서 Queue 구현

  • 구조체로 Queue를 선언한 코드를 먼저 살펴보자
// Queue 선언
struct Queue<T> {
    private var elements: [T] = []
    
    public var count: Int {
        return elements.count
    }
    
    public var isEmpty: Bool {
        return elements.isEmpty
    }
    
    public mutating func enqueue(_ element: T) {
        return elements.append(element)
    }
    
    public mutating func dequeue() -> T? {
        return isEmpty ? nil : elements.removeFirst()
    }
}

// Queue 사용
var intQueue = Queue<Int>()
print(intQueue)
// Queue<Int>(elements: [])

intQueue.enqueue(3)
print(intQueue)
// Queue<Int>(elements: [3])

print(intQueue.count)
print(intQueue.isEmpty)
// 1
// false

intQueue.dequeue()
print(intQueue.isEmpty)
// true
  • 구조체를 이용해 [ count , isEmpty, enqueue, dequeue ] 를 간단히 구현 할수 있었다

 

문제점

구현 한거 아니야? 뭐가 문제야!!!

응.. 문제야..

  • 위에서 선언한 방식으로 Queue를 사용하는데 ‘기능상' 문제는 없다, 하지만 메모리 관리 측면에서 너무 비효율적이다
  • 비효율 적인 이유

  • dequeue 를 수행할 때 removeFirst() 내장 함수를 사용하게 되는데 이것이 문제가 된다.
  • 첫 요소를 제거한 후 모든 요소들의 인덱스를 한칸씩 앞으로 당겨야한다.

⇒ 위 그림에서는 한번만 수행되지만 만약 dequeue가 여러번 수행된다면..? 엄청난 메모리 낭비를 하게 될것이다

요소 하나하나 제거할 때마다 만약 queue내의 item이 100개, 1000개 라면,, 요소 하나를 제거하기 위해 수없이 많이 인덱스를 한칸씩 당겨주어야 한다

 

해결 방법

  • head 라는 놈을 추가해 주고, dequeue될때마다 head 인덱스를 nil 로 바꿔준다
  • 그리고 일정 수준 이상으로 nil 이 채워지면, 0인덱스부터 head 까지의 요소를 한번에 제거하고 head 를 0으로 초기화 해준다

  • 이런 방법을 통해서 dequeue를 매번 하지않고, 쌓아 두었다가 일정 수준 이상에 도달하면 한번에 제거하기 때문에 메모리 측면에서 기존 방식보다 더 효율적이다

향상된 Queue 구현

struct Queue<T> {
		// nil 이 포함되기 때문에 [T?] 의 Optional 타입으로 선언
    private var elements: [T?] = []
    private var head = 0
    
		// nil 인경우 count를 안하기 위해 for case 문을 사용
    public var count: Int {
        var  elementCount = 0
        for case .some in elements { elementCount += 1 }
        return elementCount
    }
    
    public var isEmpty: Bool {
        return elements.isEmpty
    }
    
    public mutating func enqueue(_ element: T) {
        return elements.append(element)
    }
    
    public mutating func dequeue() -> T? {
				// head 가 큐의 아이템 갯수보다 크거나, head가 가르키는 곳이 nil 일경우 nil 반환
        guard head < elements.count, let element = elements[head] else { return nil }
        elements[head] = nil
        head += 1

				// Queue아이템의 1/3 보다 클경우마다 인덱스 0부터 head 까지 요소를 제거하고 head 0으로 초기화
        if head > (elements.count / 3) {
            elements.removeFirst(head)
            head = 0
        }
        return element
    }
}
  • 기존 방법과 다르게 nil 이 포함되기 때문에 옵셔널 타입의 배열로 선언해야 한다 [T?]
  • 아이템의 1/3 보다 head가 클 경우마다 nil 요소들을 지우게 설정했지만, 적절하게 조절해서 사용하면 될거같다
  • Count 의 경우, nil 인경우 count를 안하기 위해 for case 문을 사용 Nil 이 아닌 경우만 세서 반환한다.
var iQ = Queue<Int>()

iQ.enqueue(3)
iQ.enqueue(4)
iQ.enqueue(5)

iQ.count
// 3

iQ.dequeue()
// 3
iQ.count
// 2

iQ
// [nil, 4, 5] , head = 1
iQ.dequeue()
// 4
iQ
// [5], head = 0

iQ.count
// 1

iQ.dequeue()
// 5
iQ
// [], head = 0

iQ.dequeue()
// nil
iQ.count
// 0
iQ.dequeue()
// nil

 

참고 링크

https://babbab2.tistory.com/84

멈춰!!! 선행 학습이 필요한 글이니 앞선 글들을 읽고 학습해보자!

URLSession 이란?

URL 생성하기

URLSession 적용하기

Codable 개념 파악하기

선행학습을 끝냈으니 드디어 API를 통해 원하는 데이터를 가져와보자잇!

Codable 프로토콜을 채택한 구조체 선언

  • 우선 구조체를 정의하기 전에 API를 호출했을때 데이터가 어떻게 날아오는지 먼저 확인하자
  • 주소창에 API 호출 주소를 입력하고 표시된 내용을 복사하여 https://jsonlint.com 이곳에서 확인해보자
  • 내용을 확인해보니 위와 같은 데이터로 Json 형태가 호출된다
  • 여기서 [ 평점, 제목, 줄거리, 포스터 경로 ] 네개만 일단 받아와 보자!
struct Response: Codable {
    let page: Int?
    let result: [MovieInfo]
    
    enum CodingKeys: String, CodingKey {
        case page
        case result = "results"
    }
}

struct MovieInfo: Codable {
    let title: String?
    let rating: Double?
    let summary: String?
    let post: String?
    
    enum CodingKeys: String, CodingKey {
        case title
        case rating = "vote_average"
        case summary = "overview"
        case post = "poster_path"
    }
}
  • Response 구조체를 Page 정보와, 정보를 배열로 담고있는 results를 정의한다
  • MovieInfo 구조체에는 [ 영화제목, 평점, 개요, 영화 포스터 ] 의 정보를 담는다
  • 구조체의 형태와 CodingKey 가 뭔지 이해하려면 본 글 제일 위에있는 선행학습을 하자! → Codable

Json 을 Model 로 Decode

// 데이터 파싱하기
    do {
        let decoder = JSONDecoder()
        let respons =  try decoder.decode(Response.self, from: resultData)
        let searchMovie = respons.result
        
        print("영화 제목 : \(searchMovie[0].title ?? "")")
        print("영화 평점 : \(searchMovie[0].rating ?? 0)")
        print("영화 줄거리 : \(searchMovie[0].summary ?? "")")
        print("포스터 경로 : \(searchMovie[0].post ?? "")")
    } catch let error {
        print(error.localizedDescription)
    }
  • JSONDecoder 를 통해 파싱을 시도하고, 이때 Error가 발생하면 Catch로 이동 할 것이다
  • 파싱이 완료 된후, 첫번째 인덱스의 정보들을 출력해 본다
/*
 영화 제목 : 어벤져스 오브 저스티스
 영화 평점 : 4.1
 영화 줄거리 : 은하계 최강의 빌런 조크스터는 태양을 없애 지구에 새로운 빙하기를 불러일으키고자 한다. 슈퍼히어로 슈퍼배트는 히어로 동료들과 함께 조크스터에 맞서 지구를 지켜내야만 한다. 이제 전 인류의 운명이 걸린 최후의 대결이 시작된다.
 포스터 경로 : /yymsCwKPbJIF1xcl2ih8fl7OxAa.jpg
 */
  • 정보를 잘 가져온 것을 확인할수 있다!
  • print 문에서 nil 병합 연산자 ( \\(searchMovie[0].title ?? "" ) 를 사용한 이유는 해당 데이터가 Optional로 오기 때문에 사용하였다, 실제 iOS 앱에서는 적절한 바인딩을 해야한다!
  • nil 병합 연산자는 해당 변수 및 상수가 nil이 아니면 원래 값을 반환하고, nil 일 경우 ?? 뒤에 오는 Default 값을 반환한다

⇒ 포스터 경로의 경우 TMDB의 API 문서를 보면 되는데

https://www.themoviedb.org/t/p/[포스터 사이즈]/[포스터 경로]

형태로 들어가면 img 파일을 볼수 있다

 

예시 아래 주소를 들어가보자!

https://www.themoviedb.org/t/p/w220_and_h330_face/yymsCwKPbJIF1xcl2ih8fl7OxAa.jpg

전체코드

// 구조체 선언
struct Response: Codable {
    let page: Int?
    let result: [MovieInfo]
    
    enum CodingKeys: String, CodingKey {
        case page
        case result = "results"
    }
}

struct MovieInfo: Codable {
    let title: String?
    let rating: Double?
    let summary: String?
    let post: String?
    
    enum CodingKeys: String, CodingKey {
        case title
        case rating = "vote_average"
        case summary = "overview"
        case post = "poster_path"
    }
}

let API_KEY = "발급받은 API KEY"
var movieSearchURL = URLComponents(string: "https://api.themoviedb.org/3/search/movie?")

// 쿼리 아이템 정의
let apiQuery = URLQueryItem(name: "api_key", value: API_KEY)
let languageQuery = URLQueryItem(name: "language", value: "ko-KR")
let searchQuery = URLQueryItem(name: "query", value: "어벤져스")

// URLComponents 에 쿼리 아이템 추가
movieSearchURL?.queryItems?.append(apiQuery)
movieSearchURL?.queryItems?.append(languageQuery)
movieSearchURL?.queryItems?.append(searchQuery)

// 옵셔널 바인딩
guard let requestMovieSearchURL = movieSearchURL?.url else { throw NSError() }

// configuration 설정 -> default
let config = URLSessionConfiguration.default

// session 설정
let session = URLSession(configuration: config)

// dataTask 설정
let dataTask = session.dataTask(with: requestMovieSearchURL) { (data, response, error) in
    guard error == nil else { return }
    
    guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
    let successRange = 200..<300
    guard successRange.contains(statusCode) else { return }
    
    guard let resultData = data else { return }

    // 데이터 파싱하기
    do {
        let decoder = JSONDecoder()
        let respons =  try decoder.decode(Response.self, from: resultData)
        let searchMovie = respons.result
        
        print("영화 제목 : \(searchMovie[0].title ?? "")")
        print("영화 평점 : \(searchMovie[0].rating ?? 0)")
        print("영화 줄거리 : \(searchMovie[0].summary ?? "")")
        print("포스터 경로 : \(searchMovie[0].post ?? "")")
    } catch let error {
        print(error.localizedDescription)
    }
}

dataTask.resume()

공식문서 정의

개요

A type that can convert itself into and out of an external representation.

→ 외부 표현으로 전환할 수 있는 유형

정의

typealias Codable = Decodable & Encodable

내용

Codable is a type alias for the Encodable and Decodable protocols. When you use Codable as a type or a generic constraint, it matches any type that conforms to both protocols.

→ Codable은 Encodable 및 Decodable 프로토콜의 typealias입니다. Codable을 사용하면 두 프로토콜을 모두 준수해야 한다.

  • Encodable -> data를 Encoder에서 변환해주려는 프로토콜로 바꿔주는 것 ( 모델 → Json )
  • Decodable -> data를 원하는 모델로 Decode 해주는 것 ( Json → 모델 )

⇒ Swift4 버전부터 추가되었으며 기존 Decodable 과 Encodable 을 하나로 합친 프로토콜

⇒ Type 이 모두 지정되어 있는경우 Decoable과 Encodable을 별도 채택 안해도 Codable만으로 사용

⇒ Class , Struct, Enum 상관 없이 모두 사용 가능한 프로토콜!!!

 

코드로 확인

Codable 채택

  • Codable 을 채택한 Struct 생성
struct User: Codable {
    let age: Int
    let name: String
    let phone: String
    let address: String
    
    // Json 정보와 struct의 정보를 파싱하기 위해 사용 CodingKey
    // 이름이 같으면 원형 그대로 사용하고, 다른경우 별도 지정해주면 된다
    enum CodingKeys: String, CodingKey {
        case age
        case name
        case phone = "phone_number"
        case address
    }
}
  • Codable 프로토콜을 User 구조체에 채택
  • 서버로부터 받아오는 데이터의 이름과 구조체에서 정의한 이름이 다를 수 있기 때문에 CodingKey를 이용하여 적용해준다 , 이때 서버와 구조체의 이름이 동일하다면 그대로 사용하면 되며 다를경우 파싱해준다

⇒ 예제 코드에서 Json 데이터의 phone 이름이 phone_number 로 오기 때문에 파싱을 해주었다.

Decodable ( 외부 데이터 → 모델 ) 적용

// Decode
extension User {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        age = try container.decode(Int.self, forKey: .age)
        name = try container.decode(String.self, forKey: .name)
        phone = try container.decode(String.self, forKey: .phone)
        address = try container.decode(String.self, forKey: .address)
    }
}

func decode() {
    let jsonString = """
                            [
                                {
                                    "age": 18,
                                    "name": "홍길동",
                                    "phone_number": "010-1234-5678",
                                    "address": "인천"
                                },
                                {
                                    "age": 19,
                                    "name": "이길동",
                                    "phone_number": "010-1234-5678",
                                    "address": "서울"
                                },
                                {
                                    "age": 23,
                                    "name": "김동길",
                                    "phone_number": "010-1234-5678",
                                    "address": "강원"
                                }
                            ]
                          """
    let jsonData = jsonString.data(using: .utf8)
    do {
        guard let jsonData = jsonData else { return }
        let dataModel = try JSONDecoder().decode([User].self, from: jsonData)
        print(dataModel)
				/*
					 [
							{age 18, name "홍길동", phone "010-1234-5678", address "인천"}, 
							{age 19, name "이길동", phone "010-1234-5678", address "서울"}, 
							{age 23, name "김동길", phone "010-1234-5678", address "강원"}
						]
				*/
    } catch let error {
        print(error)
    }
}

decode()
  • extension 을 이용해 User 모델에 Decode를 적용시킨다
  • Decode의 경우 초기화를 우선 해주어야 하기 때문에 init을 통해 이를 수행한다
  • container 상수에 CodingKey를 적용시킨 Decode 를 수행 각 모델 내의 상수또한 해당 이름으로 파싱한다
  • jsonString 변수에 Json 형태의 데이터가 들어있으므로 이를 우리가 원하는 User 모델로 파싱
  • let dataModel = try JSONDecoder().decode([User].self, from: jsonData) 해당 구문을 통해 Json 형태의 데이터가 User 모델로 파싱된다.

⇒ 해당 구조는 공식문서에도 나와있는 형태이기 때문에 잘 기억하자!

Encode ( 모델 → 외부 데이터 ) 적용

// Encode
extension User {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(age, forKey: .age)
        try container.encode(name, forKey: .name)
        try container.encode(phone, forKey: .phone)
        try container.encode(address, forKey: .address)
    }
}

func encode() {
     let userObject = [
        User(age: 18, name: "홍길동", phone: "010-1234-5678", address: "인천"),
        User(age: 24, name: "김재우", phone: "010-1234-5678", address: "서울"),
        ]
        
     do {
         let jsonData = try JSONEncoder().encode(userObject)
         let jsonString = String(data: jsonData, encoding: .utf8)
         guard let printJsonString = jsonString else { return }
         print(printJsonString)
				/*
					 "[
							{"age":18,"name":"홍길동","phone_number":"010-1234-5678","address":"인천"},
							{"age":24,"name":"김재우","phone_number":"010-1234-5678","address":"서울"}
						]"
				*/
     } catch let error {
         print(error)
     }
}

encode()
  • 이번엔 Json 형태의 데이터를 우리가 만든 User 모델로 파싱하기 위해 Encodable을 사용해 보자
  • Decode와 과정이 비슷하지만 그 순서를 역순으로 하면 된다!
  1. Encoder 를 통해 Json 파일을 인코딩 하고
  2. 인코딩한 데이터를 String 형태로 변환해주고 ( 한글이 포함될 수 있으니 utf8 로! )
  3. 인코딩한 데이터가 nil 인지 파악한후! 사용하면 된다 ( 와! 정말 대단해! )

⇒ Encodable 과 Decodable 을 따로 쓰면 다소 복잡할 수 있지만, Codable 프로토콜을 채택 함 으로써 한번에 적용 시킬수 있다!!!!!

이제 우리가 하려고 하는 API 호출을 통해 원하는 데이터를 받아와 보자 ( 후.. 드디어!! )

+ Recent posts