このページについて
このページは、paiza ラーニング内に開設されているコンテンツ「レベルアップ問題集」で取り扱われているプログラミング課題について、独自の見解を述べたものです。
見解については、paizaラーニングの規約に基づき、許可されている範囲でのみ公開していますが、その内容については paiza とは一切関係なく、また paiza の立場を反映したものではありませんのでご注意ください。
挑戦する課題
レベルアップ問題集のデータセット選択メニューから「文字列の出現率 (paizaランク C 相当)」を取り上げます。
以下は、問題公開 Web ページからの引用です。
問題
文字列が N 個与えられます。各文字列の出現回数を文字列の辞書順で出力してください。
入力される値
1 2 3 4 5 6 |
N S1 S2 S3 ... S_N |
- 入力値最終行の末尾に改行が1つ入ります。
- 文字列は標準入力から渡されます。
期待する出力
各文字列 S が辞書順になるように「半角スペース区切りで S とその出現回数 A 」を改行区切りで出力してください。
1 2 3 4 5 |
S1 A1 S2 A2 S3 A3 ... S_N A_N |
入力例1
1 2 3 4 5 6 |
5 bcd abc bcd bcd bcd |
出力例1
1 2 |
abc 1 bcd 4 |
考え方
N 回表示される単語のうち、重複する単語がいくつあるのか計算する問題のごく典型的な例となります。出現する文字列があらかじめわかっているのであれば、あらかじめそのテーブルを準備できますが、どのような文字列が出現するのか不明な場合には、辞書(Dictionary)を活用するのが早いと思われます。
Swift の場合、キー(
key)として与えた値が辞書に存在しない場合には
nil を返すようになっています。
なので、辞書に対して新たな担当をキーとして登録しようとした場合には、初回は
nil が戻ってきてしまいますので、その対策を取る必要があります。
しかし、Swift の辞書には「キーが登録されていない」場合、デフォルトの値( value)を返すことができる仕組みがありますので、それを使うことで記述をもう少し簡潔にすることが可能となります。デフォルトキーを使う場合には、辞書の戻り値はオプショナル型ではなく通常の型となるため、いくぶん操作が楽になります。
- subscript(_:default:)
Accesses the value with the given key. If the dictionary doesn’t contain the given key, accesses the provided default value as if the key and default value existed in the dictionary.
また、辞書に対して新たに単語を追加していく処理については、 reduce(into:_:) を使うことでさらに簡潔に記述することも可能です。
これらの方法を使った解答が、解答例1となります。 reruce(into:_:) を使わず forEach で処理させると解答例2のような感じとなります。
もう一つの方法は、新規単語の出現判断にオプショナルバインディングを使う方法です。
辞書の値( value)を直接加算する場合、このような操作は辞書に対する key を用いたアクセスに相当しますので、アンラップする必要もあります( key を使った辞書へのアクセスは、必ずしも対応する value が存在するとは限らないため、 value はオプショナル型で返されるようになっています)。
この点に注意して記述すれば、辞書への登録はさほど難しくないことだと思われます。
データの出力ですが、Swift の辞書は内部での保存順番が一意ではありません。つまり、アクセスする毎にデータの並びが変わるような作りとなっています。
これでは出力時に不便ですので、Dictionary のプロパティである
sorted を使い、辞書のデータを指定された方法でソートし、その結果をタプルの配列として出力させます。
- sorted(by:)
Returns the elements of the sequence, sorted using the given predicate as the comparison between elements.
出力は、 forEach を使い、配列内のタプルに対して順次アクセスし、指定された形式で出力させます。
解答例
解答例1
Dictionary の subscript(_:default:) と reduce(into_:_) を組み合わせた例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 1. 行末まで一度に読み込み // 2. 不要となる行頭を切り落とし // 3. String に変換し // 4. reduce を使い、辞書(Dictionary)に畳み込む // 5. 単語名で並びかえる // 6. 結果を表示する Array(AnyIterator { readLine() }) .dropFirst() .map{ String($0) } .reduce(into: [:]){ $0[$1, default: 0] += 1 } .sorted { $0.key < $1.key } .forEach { print($0.key, $0.value) } |
解答例2
辞書の追加部分に Dictionary の subscript(_:default:) を適用させた例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// 出現回数を保存する辞書 var count: [String:Int] = [:] // 1. 行末まで一度に読み込み // 2. 不要となる行頭を切り落とし // 3. String に変換し // 4. 各行を処理する // 4-1. 既に辞書に登録済みであれば、1加算する // 4-2. 新規に出現した単語であれば、該当するキーの辞書に1を代入する Array(AnyIterator { readLine() }) .dropFirst() .map{ String($0) } .forEach { s in count[s, default: 0] += 1 } // 出力結果の表示 // DIctionary は sorted メソッドがあるので、それを使う count.sorted { $0.key < $1.key } .forEach { print($0.key, $0.value) } |
解答例3
オプショナルバインディングを適用し、都度新規追加を判断する例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// 出現回数を保存する辞書 var count: [String:Int] = [:] // 1. 行末まで一度に読み込み // 2. 不要となる行頭を切り落とし // 3. String に変換し // 4. 各行を処理する // 4-1. 既に辞書に登録済みであれば、1加算する // 4-2. 新規に出現した単語であれば、該当するキーの辞書に1を代入する Array(AnyIterator { readLine() }) .dropFirst() .map{ String($0) } .forEach { s in // オプショナルバインディングを使って既出か判断 if let _ = count[s] { // アンラップしなければならない点に注意する count[s]! += 1 } else { // 辞書に新規登録する count[s] = 1 } } // 出力結果の表示 // DIctionary は sorted メソッドがあるので、それを使う count.sorted { $0.key < $1.key } .forEach { print($0.key, $0.value) } |