ハッシュ – 剰余を使った例

考え方

以下のように考える。

  • 文字列を文字コード化し、眺めのバッファーに先頭から収めていく
  • バッファーが余った場合は、終わりまで0x00で埋める
  • バッファー全体を一つの整数として、特定の値で割った余りを求める
  • 余りの下位で適当なバイト数の値を取り出し、ハッシュ値とする

コード全体

上記の考え方を、クラスに実装してみる。テスト用なので、引数やオーバーフローなどのチェックはしていない。

この実装例では、バッファーの長さを16バイト、ハッシュのバイト数は下位8バイトとし、余りを計算するための除数は実行時に引数として与えるようにしている。

実行結果

除数で変わる結果

これに適当な文字列と除数を与えてみる。

1文字だけ変えると、値が大きく変わり、なんとか使えそう。

簡単な文字列で除数を変えて試してみると、除数を「ややこしそうな値」にするとハッシュはばらつくが、除数の設定によっては入力文字列そのままの値が出たりする。

除数→ 0xfedcba98 0xffffffff
A 9ED40708 41000000
AA 24FECD68 41410000
AB B81E0A58 41420000
AC 4C608CB0 41430000
BA EB7C9820 42410000
BB 7FBF1A78 42420000
BC 14019CD0 42430000

除数の影響

被除数の値によって、なぜハッシュ値が単純になるのか。

たとえば10進数で以下の様な例を考える。

 \begin{array}{rcrcrcr} 0000 & = & 99 & \times & 0 & + & 0 \\ 0100 & = & 99 & \times & 10 & + & 10 \\ 0200 & = & 99 & \times & 20 & + & 20 \\ & \vdots \\ 9000 & = & 99 & \times & 90 & + & 90 \\ 9100 & = & 99 & \times & 91 & + & 91 \\ & \vdots \\ 9800 & = & 99 & \times & 98 & + & 98 \\ 9900 & = & 99 & \times & 100 & + & 0 \\ \end{array}

除数未満の値が余りに出ているが、上位桁のパターンがそのまま順番に現れており、先の2進数と同じ結果となっている。

そこで、より簡単な例を考えてみる。

以下の表は、左の列が被除数で、それらに対して最上段の除数で割ったときの余りを示している。被除数の1/2の桁の除数では、7あたりが最も結果がばらつき、その他の値では単純な余りの値が繰り返される。

 \begin{array}{r|ccccccccc} & 9 & 8 & 7 & 6 & 5 & 4 & 3 & 2 & 1 \\ \hline 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 10 & 1 & 2 & 3 & 4 & 0 & 2 & 1 & 0 & 0 \\ 20 & 2 & 4 & 6 & 2 & 0 & 0 & 2 & 0 & 0 \\ 30 & 3 & 6 & 2 & 0 & 0 & 2 & 0 & 0 & 0 \\ 40 & 4 & 0 & 5 & 4 & 0 & 0 & 1 & 0 & 0 \\ 50 & 5 & 2 & 1 & 2 & 0 & 2 & 2 & 0 & 0 \\ 60 & 6 & 4 & 4 & 0 & 0 & 0 & 0 & 0 & 0 \\ 70 & 7 & 6 & 0 & 4 & 0 & 2 & 1 & 0 & 0 \\ 80 & 8 & 0 & 3 & 2 & 0 & 0 & 2 & 0 & 0 \\ 90 & 0 & 2 & 6 & 0 & 0 & 2 & 0 & 0 & 0 \\ \end{array}

これを細分化すると、余りの値の繰り返しはより顕著であることがわかる。

 \begin{array}{r|ccccccccc} & 9 & 8 & 7 & 6 & 5 & 4 & 3 & 2 & 1 \\ \hline 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 1 & 0 \\ 2 & 2 & 2 & 2 & 2 & 2 & 2 & 2 & 0 & 0 \\ 3 & 3 & 3 & 3 & 3 & 3 & 3 & 0 & 1 & 0 \\ 4 & 4 & 4 & 4 & 4 & 4 & 0 & 1 & 0 & 0 \\ 5 & 5 & 5 & 5 & 5 & 0 & 1 & 2 & 1 & 0 \\ 6 & 6 & 6 & 6 & 0 & 1 & 2 & 0 & 0 & 0 \\ 7 & 7 & 7 & 0 & 1 & 2 & 3 & 1 & 1 & 0 \\ 8 & 8 & 0 & 1 & 2 & 3 & 0 & 2 & 0 & 0 \\ 9 & 0 & 1 & 2 & 3 & 4 & 1 & 0 & 1 & 0 \\ \end{array}

