二重配列のキー値の重複チェックを色々と考えてみた(おまけ付き)

公開日:2019-01-15
最終更新:2019-01-15

例えばこんな二重配列があるとします。

datas = [  
  ["佐藤", "33", "M"],  
  ["鈴木", "42", "S"],  
  ["伊藤", "22", "M"],  
  ["加藤", "49", "M"],  
  ["田中", "36", "S"],  
  ["伊藤", "73", "M"],  
  ["鈴木", "31", "M"],  
  ["佐藤", "36", "S"],  
  ["斎藤", "73", "M"],  
  ["鈴木", "31", "M"]  
  ]

よくある[名前,年齢,性別]って感じのリストですね。ここから重複している名前(佐藤さんとか鈴木さんとか)を抜き出したい時どうすればいいかを考えました。いくつかやり方があったのでまとめておきます。

重複チェック

処理① 配列をeach文で回し、キー値が複数あるかをtemp配列とincludeで管理する

  tmp1 = [] # 一度目に出てきた名前を格納する  
  result = []  
  datas.each{|data| # データごとにみてみる  
     if tmp1.include?(data[0]) # 一度出てきた名前かどうか確認  
       if !result.include?(data[0])  
         result.push(data[0]) # tmp1に存在するがresultに存在しない名前を結果として格納  
       end  
     else  
       tmp1.push(data[0]) # tmp1に存在しない名前を格納  
     end  
  }  
  p result.join(" ") # => "伊藤 鈴木 佐藤"

たくさんif文を使って読みづらい気がします。

処理② キー項目のみの配列を作成しグルーピングして調べる

  tmp2 = []  
  datas.map{|data| tmp2.push(data[0])} # 名前欄のみの配列を作成  
  p tmp2.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first).sort.join(" ") # => "伊藤 佐藤 鈴木"

細かく見てみると

⑴group_byにより同じ値をグルーピングしてHash化

p tmp2.group_by{ |e| e }  
# => {"佐藤"=>["佐藤", "佐藤"], "鈴木"=>["鈴木", "鈴木", "鈴木"], "伊藤"=>["伊藤", "伊藤"], "加藤"=>["加藤"], "田中"=>["田中"], "斎藤"=>["斎藤"]}

⑵Hashの値が1より大きいHashをSelect

p tmp2.group_by{ |e| e }.select { |k, v| v.size > 1 }  
# => {"佐藤"=>["佐藤", "佐藤"], "鈴木"=>["鈴木", "鈴木", "鈴木"], "伊藤"=>["伊藤", "伊藤"]}

⑶先頭の要素のみをまとめる

p tmp2.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first)  
# => ["佐藤", "鈴木", "伊藤"]

なるほど3行で済むようになりましたね。

処理③ キー値はtransposeでまとめて、グルーピングして調べる

p datas.transpose[0].group_by{ |e| e }.reject { |k, v| v.one? }.keys.sort.join(" ") # => "伊藤 佐藤 鈴木"

細かく見てみると

⑴transpose[0]でキー値のみを抽出

p datas.transpose[0]  
# => ["佐藤", "鈴木", "伊藤", "加藤", "田中", "伊藤", "鈴木", "佐藤", "斎藤", "鈴木"]

⑵グルーピングしたHashの中から一つだけ存在するものを削除する

p datas.transpose[0].group_by{ |e| e }.reject { |k, v| v.one? }  
# => {"佐藤"=>["佐藤", "佐藤"], "鈴木"=>["鈴木", "鈴木", "鈴木"], "伊藤"=>["伊藤", "伊藤"]}

⑶Keyのみを取り出す

p datas.transpose[0].group_by{ |e| e }.reject { |k, v| v.one? }.keys  
# => ["佐藤", "鈴木", "伊藤"]

色々やってはいるものの、1行で表現できました!

処理④ 配列をSortし、それぞれの要素について、次の要素と比較して同じなら重複とみなす

  tmp4 = []  
  datas.map{|data| tmp4.push(data[0])} # 名前のみの配列を作成  
  p tmp4.sort!.select.with_index{|e,i| e == tmp4[i+1] }.uniq.join(" ") # => "伊藤 佐藤 鈴木"

細かく見てみると

⑴sortした名前配列

p tmp4.sort!  
# => ["伊藤", "伊藤", "佐藤", "佐藤", "加藤", "斎藤", "田中", "鈴木", "鈴木", "鈴木"]

⑵名前配列で右隣の要素が自身と等しい値のみを抽出

p tmp4.sort!.select.with_index{|e,i| e == tmp4[i+1] }  
# => ["伊藤", "佐藤", "鈴木", "鈴木"]

⑶uniqにより3回以上出てくる名前の重複を削除

p tmp4.sort!.select.with_index{|e,i| e == tmp4[i+1] }.uniq  
# => ["伊藤", "佐藤", "鈴木"]

