─ 問題 ─

ASCII文字群を格納した配列Aと、各ASCII文字に対応した上下反転文字群を格納した配列Bがある。

# encoding: UTF-8
A = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", ",", "-", ":", ";", "!", "?", "&", "(", ")", "<", ">", "[", "]", "_", "‾", "{", "}", "∴", "∵", " "]

B = ["ᗄ", "ᗺ", "Ɔ", "ᗡ", "Ǝ", "Ⅎ", "⅁", "H", "I", "ᒋ", "丬", "⅂", "W", "N", "O", "Ԁ", "Ό", "ᴚ", "S", "⊥", "Ո", "Λ", "M", "X", "⅄", "Z", "ɐ", "q", "ɔ", "p", "ə", "ɟ", "ɓ", "ɥ", "ᴉ", "ɾ", "ʞ", "l", "ɯ", "u", "o", "d", "b", "ɹ", "s", "ʇ", "n", "ʌ", "ʍ", "x", "ʎ", "z", "0", "⇂", "Ƨ", "ε", "ᔭ", "5", "9", "L", "8", "6", "˙", "‘", "-", ":", "؛", "¡", "¿", "⅋", ")", "(", ">", "<", "]", "[", "‾", "_", "}", "{", "∵", "∴", " "]

AおよびBを使って、ASCII文字を対応する上下反転文字に変換するメソッドflip、およびその上下反転文字を元のASCII文字に戻すメソッドunflipを定義せよ。


─ 僕のやり方1 ─

Array#indexでAにおける文字の位置を求め、Array#atでBにおける対応位置の文字を取る。

flip.rb

def flip(chr)
  B.at A.index(chr)
end

def unflip(chr)
  A.at B.index(chr)
end

_B = A.map { |chr| flip chr }
_A = _B.map { |chr| unflip chr }

B == _B # => true
A == _A # => true

flipped = "Ruby is fun!".chars.map { |chr| flip chr }.join.reverse # => "¡unɟ sᴉ ʎqnᴚ"
flipped.chars.map { |chr| unflip chr }.join.reverse # => "Ruby is fun!"


─ 僕のやり方2 ─

Array#zipで対応文字のペアを作り、Array#assocで検索する。unflipではArray#rassocを使う。

T = A.zip(B) # => [["A", "ᗄ"], ["B", "ᗺ"], ["C", "Ɔ"], ["D", "ᗡ"], ["E", "Ǝ"], ["F", "Ⅎ"], ["G", "⅁"], ["H", "H"], ["I", "I"], ["J", "ᒋ"], ["K", "丬"], ["L", "⅂"], ["M", "W"], ["N", "N"], ["O", "O"], ["P", "Ԁ"], ["Q", "Ό"], ["R", "ᴚ"], ["S", "S"], ["T", "⊥"], ["U", "Ո"], ["V", "Λ"], ["W", "M"], ["X", "X"], ["Y", "⅄"], ["Z", "Z"], ["a", "ɐ"], ["b", "q"], ["c", "ɔ"], ["d", "p"], ["e", "ə"], ["f", "ɟ"], ["g", "ɓ"], ["h", "ɥ"], ["i", "ᴉ"], ["j", "ɾ"], ["k", "ʞ"], ["l", "l"], ["m", "ɯ"], ["n", "u"], ["o", "o"], ["p", "d"], ["q", "b"], ["r", "ɹ"], ["s", "s"], ["t", "ʇ"], ["u", "n"], ["v", "ʌ"], ["w", "ʍ"], ["x", "x"], ["y", "ʎ"], ["z", "z"], ["0", "0"], ["1", "⇂"], ["2", "Ƨ"], ["3", "ε"], ["4", "ᔭ"], ["5", "5"], ["6", "9"], ["7", "L"], ["8", "8"], ["9", "6"], [".", "˙"], [",", "‘"], ["-", "-"], [":", ":"], [";", "؛"], ["!", "¡"], ["?", "¿"], ["&", "⅋"], ["(", ")"], [")", "("], ["<", ">"], [">", "<"], ["[", "]"], ["]", "["], ["_", "‾"], ["‾", "_"], ["{", "}"], ["}", "{"], ["∴", "∵"], ["∵", "∴"], [" ", " "]]