先のコードは、上記の例で言えば除数を9に設定した場合に、その結果がきれいにもとの値と同じとなることに相当する。

衝突状況のチェック

除数を変化させて、アルファベット大文字1~4文字の文字列でハッシュ値の衝突が出るか確認してみた。各文字数に対するバリエーションの数は以下の通り。

  • 1文字→26通り
  • 2文字→26*26=676通り
  • 3文字→26*26*26=17,576通り
  • 4文字→26*26*26*26=456,976通り
  • 合計、475,254通り

その結果は次の通り。

0xFEDCBA98
アルファベット1~4文字で衝突なし。

0xFEDCBA9
まだ衝突なし。

0xFEDCBA
衝突が多数発生。みな2つの文字列で衝突。たとえば
0071BB5E : [‘SZQA’, ‘ZRZW’]
003C0D52 : [‘SZQB’, ‘ZRZX’]
00065F46 : [‘SZQC’, ‘ZRZY’]
00CF8DF4 : [‘SZQD’, ‘ZRZZ’]
除数の10進値は16,702,650と文字列のバリエーションより大きいが、衝突が出始めた。

0xFFFFFFFF
意外にも衝突なし。

0xFFFFFFF
衝突が多数発生。これも2つの文字列で重複。たとえば
05A5CA5A : [‘JZZX’, ‘ZZZW’]
05A5DA5A : [‘JZZY’, ‘ZZZX’]
05A5EA5A : [‘JZZZ’, ‘ZZZY’]
除数の10進値は268,435,455とかなり大きいが、単純なパターンの値ほど衝突が起こりやすいのかもしれない。

コード詳細

コンストラクタ__init__()

  • インスタンス生成時にハッシュを生成する文字列を受け取り、空のバッファーを準備
  • バッファーの長さを16バイト、ハッシュ値の長さを8バイトに設定
  • 引数で与えられた文字列から1文字ずつ取り出し、文字コードを追加
  • バッファー長までの余った部分に0を追加

内部用メソッドget_nbytes()

文字コードを追加し終わったときのバッファー長を計算する処理を独立させたメソッド。

ハッシュ値計算get_hash()

  • バッファーの内容を整数として、引数で与えられた除数から剰余を計算する
  • 計算結果の下位バイトを取り出して、ハッシュ値として返す

文字列化メソッド__str()__

インスタンスの文字列化メソッド。

 

ハッシュ – 簡単すぎる例

ハッシュ関数のイメージを掴むために、簡単な例を考えてみる。

まず、任意の文字列が与えられたとき、文字列中の全ての文字コードを加えた値を返す関数を考える。

この関数を以下のような文字列で実行すると、1文字加えたり入れ替えただけで結果が違ってくるのがわかる。

長い文字列で試してみると、1文字入れ替えると値が変わってくるが、1文字分の足し込むコードが変わるだけなので、大きな変化は期待できない。

この方法で致命的なのは、同種の文字を順序違いで並べても結果が同じになる点。

このやり方は、あまりに単純すぎて使えない。

 

Python3 – 文字列とリストの相互変換

文字列からリストへの変換

リストから文字列への変換

リストから文字列に変換するときに、区切り文字を指定できる。

区切りは2文字以上でもok。

 

Python3 – 文字を順に取り出す

数字やアルファベットの文字を順に取り出して使いたい時、stringモジュールを使うと便利。

 

2の補数

簡単なケース

正の数の表現

桁数が固定された2進数の加算を考える。たとえば4桁(4 bits)の場合に1ずつ加える。

 \begin{array}{cc} 0000 + 0001 = 0001 & (1)\\ 0001 + 0001 = 0010 & (2)\\ 0010 + 0001 = 0011 & (3)\\ 0011 + 0001 = 0100 & (4)\\ 0100 + 0001 = 0101 & (5)\\ 0101 + 0001 = 0110 & (6)\\ 0110 + 0001 = 0111 & (7) \end{array}

負の数の表現

一方、-1は0から1を引いた値として計算される。最初の計算だけ、上位桁から1を借りてこられるとして、以下の様な計算になる。

 \begin{array}{cc} 0000 - 0001 = 1111 & (-1)\\ 1111 - 0001 = 1110 & (-2)\\ 1110 - 0001 = 1101 & (-3)\\ 1101 - 0001 = 1100 & (-4)\\ 1100 - 0001 = 1011 & (-5)\\ 1011 - 0001 = 1010 & (-6)\\ 1010 - 0001 = 1001 & (-7)\\ 1001 - 0001 = 1000 & (-8) \end{array}

