Ruby | 株式会社Altus-Five / 株式会社Altus-Five は、技術力で勝負するシステム開発会社です。 Sun, 01 Jun 2025 15:20:06 +0000 ja hourly 1 https://wordpress.org/?v=6.8.2 /wp-content/uploads/2025/01/cropped-favicon-32x32.png Ruby | 株式会社Altus-Five / 32 32 Rubyの配列(Array)を魔改造して、連想配列として使ってみた /blog/2018/05/07/associative-array/ /blog/2018/05/07/associative-array/#respond Sun, 06 May 2018 15:15:00 +0000 http://43.207.2.176/?p=313 可読性が高く、高速な「連想配列」 エンジニアであれば、「連想配列」のお世話になる機会は多いと思います。言語によりますが、任意のデータをkeyとして指定できる連想配列は、 コードの可読性を高く保ったまま、ニーズに合わせた柔 […]

The post Rubyの配列(Array)を魔改造して、連想配列として使ってみた first appeared on 株式会社Altus-Five.

]]>
可読性が高く、高速な「連想配列」

エンジニアであれば、「連想配列」のお世話になる機会は多いと思います。言語によりますが、任意のデータをkeyとして指定できる連想配列は、 コードの可読性を高く保ったまま、ニーズに合わせた柔軟なシステム実装をサポートしてくれる強力なツールです。