こんなやり方もあるんですね〜

昨日今日で思いついたり調べられたのはこんな感じですが、他にもあるのかな?

それぞれのやり方でベンチマークをとってみたよ(おまけ)

色々やり方があると、それぞれの実行速度に差があるか気になったので取ってみました。
データは1万件です。頑張ってコピペしました・・・こういうデータを作るのに簡単な方法とかあれば教えていただきたいです(他力本願

datas = [  
  ["一人目", "33", "M"],  
  ["鈴木", "42", "S"],  
  ["佐藤", "22", "M"],  
  ["加藤", "49", "M"],  
  ["田中", "36", "S"],  
〜〜〜〜中略〜〜〜〜  
  ["鈴木", "31", "M"],  
  ["百人目", "31", "M"],  
  ["小林", "33", "M"],  
〜〜〜〜中略〜〜〜〜  
  ["鈴木", "31", "M"],  
  ["千人目", "31", "M"],  
    ["加藤", "33", "M"],  
〜〜〜〜中略〜〜〜〜  
      ["鈴木", "31", "M"],  
      ["三千人目", "31", "M"],  
        ["加藤", "33", "M"],  
〜〜〜〜中略〜〜〜〜  
                    ["鈴木", "31", "M"],  
                    ["鈴木", "31", "M"],  
                    ["一万人目", "31", "M"]  
]

こいつを使って上の処理①〜④が終わるまでの時間をそれぞれ計測します。

ソース

require 'benchmark'  

datas = [ 中略 ]  

result1 = Benchmark.realtime do  
  tmp1 = [] # 一度目に出てきた名前を格納する  
  result = []  
  datas.each{|data| # データごとにみてみる  
     if tmp1.include?(data[0]) # 一度出てきた名前かどうか確認  
       if !result.include?(data[0])  
         result.push(data[0]) # tmp1に存在するがresultに存在しない名前を結果として格納  
       end  
     else  
       tmp1.push(data[0]) # tmp1に存在しない名前を格納  
     end  
  }  
  result.sort.join(" ")  
end  
puts "処理①  #{result1} 秒"  

result2 = Benchmark.realtime do  
  tmp2 = []  
  datas.map{|data| tmp2.push(data[0])}  
  tmp2.group_by{ |e| e }.select { |k, v| v.size > 1 }.map(&:first).sort.join(" ")  
end  
puts "処理②  #{result2} 秒"  

result3 = Benchmark.realtime do  
  datas.transpose[0].group_by{ |e| e }.reject { |k, v| v.one? }.keys.sort.join(" ")  
end  
puts "処理③  #{result3} 秒"  

result4 = Benchmark.realtime do  
  tmp4 = []  
  datas.map{|data| tmp4.push(data[0])}  
  tmp4.sort!.select.with_index{|e,i| e == tmp4[i+1] }.uniq.sort.join(" ")  
end  
puts "処理④  #{result4} 秒"

結果

初めてnumbersを使ってまとめてみました。Excelに慣れてるとめっちゃ使いづらいですね・・・

10回の平均値
処理① 4.259E-03
処理② 3.549E-03
処理③ 2.829E-03
処理④ 5.022E-03

これをみると、処理③tranposeを使って配列をまとめた後にチェックする方法が一番早いようです。
逆に隣り合う要素を比較する処理④は遅めみたいですね。

とまぁ結果を出してみたはいいのですが、正直1万件のデータじゃ少ない気がしますし、9回目の処理①がめっちゃ時間かかってるみたいにだいぶ誤差が出てきちゃってる気がします。多分PCの稼働状況とかで処理が安定してないっぽいです。そういう意味でこのベンチマークはあまり役に立たないかも・・・おまけ要素的なものとして扱いたいです。
一応処理3と処理4だとかかる時間の差は1.5〜2倍ある、処理3が一番早いなどの傾向は信頼できそうですね。

ちなみに、require ‘benchmark’ でBenchmarkモジュールを使えるようにできるとのことです。
詳細はRuby でベンチマークを取る方法でどうぞ。

記事が少しでもいいなと思ったらクラップを送ってみよう!
0
+1
作ったもの、プログラミングで共有できそうなこと、IT関連でやってみたことなど、なんでも書いていきます。ヤドン可愛い

よく一緒に読まれている記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる

技術ブログをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

技術ブログを開設する

Qrunchでアウトプットをはじめよう

Qrunch(クランチ)は、ITエンジニアリングに携わる全ての人のための技術ブログプラットフォームです。

Markdownで書ける

ログ機能でアウトプットを加速

デザインのカスタマイズが可能

技術ブログ開設

ここから先はアカウント(ブログ)開設が必要です

英数字4文字以上
.qrunch.io
英数字6文字以上
ログインする