上の足し算の結果と下の引き算の結果をを見ると、以下のことがわかる。

  • 正の数(0~7)と負の数(-8~-1)の合計15個で、4ビットの全てのパターンが網羅されている
  • 正の数の最上位ビットは0、負の数の最上位ビットは1

引き算と補数の足し算

たとえば上の例で、正の数と負の数を加えてみる。

 \begin{array}{cl} 0101 + 1110 = 0011 & (5-2=3) \\ 0011 + 1001 = 1100 & (3-7=-4) \\ 1111 + 1100 = 1011 & (-1-4=-5) \end{array}

2の補数を計算したときに0からその数を減じているので、当然の結果ではある。いずれにしても、負数を2の補数で表現することで、減算を加算で代えることができる。

2の補数を得る方法

2の補数を得るのに、いちいち0から引かなくても、以下の方法で可能。

  1. 整数のビットパターンを反転
  2. 1を加える

4ビットの例では以下のとおり。

 \begin{array}{cc} 0001 \rightarrow 1110 \rightarrow 1111 & (-1)\\ 0010 \rightarrow1101 \rightarrow 1110 & (-2)\\ 0011 \rightarrow1100 \rightarrow 1101 & (-3)\\ 0100 \rightarrow1011 \rightarrow 1100 & (-4)\\ 0101 \rightarrow1010 \rightarrow 1011 & (-5)\\ 0110 \rightarrow1001 \rightarrow 1010 & (-6)\\ 0111 \rightarrow1000 \rightarrow 1001 & (-7)\\ 1000 \rightarrow0111 \rightarrow 1000 & (-8)\\ \end{array}

すなわち、固定長の整数の場合、ビット反転と加算機能があれば、減算を実現できる。

典型的なビット数の整数範囲

8ビット

byte型でよく見る値。

 \begin{array}{lllr} 0111\:1111 = &2^7 - 1 &=& 127 \\ 1000\:0000 = &-2^7 &=& -128 \end{array}

16ビット

short型でよく見る値。

 \begin{array}{lllr} 0111\:1111\:1111\:1111 = &2^{15} - 1 &= &32767 \\ 1000\:0000\:0000\:0000 = &-2^{15} &= &-32768 \end{array}

32ビット

一般的なint型で絶対値が約21億。

 \begin{array}{llr} 2^{31} - 1 &=& ‭2,147,483,647 \\ -2^{31} &=& -‭2,147,483,648‬ \end{array}

64ビット

long型、絶対値が約920京(!)

 \begin{array}{llr} 2^{63} - 1 &=& ‭9,223,372,036,854,775,807 \\ -2^{63} &=& -‭9,223,372,036,854,775,808‬ \end{array}

 

Python3 – 数値と文字列の相互変換

数値から文字列への変換

10進数(str関数)

str関数で引数の数値を文字列化。

整数の場合、桁数が多くてもそのまま文字列化される。

実数の場合は有効数値で丸められる。

浮動小数点で与えると、桁数が収まる範囲で固定少数表示の文字列になる。

2進数、8進数、16進数

組み込み関数を使う方法

bin、oct、hex関数を使った場合、プレフィックス’0b’、’0o’、’0x’がついた文字列になる。

format関数を使う方法

format関数の第2引数に基数に対応した文字を指定する。

第2引数で、桁数を指定して空いた上位桁を0で埋めることができる。

書式文字列を使う方法

書式文字列と%演算子を使っても、数値を文字列化できるが、2進数には対応していない。

文字列から数値への変換

10進数

整数(int関数)

int関数は整数の文字列を数値化するが、実数形式の文字列を与えるとエラーになる。

実数(float関数)

float関数は実数の文字列を数値化する。固定小数点/浮動小数点のどちらの表現でもかまわない。

2進数、8進数、16進数

int関数の第2引数で基数を指定する。

第2引数には2~36まで指定可能で、0~9とA~Zまで使った36進法まで変換可能。

‘0b’、’0o’、’0x’のプレフィックスを付けた文字列を変換するときは、第2引数を0にする。この場合、10進数も変換可能。

 

Python3 – 数値

int型

整数型はintで表される。

桁数が多くなってもint型として扱われる。

int型のチェック

float型