ユーザーとして連想配列を使う分には、普段あまり意識しませんが、 各言語に標準実装されている連想配列の利便性は、下記の2点を満たすことで支えられています。

  1. keyを引数とすることでvalueを取り出す(もしくは代入する)操作が簡便であること
  2. 上記の操作が、連想配列に格納済みのデータサイズによらず高速であること(重要!

今回は、普段お世話になっている「連想配列」について、あらためてその成り立ちや性質を見直してみたいと思います。 本記事(および 次回)は、

  • 「連想配列はどんな技術で実現されている?」疑問に思った方
  • 「自分で連想配列を実装してみたい」という物好きな方
  • 「平均検索速度が定数時間O(1)ってどういう意味なんだ」と疑問に思い夜も眠れない方

に捧げます。

連想配列は、配列ではない!

(みなさんの興味を惹くために、ちょっとラジカルな見出しをつけてみました。)

本記事では、配列と連想配列を 似たようなものとみなすことをやめ、その成り立ちを解剖する ことで、 連想配列(特にハッシュによるもの = ハッシュテーブル)がいかにありがたいツールであるかを明らかにしてみたいと思います。 なお、ここまでで御察しの通り、 筆者は連想配列、ハッシュテーブルが大好き です。 まだエンジニアリング経験の浅い学生時代、C++を中心にコーディングをしていたのですが、 C++11に unordered_map クラスとして連想配列が存在することを知らず、自前のハッシュテーブルを作ったりしていました。 要素へのアクセスが目に見えて高速化され、とても感動した覚えがあります。

連想配列と配列の対比

さて、連想配列と配列の違いについて詳しく見ていきましょう。 両者の共通点といえば、

  • データを格納するために、メモリを使うこと(あたりまえ)
  • key(もしくは添字)の指定によってデータにアクセスすること

であり、逆にいえば それくらいしか共通点はない 、と筆者は認識しております。 そのわずかな接点についても、下記のような違いがあり、むしろこの相違点が「配列と連想配列の違いである」と理解されている方も多いかと思います。

point配列連想配列
確保するメモリ領域の広さ任意、もしくは言語ごとの実装により動的確保言語ごとの実装による
指定するkey(添字)確保するメモリ領域の先頭からのオフセット(ゆえに整数に限る)任意(整数や文字列など)

ですが、実は上記のちがい以上に、 key(添字)・valueの組による値の参照/代入の過程 が、大きく異なっている点が両者の違いの本質です。 以下では、簡単な実験的実装をテーマに、両者のロジックの中身がどう異なるかを明るみにしてみたいと思います。

Rubyの配列を(無理やり)連想配列にしてみよう

なお、Rubyの連想配列クラスはHashクラスですが、これはハッシュ関数を用いて実装されたハッシュテーブルであることを意味します。 今回は、連想配列の定義を、

  • keyに対するvalueを記憶させることができる (array["altus"] = 5)
  • keyを渡すと、記憶されているvalueを取得できる (p(array["altus"]) #=> 5)
  • keyに対するvalueを上書きすることが出来る (array["altus"] = 50 #"altus"に対応するvalueを50に上書き)

であるとし、Hashクラスとは別の 「粗悪な」 連想配列を作ってみたいと思います。

「粗悪な」連想配列を実装してみる

まずは、下記ソースコードをご覧ください。

# 配列を連想配列化する(強引な)実装

class MyAssociativeArray < Array
  def []= (k, v)
    found = false
    self.each {|item|
      if item[0] == k then
        item[1] = v
        found = true
        break
      end
    }
    if not found then
      self.push([k, v])
    end
  end

  def [] (query)
    found = false
    value = nil
    self.each {|item| #線形走査
      if item[0] == query then
        found = true
        value = item[1]
        break
      end
    }
    value
  end
end
view rawassociative_array.rb hosted with ❤ by GitHub

上記コードは、RubyのArrayクラス(配列クラス)を継承し、MyAssociativeArrayクラスを生成しています。 要素取り出し []と代入演算子[]=をオーバーロードすることで、 arr["altus"] = 5 や p(arr["altus"]) といったアクセスが実現。 MyAssociativeArrayクラスを 連想配列として用いる ことに成功していることがわかりますね。

ほか、値の上書きや、存在しないkeyに対してnilを返すといった実装にも成功しており、 Arrayクラスをベースに、とりあえず連想配列として使える実装ができたことがわかります。

どうやっているのか、以下ソースコードの解説をしていきます。

RubyのArrayクラスを継承・メソッドをオーバーロードして連想配列にするまで

代入メソッド([]=)の実装

まず、Arrayクラスを継承し、MyAssociativeArrayクラスを定義します。

class MyAssociativeArray < Array
view rawassociative_array.rb hosted with ❤ by GitHub

そのため、MyAssociativeArrayクラスは、Arrayクラスの性質を受け継いでいます。 Rubyでは組み込みクラスのメソッドを上書きできるので(シンタックスシュガーの一種)、 「手軽かつ、パッと見わかりやすく、性能が悪いので反面教師にもなる」オリジナル連想配列が作れるのではないか、 というのが本記事のモチベーションでした(余談)。

さて、肝心のメソッド上書きの内容です。 はじめに「値の代入」に取り掛かりました。そのためにオーバーロードする必要のあるクラスメソッドは []=です。 このメソッドは第一引数に添字(key)、第二引数に値(value)を取ります。

今回の実装は、配列の要素としてkey, valueの組(2つの要素からなる配列)をもたせてしまおう という原始的なもので、 第一要素を 線形走査 することで、お目当ての要素にアクセスしたり、値を上書きすることができます(この点が 粗悪 たる所以です。詳しくは後述)。

代入すべきkey, valueが与えられた場合、まずは過去に格納したデータの中に、同一keyの値が存在しないかどうかを調べます。

    self.each {|item|
      if item[0] == k then
        item[1] = v
        found = true
        break
      end
    }
view rawassociative_array.rb hosted with ❤ by GitHub

もし存在すれば、該当する要素のvalueを上書きし(上記L9)、 存在しなければ、key, valueの組を新しく配列に格納します(下記コード)。

    if not found then
      self.push([k, v])
    end
view rawassociative_array.rb hosted with ❤ by GitHub

蛇足

第一要素がkey, 第二要素がvalueとみなす実装は、暗黙的な悪い実装なので、 連想配列の中身を連想配列で定義したくなりますが、もちろん今回は Hashクラスの利用は禁じ手です。

さて、これで値の代入ができるようになりました。

参照メソッドの実装

次は値の参照です。こちらも線形走査をする点は変わらないので、一気に紹介してしまいます。

  def [] (query)
    found = false
    value = nil
    self.each {|item| #線形走査
      if item[0] == query then
        found = true
        value = item[1]
        break
      end
    }
    value
  end
view rawassociative_array.rb hosted with ❤ by GitHub

たとえば p(arr["altus"])とすれば、arr において ”altus” keyに対応する要素を返すので、 格納されているvalue、例えば 5が出力されるといった具合です。 もし該当するkeyの要素が存在しない場合は nilを返します。

代入・参照に線形時間かかる連想配列 – 上記実装は何が悪いのか?

さて、ここまで度々(しつこく?)触れてきた、上記連想配列の「粗悪さ」について解説してみたいと思います。

ここまでの実装をイメージ化すると、下図のようになります。

(代入・参照とも仕組みは同じなので、一つの図で済ませてしまいました。)

たとえばarr["altus"]のように、keyに対応するvalueを参照する処理や、 arr["altus"] = 5のような代入処理の際に、上図で表されるような処理、 すなわち「既存のkey一つひとつに対する、与えられたkey(query)との等値判定」を行って、 等値なものが見つかったタイミングでvalueを返したり、代入したりといった処理を行います。

これがいわゆる 線形走査 であり、において、データサイズnに対して比例する時間、すなわちO(n)の時間がかかります(最悪値評価)。

ちなみに、参照・代入における最悪ケースは何かと言うと、「まだ連想配列内に存在しないkeyをqueryとして投げた場合」 です。 非常によくあるケースが最悪値なので、この性能の悪さは実用上の障害になり、 ある程度データサイズが大きくなると、上記の連想配列はほとんど役に立ちません。

では、どうすれば性能の良い連想配列を得られるのでしょう… という問いへの答え(の一つ)が、 次回の記事でご紹介する「ハッシュテーブル」です。 ご期待ください!

参考資料

The post Rubyの配列(Array)を魔改造して、連想配列として使ってみた first appeared on 株式会社Altus-Five.

]]>
/blog/2018/05/07/associative-array/feed/ 0
「ハッシュ」完全理解のための覚書 ハッシュテーブルをRubyで実装してみる /blog/2018/05/07/hashtable/ /blog/2018/05/07/hashtable/#respond Sun, 06 May 2018 15:04:00 +0000 http://43.207.2.176/?p=309 「ハッシュ」を完全理解するための道とは 前回、エンジニアが常々お世話になる「連想配列」についてのあれこれを述べました。Rubyのシンタックスシュガーを使って、 配列を連想配列に魔改造することで、どんな性能の連想 […]

The post 「ハッシュ」完全理解のための覚書 ハッシュテーブルをRubyで実装してみる first appeared on 株式会社Altus-Five.

]]>
「ハッシュ」を完全理解するための道とは