def flip(chr)
  T.assoc(chr)[1]
end

def unflip(chr)
  T.rassoc(chr)[0]
end

_B = A.map { |chr| flip chr }
_A = _B.map { |chr| unflip chr }

B == _B # => true
A == _A # => true

flipped = "Ruby is fun!".chars.map { |chr| flip chr }.join.reverse # => "¡unɟ sᴉ ʎqnᴚ"
flipped.chars.map { |chr| unflip chr }.join.reverse # => "Ruby is fun!"


─ 僕のやり方3 ─

Array#zipで対応文字のペアを作り、Enumerable#find(または#detect)で検索する。

T = A.zip(B)

def flip(chr)
  T.find { |a,b| a == chr }[1]
end

def unflip(chr)
  T.find { |a,b| b == chr }[0]
end


_B = A.map { |chr| flip chr }
_A = _B.map { |chr| unflip chr }

B == _B # => true
A == _A # => true

flipped = "Ruby is fun!".chars.map { |chr| flip chr }.join.reverse # => "¡unɟ sᴉ ʎqnᴚ"
flipped.chars.map { |chr| unflip chr }.join.reverse # => "Ruby is fun!"


─ 僕のやり方4 ─

Hash.[]で対応文字のハッシュを作り、Hash#[]で検索する。unflipはHash#invertでデータを反転してから検索する。

T = A.zip(B)
H = Hash[ T ] # => {"A"=>"ᗄ", "B"=>"ᗺ", "C"=>"Ɔ", "D"=>"ᗡ", "E"=>"Ǝ", "F"=>"Ⅎ", "G"=>"⅁", "H"=>"H", "I"=>"I", "J"=>"ᒋ", "K"=>"丬", "L"=>"⅂", "M"=>"W", "N"=>"N", "O"=>"O", "P"=>"Ԁ", "Q"=>"Ό", "R"=>"ᴚ", "S"=>"S", "T"=>"⊥", "U"=>"Ո", "V"=>"Λ", "W"=>"M", "X"=>"X", "Y"=>"⅄", "Z"=>"Z", "a"=>"ɐ", "b"=>"q", "c"=>"ɔ", "d"=>"p", "e"=>"ə", "f"=>"ɟ", "g"=>"ɓ", "h"=>"ɥ", "i"=>"ᴉ", "j"=>"ɾ", "k"=>"ʞ", "l"=>"l", "m"=>"ɯ", "n"=>"u", "o"=>"o", "p"=>"d", "q"=>"b", "r"=>"ɹ", "s"=>"s", "t"=>"ʇ", "u"=>"n", "v"=>"ʌ", "w"=>"ʍ", "x"=>"x", "y"=>"ʎ", "z"=>"z", "0"=>"0", "1"=>"⇂", "2"=>"Ƨ", "3"=>"ε", "4"=>"ᔭ", "5"=>"5", "6"=>"9", "7"=>"L", "8"=>"8", "9"=>"6", "."=>"˙", ","=>"‘", "-"=>"-", ":"=>":", ";"=>"؛", "!"=>"¡", "?"=>"¿", "&"=>"⅋", "("=>")", ")"=>"(", "<"=>">", ">"=>"<", "["=>"]", "]"=>"[", "_"=>"‾", "‾"=>"_", "{"=>"}", "}"=>"{", "∴"=>"∵", "∵"=>"∴", " "=>" "}

def flip(chr)
  H[chr]
end

def unflip(chr)
  H.invert[chr]
end

_B = A.map { |chr| flip chr }
_A = _B.map { |chr| unflip chr }

B == _B # => true
A == _A # => true

flipped = "Ruby is fun!".chars.map { |chr| flip chr }.join.reverse # => "¡unɟ sᴉ ʎqnᴚ"
flipped.chars.map { |chr| unflip chr }.join.reverse # => "Ruby is fun!"


─ 僕のやり方5 ─

Hash.[]で対応文字のハッシュを作り、Hash#[]で検索する。unflipではHash#keyを使う。

T = A.zip(B)
H = Hash[ T ]

def flip(chr)
  H[chr]
end

def unflip(chr)
  H.key(chr)
end

_B = A.map { |chr| flip chr }
_A = _B.map { |chr| unflip chr }