数値リテラルに小数点を付けるとfloat型として扱われる。float型はCやFORTRANの倍精度の精度。

‘/’による除算の結果は常にfloat型になる。

‘//’による除算では商が整数化される(整数除算と剰余を参照)。

 

Python3 – 書式設定~format

format関数

一つの数値・文字列の書式を設定する場合、format組み込み関数が使える。引数はリテラルでも変数でもよい。

formatメソッド

文字列に対するformatメソッドでも数値・文字列の書式を設定できる(書式設定の書き方は後述)。引数はリテラルでも変数でもよい。

formatメソッドは複数の数値・文字列を扱える。

位置引数として整数を指定し、繰り返して利用することが可能。

キーワード引数として文字列を指定可能。

リストを引数にするときは整数の位置引数とリスト内の引数で指定する。複数のリストを指定するときは位置引数を0、1…と対応させていく。

辞書を引数にするときは整数の位置引数とキーで指定する。キーはクォートで囲まない。

書式設定する場合は、[位置引数]:[書式設定文字列]の形式にする。位置引数を指定しない場合でも':'は必要。

書式設定文字列

formatメソッド

位置引数を省略して書式設定文字列を各場合{:[書式設定文字列]}のように':'を省略することができない。

文字列

{:[</^/>][width]}で左寄せ/中央ぞろえ/右寄せと、全体の幅widthを指定。

{:[char][</^/>][width]}で空白部分をcharの文字でパディング

整数

整数の書式は幅の値に'd'を続けて書く。

整数を2進数、8進数、16進数で表現できる。

基数を指定するとき、'#'を前置すると0b、0o、0xが前に付加される。以下の例では、同時に幅も指定している。2進数表現の場合は0bを付加すると設定幅を超えるので、そのまま表示されている。

浮動小数点

浮動小数点形式の実数の書式は、小数部の桁数だけ指定するときは".3f"のように、幅を設定するときは"10.3f"のように指定する。

固定小数点

固定小数点形式の実数の書式は、小数部の桁数だけ指定するときは".5e"のように、幅を指定するときは"15.5e"のように指定する。

符号の表示方法

正の数値に対して'+'を表示させるときは、数値書式の前に'+'をつけ、正の時にスペースを表示させるときには' 'をつける。

 

指定した幅の先頭に符号等を表示し、右寄せの数値との間をパディングする方法。

format関数

基数表示

数値の基数を指定して、10進数、2進数、8進数、16進数を表示。16進数は英文字の大文字/小文字を選択可能。

位置揃え

文字列や数値を固定幅の中で左寄せ、センタリング、右寄せ(センタリングで左右幅が異なるときは、左に1桁寄せられる)。

パディング

数値を固定幅で表示した場合のデフォルトは右寄せ。'[任意の文字]=’で空白をパディング。特にゼロで埋める場合は’=’を省略できる。

2、8、16の基数に対して固定幅で0でパディングする例。

+符号

‘+’を指定すると、正の値の時に+記号がつく。

カンマ区切り

3桁ごとのカンマ区切り指定。

固定小数点実数

固定小数点の小数部の桁数を’.[桁数]f’で指定する。

浮動小数点実数

浮動小数点の小数部の桁数を’.[桁数]e’で指定する。

実数全体の桁数

小数部の桁数ではなく全体の桁数を’.[桁数]g’で指定する。全体の桁数が整数部の桁数以上のときは、固定小数点で表示される。

 

Python3 – 整数除算と剰余(//, %)

//演算子

//演算子は、切り捨て除算と言われることが多いが、正確には小数部の切り捨てではなく、除算値を超えない最大の整数となる。これは実数の除算結果にfloorを適用した値と同じで、除算結果が負の時に注意を要する。

なお、被除数・除数の何れがマイナスかは問わず、計算結果がマイナスかどうかだけによる。

%演算子

%演算子は、引数の剰余(mod)を与える。除数の正負によって挙動が異なる点に注意。

除数が正のとき剰余は正となり、除数が負のときは剰余は負となる。

 \begin{array}{lcl} 13 \% 3 = 1 & \Rightarrow & 13 = 3 \times 4 + 1 \\ -13 \% 3 = 2 & \Rightarrow & -13 = 3 \times (-5) + 2 \\ 13 \% (-3) = -2 & \Rightarrow & 13 = -3 \times (-5) - 2 \\ -13 \% (-3) = -1 & \Rightarrow &  -13 = -3 \times 4 -1 \end{array}