前回、エンジニアが常々お世話になる「連想配列」についてのあれこれを述べました。Rubyのシンタックスシュガーを使って、 配列を連想配列に魔改造することで、どんな性能の連想配列になるかを確かめる ことが前回の内容でした。

今回は、連想配列実装の正式な(?)方法、ハッシュテーブルを実際に作ってみよう という試みです。粗悪な性能の連想配列だけでなく、実用性の高い連想配列も、さほど長くないコードで実現できるんです。

Rubyでの実装を通じて「ハッシュ」を(なんとなくではなく)理解しよう、というのが本記事のゴールになります。

注釈 ハッシュの完全理解とは言いつつも、今回 暗号化の話には触れない ので、その旨ご注意頂ければ幸いです。 ただし、暗号化についても今回の話と基本は同一ですので、理解の助けになるはずです。

実装が理解への近道だ

筆者は学生時代にハッシュテーブルを自作して、その性能の良さ(要素へのアクセスの機敏さ)に感動した人間であることを 前回 述べました。

ハッシュテーブルについて書かれた(まともな)文献については、「ハッシュ」と名のつく用語が複数出てきます。ハッシュ関数、ハッシュ値、などなど。

その関連性については色々な喩えがあるものの、 keyの成績や身長などが引き合いに出され、 それ内部ロジックどーなってんのよ? と言いたくなるような例が用いられたりしており、 「とりあえず」の理解の助けにはなるものの、エンジニアにとっては少し物足りない記述になっているように思えます。

本記事の内容は、下記の2つの項目について理解するにあたっては、なかなか良い内容なのではないかと自負しています。

  1. ハッシュ関数は、文字列など仕様によって規定されるいかなるkeyも入力できる形式でなければならない
    • その役割はハッシュ値を発行し、メモリ上の要素の格納場所を決めることである
  2. 異なるkeyに対するハッシュ値は、可能な限り重複がないことが望ましい
    • そのような性質を満たす「よい」ハッシュ関数のうち、極めてシンプルな実装のものも存在する(後述)

また、本記事では 下記3つの用語について、明確に区別すること をゴールとして、ハッシュテーブルの仕組みについてまとめていきます。

  1. ハッシュ関数
  2. ハッシュ値
  3. ハッシュテーブル(ハッシュ)

最後に、ハッシュテーブルの性能について書かれた文献を読むと、たいてい 「定数時間O(1)で要素にアクセスできる(ゆえに早い、最高だ!)」 と書かれています。 一体これがどういうことなのかも、分かりやすい解説をあまり見かけたことがありません。この点にも、チャレンジしてみたいと思います。

ハッシュ関数の設計

ハッシュテーブルの実装に必要な 最重要のメソッド がハッシュ関数です。

ここでは、文字列をkeyとするハッシュテーブルをユースケースとして想定し、 ハッシュ関数をRubyのメソッドとして、一から実装してみたいと思います。

ハッシュ関数の役割 – メモリ上のどこに要素を格納するか決める

早速前回の内容を引くのですが、 配列を強引に連想配列化した MyAssociativeArrayの実装では、 「メモリ上のどこにkey, valueの組を格納するか?」という問題については触れてきませんでした。 ここで、前回実装した連想配列クラス、 MyAssociativeArrayにおける要素代入のメソッドを見直してみましょう。


  def []= (k, v)
    found = false
    self.each {|item|
      if item[0] == k then
        item[1] = v
        found = true
        break
      end
    }
    if not found then
      self.push([k, v])
    end
  end
view rawassociative_array.rb hosted with ❤ by GitHub

ポイントは、MyAssociativeArrayクラスの継承元であるArrayクラスのpush関数を使ってkey, valueの組を格納している点です。 push関数は配列の末尾に要素を追加するメソッドなので、MyAssociativeArrayでは、新しく到着するkey, valueの組を既存要素の末尾に付け足していくという、ナイーブな方法でメモリ内に格納しています。

ハッシュテーブル実装の第一のポイントは、 メモリ内のどこに要素を格納するか? について、もう少し計画的なやり方を用いることです。

その際に登場するのが、先述した ハッシュ関数 です。

ハッシュを噛ませた場合の実装イメージ(要素格納の方法)を上図に表してみました。

データ代入の手続き

  1. あらかじめメモリ上の決まった広さの領域を確保しておく
    • 本記事ではテーブルサイズと呼ぶこととします
  2. key, valueを受け取ったら、ハッシュ関数 にkeyを入力
  3. ハッシュ値 を得て、1.で確保した領域のサイズTABLESIZEで割った値 offset を得ることで、メモリ上のどこに格納するかを決める

それぞれ、実際にハッシュテーブルを実装したコード(hashtable.rb)を参照しつつ、解説していきたいと思います。

今回、Arrayオブジェクトをインスタンス変数として持つMyHashTableクラスを実装することで、ハッシュテーブルの実装をしてみたいと思います。 まず、1.で述べた「決まった広さの領域を確保する」ことからです。今回は下記のように行います:

  def initialize()
    @arr = Array(TABLESIZE)
  end
view rawhashtable.rb hosted with ❤ by GitHub

あらかじめ定めた定数 TABLESIZE の要素数をもつ配列(Arrayオブジェクト)を生成し、インスタンス変数 @arr として保持しているだけの単純なコードです。 これで、メモリ上にkey, valueの組を保持するための領域が確保されます。 ( TABLESIZEの適切な決め方については、後ほど触れたいと思います。)

次に、代入処理を見てみましょう。(上述した流れの2, 3にあたる部分です。) 代入処理ではまず、確保した配列領域のどの位置にkey, valueの組を格納するかを決定します。配列の先頭から要素いくつ分後ろに格納するかを表すoffset変数を用意し(0オリジン)、ここに数値を代入します。 (もちろん、 0 <= offset < TABLESIZEを満たすような値を与える必要があります。)

さて、肝心のoffsetの数値は下記コードで決定されています。

  def []= (k, v)
    offset = hash_function(k) % TABLESIZE
view rawhashtable.rb hosted with ❤ by GitHub

右辺のMyHashFunctionクラスのone_at_a_timeメソッドを呼び出し、引数としてk(key)を渡しています。それをTABLESIZEで割った値をoffsetとして採用しています。

ここで、このone_at_a_timeこそが ハッシュ関数 と呼ばれるものです。 One at a timeハッシュは、質の良いハッシュ関数について論じたJenkinsの文献(英語)に記載されている中で、もっとも古典的なハッシュ関数です。 シンプルなアルゴリズムで動作の理解が易しいこと、デモとしては十分な性能があることから、今回の目的にピッタリと判断しました。動作を理解するのに必要な知識は ビット演算のみ ですので、不慣れな方は、ビット演算に関する文献を片手に読めば十分理解できるのではないかと思います。

one_at_a_timeハッシュ関数の中身をみてみましょう。


  def one_at_a_time(key) #keyは文字列を想定
    hash = 0
    key.each_char {|ch| #文字を取り出す
      hash += ch.ord
      hash += (hash << 10)
      hash ^= (hash >> 6)
    }
    hash += (hash << 3)
    hash ^= (hash >> 11)
    hash += (hash << 15)
    hash %= (2**30)
    # 0 <= hash < 2**30 の範囲で扱えるような数値に補正
    # FixNum クラスの数値範囲による
  end
view rawhash_function.rb hosted with ❤ by GitHub

ここで、上記コードは下記4つのステップで説明できます。

  1. key文字列の先頭から1文字取り出し、文字コード化し、二進数化する(L7)
  2. 1.で得られた値をhash変数に加える(L7)
  3. hash変数の値をビット演算でいい感じにかき混ぜる(L8-L9)
  4. 以下、文字列の末尾文字まで繰り返し
  5. 得られた値を最後にもう一度かき混ぜて出力(L11-L14)

例えば、このハッシュ関数に”altus”というkeyを入力してみると、0x25746a3bという出力値が返されます。この数値を ハッシュ値 と呼びます。

ハッシュ値をTABLESIZEで割った値をoffsetとして採用することで、配列内のどの位置に要素を格納するかを決定します。

このようにしてoffsetを確定したら、その位置にkey, valueの組を格納します。 ただし、最初の実装と同様、「既にそのkeyをもつ要素が格納されていれば、値を上書きする」よう実装する必要がありますので、ロジックは若干長くなります。 下記コードのL21が、肝心の代入を行なっている箇所です。

    if @arr[offset].nil? then
      @arr[offset] = Array.new().push([k, v])
    else
      found = false
      @arr[offset].each {|e|
        if(e[0] == k)
          found = true
          e[1] = v
          break
        end
      }
      if not found
        @arr[offset].push([k, v])
      end
    end
view rawhashtable.rb hosted with ❤ by GitHub

ハッシュ関数に求められる2つの性質

  1. 上記手続きには一切ランダム性がなく、同一のkeyを与えた時は必ず同じ出力値を返すこと(入力に対する出力の一意性)
  2. 異なるkeyを与えたとき、同じ出力値を返す可能性が極めて低いこと

ハッシュテーブルにおける値の参照 – keyのハッシュ値で格納場所がわかる

代入の次は参照です。最初の実装では線形走査でkeyにヒットする要素を見つけていましたが、今回は代入の方法が変わったため、参照の方法が劇的に効率的になります。 ポイントは明快で、 参照にもkeyのハッシュ値を使えば良い のです。

今回、代入時の要素格納を、keyのハッシュ値に基づいて行うようロジックを変更しました。そのため、要素参照時にもハッシュ値を発行し、ハッシュ値の指す位置を調べることで、与えられたkeyをもつ要素が、既に連想配列内にあるかどうかを高速に調べることができるのです。 実装方針としては、下記の通りです。

  1. keyに対応するハッシュ値からoffsetを求める
  2. offsetの指す位置に、与えられたkeyを持つ要素が格納されているかどうかを調べる

「keyに対応するハッシュ値からoffsetを求める」コードは下記の通りで、代入の時と全く同一です。

    hash_value = hash_function(k)
    offset = hash_value % TABLESIZE
view rawhashtable.rb hosted with ❤ by GitHub

「offsetの指す位置に、与えられたkeyを持つ要素が格納されているかどうかを調べる」コードは下記の通りです。

    value = nil
    if @arr[offset].nil?
      puts("not found")
    else
      found = false
      @arr[offset].each {|e|
        if e[0] == k
          found = true
          print("found: ", e[1], "\n")
          value = e[1]
        end
      }
      if not found
        puts("not found")
      end
    end
    puts()
view rawhashtable.rb hosted with ❤ by GitHub

まとめると、ハッシュテーブルの実装上の工夫、およびその結果としての性能の良さ(高速さ)は、下記の文章で言い表すことができます。

  • ハッシュテーブルでは、値の代入と参照を同じ手段(ハッシュ関数)を介して行うため、 1回 の等値判定で指定のkeyを持つ要素が見つかる
  • 同様の理由から、指定のkeyをもつ要素が連想配列内に存在しない場合は、 1回 の等値判定でそのことがわかる
    • ただし、いずれも例外あり(後述する『衝突』が起きた場合)

このことが、 ハッシュテーブルは定数時間O(1)で値の参照/代入が可能である という文言の意味するところなのですね。

衝突(collision)とは? – ハッシュテーブルの性能が劣化するケース

さて、上記ソースコードを載せつつさらりと流しましたが、性能に関して見過ごせない点があったことにお気づきだったでしょうか。 「ハッシュテーブルは1回の等値判定で参照/代入ができる!」などと言いつつ、 実は今回も、 参照/代入の実装において線形走査(each)を使っていた のですね。

代入

      @arr[offset].each {|e|
        if(e[0] == k)
          found = true
          e[1] = v
          break
        end
      }
view rawhashtable.rb hosted with ❤ by GitHub

参照


      @arr[offset].each {|e|
        if e[0] == k
          found = true
          print("found: ", e[1], "\n")
          value = e[1]
        end
      }
view rawhashtable.rb hosted with ❤ by GitHub

さて、こちらは 衝突(collision)に対する処理 と呼ばれるもので、ハッシュテーブルに実装について書かれた文献であれば、必ず言及されている重要な処理です。

衝突とは、 相異なる複数の入力キーに対して、同じハッシュ値が発行されること、もしくは配列内の同じ位置(offset)が格納場所として指定されてしまうこと を指します。

One-at-a-timeハッシュは、古典的ながらもよく考慮されたハッシュ関数ですので、相異なる複数の入力キーに対して同じハッシュ値を発行してしまうことは稀ですが、 その出力値は 0以上2^31未満と幅が大きいことから、今回は実用上TABLESIZEを指定し、One-at-a-timeの出力値をTABLESIZEで割った値を用いることで、値を0以上TABLESIZE未満に補正して用います。

今回の実装では、TABLESIZE52291を指定していましたので、ある程度大きな要素数になると、かなりの高確率で衝突が生じることになります。

ハッシュテーブルのサイズに関する余談

この時TABLESIZE365と指定してみると、 有名な「誕生日のパラドックス」と同じ問題設定になります。 「何人集まれば、その中に誕生日が同一の2人(以上)がいる確率が、50%を超えるか?」という問題に対する答えを問うもので、 その答えは 「23人」 であるという、意外な結論が有名です。 この興味深い結果をハッシュ値の文脈に載せてみると、

前提:TABLESIZE365に指定し、理想的なハッシュ関数を用いたとする

主張:keyについてランダムに要素を追加する場合、 要素数が「23」に達するまでに1回以上の衝突が起きる確率は50%以上 である

と読み換えることができ、誕生日のパラドックスは上記の主張が「正しい」ことを示す、技術的にもおもしろい(厄介な)帰結だということが分かります。

さて、話が逸れましたが、TABLESIZEをどんなに大きくした場合でも、衝突の起きる確率は0にはなりませんので、プログラム上はその際の処理を書いておく必要が生じます。 それが、先ほど挙げた参照/代入のロジックにおけるeach文の役割だったのですね。

以下、その内容を再掲・解説しておきますので、ご興味のある方はコードを解読してみてください。

代入における衝突対策ロジック

  def []= (k, v)
    offset = hash_function(k) % TABLESIZE

    if @arr[offset].nil? then
      @arr[offset] = Array.new().push([k, v])
    else
      found = false
      @arr[offset].each {|e|
        if(e[0] == k)
          found = true
          e[1] = v
          break
        end
      }
      if not found
        @arr[offset].push([k, v])
      end
    end
  end
view rawhashtable.rb hosted with ❤ by GitHub
  1. 新規のkeyに対するoffsetの位置に既存の要素が存在するかどうかを確かめる(L14)
  2. 存在しない場合は単純に要素を格納すればOK
    • ただし、今後衝突が起きた時のため配列を作っておき、いま代入したいkey, valueはその先頭要素として格納しておく(L15)
  3. 存在する場合は、既存keyの再入力(keyに対する値の上書き)か、異なるkeyに対するoffsetの重複(衝突)かを判定する(L18-L19)
  4. keyに対する値の上書きの場合は、上書き処理を行う(L21)
  5. 衝突の場合は、以前に入力されたkey, valueにおいて、上記2.の手続きで配列を準備してあるので、その末尾にいま入力したいkey, valueをpushする(L26)

参照における衝突対策ロジック

    value = nil
    if @arr[offset].nil?
      puts("not found")
    else
      found = false
      @arr[offset].each {|e|
        if e[0] == k
          found = true
          print("found: ", e[1], "\n")
          value = e[1]
        end
      }
      if not found
        puts("not found")
      end
    end
    puts()
    value
  end
view rawhashtable.rb hosted with ❤ by GitHub
  1. クエリとして投げられたkeyに対するoffsetの位置に要素が存在するかどうかを判定する(L39)
  2. 存在しない場合、nilを返す
  3. 存在する場合、@arr[offset]の要素に対して「クエリ」と「格納されているkey」との等値判定を行い、求める要素があるかどうかを調べる
    • ここで@arr[offset]内に複数要素が存在するのは、過去に衝突が起きた場合である。多くの場合、@arr[offset]の要素数は1であるため、eachループは1回しか回らない

まとめ

今回、コードを読みやすく、保守・拡張しやすくしてくれる「連想配列」について、その原理に迫ってみました。 定数時間O(1)で要素へのアクセスを可能にする実装の一つに「ハッシュテーブル」があり、その構成要素として「ハッシュ関数」が重要な役割を果たしていることを、Rubyで実際に連想配列を実装してみることでおさらいしてみました。 前回の冒頭で述べた「連想配列は配列ではない」という(物騒な)主張は、本記事でまとめた内容をゼミで学んで以来、実装と実験によって体感できた実体験に基づいています。 インタフェースが一見同じでも、内部の仕組みは随分違うし、そのことに助けられることがあるんだなぁ…と感動したことを覚えています。

普段ツールとして見ている/使っている実装について、 内部の仕組みがわかるといちいち感動できる 、そんな経験が広がっていけば、プログラミングはますます深く、楽しくなるのではないでしょうか。 (綺麗にまとめてみました!)

参考資料

The post 「ハッシュ」完全理解のための覚書 ハッシュテーブルをRubyで実装してみる first appeared on 株式会社Altus-Five.

]]>
/blog/2018/05/07/hashtable/feed/ 0