B == _B # => true
A == _A # => true

flipped = "Ruby is fun!".chars.map { |chr| flip chr }.join.reverse # => "¡unɟ sᴉ ʎqnᴚ"
flipped.chars.map { |chr| unflip chr }.join.reverse # => "Ruby is fun!"


どのやり方を選ぶべきか

さて、ここまでデータにアクセスする5種類の方法を見てきましたが、どの方法を採用したらよいか迷います。あなたはどれですか?

可読性に大差はないので、ここでは速度を比較しその結果で選ぶことにします。

ベンチマークを取ります。

flip_benchmark.rb

require "benchmark"
require "benchmark/ips"
require_relative 'flip'


Lorem = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".chars

def echo(chr)
  chr
end

Benchmark.ips do |x|
  x.report('index'){ Lorem.each { |chr| unflip_i flip_i(chr)  } }
  x.report('assoc'){ Lorem.each { |chr| unflip_a flip_a(chr)  } }
  x.report('find') { Lorem.each { |chr| unflip_f flip_f(chr)  } }
  x.report('hash') { Lorem.each { |chr| unflip_h flip_h(chr)  } }
  x.report('hash_key') { Lorem.each { |chr| unflip_hkey flip_h(chr)  } }
  x.report('echo') { Lorem.each { |chr| echo echo(chr)  } }
end

文字群を反転し元に戻す処理を単位時間当たりの実行回数(ips:iteration per second)で比較します(要gem install benchmark_suite)。各メソッドのコストを見るため、文字をそのまま返すメソッドechoを追加しています。

さあ、結果は如何に。順位を予想してみて下さい。

・・・



% ruby flip_benchmark.rb     
Calculating -------------------------------------
               index        17 i/100ms
               assoc        17 i/100ms
                find         8 i/100ms
                hash         3 i/100ms
            hash_key        26 i/100ms
                echo       358 i/100ms
-------------------------------------------------
               index      178.4 (±2.8%) i/s -        901 in   5.053797s
               assoc      173.5 (±1.7%) i/s -        867 in   4.999209s
                find       83.8 (±1.2%) i/s -        424 in   5.058501s
                hash       35.7 (±2.8%) i/s -        180 in   5.040406s
            hash_key      265.1 (±1.1%) i/s -       1326 in   5.002699s
                echo     3615.9 (±0.7%) i/s -      18258 in   5.049681s

結果は、echo >> hash_key > index > assoc > find > hashという順になりました。hash_keyが最速、hashが最遅です。

え?hashが最遅?!まさか。

hashのコードをもう一度見てみます。

def flip_h(chr)
  H[chr]
end

def unflip_h(chr)
  H.invert[chr]
end

やーこれはいかん。

この実装だと#unflipが呼ばれる度にHash#invertが実行されてしまいます。invertの実行がコストになるのは想像がつきます。直します。

H = Hash[ T ]
Hi = H.invert

def flip_h(chr)
  H[chr]
end

def unflip_h(chr)
  Hi[chr]
end

さあ今度はどうですか。

% ruby flip_benchmark.rb     
Calculating -------------------------------------
               index        18 i/100ms
               assoc        17 i/100ms
                find         8 i/100ms
                hash       226 i/100ms
            hash_key        26 i/100ms
                echo       371 i/100ms
-------------------------------------------------
               index      182.0 (±1.6%) i/s -        918 in   5.046047s
               assoc      178.4 (±0.6%) i/s -        901 in   5.051709s
                find       84.5 (±1.2%) i/s -        424 in   5.017970s
                hash     2242.5 (±1.9%) i/s -      11300 in   5.040986s
            hash_key      266.3 (±1.5%) i/s -       1352 in   5.078433s
                echo     3725.7 (±2.0%) i/s -      18921 in   5.080765s

hash速っ!

ここでは2つのハッシュの初期生成コストを考慮していませんが、これだけhashが爆速なら「hash最高!即採用!」ということでよろしいのじゃないでしょうか。

まあ実際、この問題の答えとしてはなんでもいいんでしょうけどね…

関連記事:

RubyのコードをFlipして暗号化する?


電子書籍でRuby始めてみませんか?

M’ELBORNE BOOKS



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS