ハッシュ法

ハッシュ関数 $ H(x) $ には

が要求されます。が、要求されるだけで確実に満たすものを作るのは困難です。

ハッシュ関数にはよくMod演算(余りを求める)が使われます。
例) $ H(x) = x \space Mod \space 25 $
理由は、2番目の条件を満たすためです。
配列が25個あれば、とりあえずデータを25で割ったあまりを求めておけば0~24の配列のインデックス範囲内に収まります。

こうすることで、値xがデータ配列のどのインデックスの位置に保存されているかダイレクトにわかることになります。

すなわち、データx は データ配列 Data[] の Data[H(x)](Data[H(x)] = x)に格納されるということになります。

被りがない時の理想的なハッシュテーブル

#include <iostream>
 
using std::cout;
using std::cin;
using std::endl;
 
int hashTable[8] = {-1, -1, -1, -1, -1, -1, -1, -1};
//ハッシュ関数 x Mod 8
 
int H(int _x)
{
	return( _x % 8);
}
 
//ハッシュ探索関数
int HashSearch(int key)
{
	int hash = H(key);
	if (hashTable[hash] != -1)
	{
		return(hash);
	}
	else
	{
		cout << "データなし" << endl;
		return(-1);
	}
}
 
//ハッシュ関数を使って配列に値を格納する
int main() {
	//データdat [26, 53, 59, 84, 142, 161, 175]
	int dat[7] = {26, 53, 59, 84, 142, 161, 175};
	//データをハッシュ値を使って格納していくよ
	//hashTable[H(26)] = 26;
	//hashTable[H(53)] = 53;
	//    …        =  …
	for(int i=0; i < 7; i++)
	{
		hashTable[H(dat[i])] = dat[i];
	}
 
	for(int i=0; i < 8; i++)
	{
		cout << "hashTable[" << i << "] = " << hashTable[i] << endl;
	}
	//53を検索
	cout << "53はindex" << HashSearch(53) << "に入ってます。"<< endl;
 
	return 0;
}

しかしながらデータによっては、複数のデータが同一のインデックスを指してしまうことがあります。

授業でやった基本情報技術者試験の問題の例

ハッシュ関数 $ H(x) = x \space Mod \space 8 $
データ列 : [ 26, 53, 59, 84, 142, 161, 175, 178, 179 ]
のときハッシュ値 $ H(x) $ は、
$ H(x) $ : [ 2, 5, 3, 4, 6, 1, 7, 2, 3]

すなわち、整数配列dat[8]があった時に
$$ \begin{array}{lll} データ値& 26 の時&: H(26) = 26 \space Mod \space 8 = 2 & dat[2] = 26\\ データ値& 53 の時&: H(53) = 53 \space Mod \space 8 = 5 & dat[5] = 53\\ データ値& 59 の時&: H(59) = 59 \space Mod \space 8 = 3  & dat[3] = 59\\ データ値& 84 の時&: H(84) = 84 \space Mod \space 8 = 4  & dat[4] = 84\\ データ値& 142の時&: H(142) = 142 \space Mod \space 8 = 6 & dat[6] = 142 \\ データ値& 161の時&: H(161) = 161 \space Mod \space 8 = 1 & dat[1] = 161\\ データ値&175の時&: H(175) = 175 \space Mod \space 8 = 7 & dat[7] = 175\\ データ値&178の時&: H(178) = 178 \space Mod \space 8 = 2 & dat[2] = 178\\ データ値&179の時&: H(179) = 179 \space Mod \space 8 = 3  & dat[3] = 179\\ \end{array} $$

すなわち

dat[0] = 空
dat[1] = 161
dat[2] = 26, dat[2] = 178
dat[3] = 59, dat[3] = 179
dat[4] = 84
dat[5] = 53
dat[6] = 142
dat[7] = 175

となります。
dat[2]とdat[3]に複数のデータが割り当たってしまってますね。
ここで、
$$ \begin{array}{lll} データ値& 26 の時&: H(26) = 26 \space Mod \space 8 = 2 & dat[2] = 26\\ データ値& 59 の時&: H(59) = 59 \space Mod \space 8 = 3  & dat[3] = 59\\ データ値&178の時&: H(178) = 178 \space Mod \space 8 = 2 & dat[2] = 178\\ データ値&179の時&: H(179) = 179 \space Mod \space 8 = 3  & dat[3] = 179\\ \end{array} $$ のように、$ H(x) $の出力値によりインデックスが被ってしまうことがある。 これを、衝突またはシノニムの発生という。

衝突を回避するには

絶賛執筆中

とか書きましたが、仕組み的に回避は無理なので、衝突をなんとか解決するには、配列のデータ構造を工夫します。
当然ですが、配列は「変数」=「データを入れる箱」の列なので箱1個あたりに値は1個しか入らない。というのが衝突の原因になります。
したがって、1つ箱当たりに複数のデータを入れることを考えればよいことになります。(そりゃそうだ)
そこで、こんな感じのデータを考えてみます(今のところただの2次元配列)

Fig. 1: 配列の配列

このような配列があると、複数のデータが同じインデックスに割り当たっても対応できそうです。
そして、以下のアルゴリズムを考えてみます。

あるデータxを配列 dat[]に格納する(datの要素数は十分にあるものとする)
配列dat[]の要素はあらかじめ-1で初期化されている
ハッシュ関数 H(x) で格納先のIndexが決まっているとき
値Keyがどのインデックスに入っているか探索する

 ▲ dat[ H(x) ] ≠ -1
 |  ▲ dat[H(x)] == key
 |  | ・H(x)にデータは存在する
 |  +------------------
 |  | ・H(x)にデータは存在するが、先頭にはない
 |  ▼
 +---------------------- 
 |  ・データなし
 ▼

つまり、

ということである。

の時は、上の図のdat[H(x)]を横に線形探索することで、keyが見つかるはずである。
2次元配列を使うと

あるデータxを配列 dat[][]に格納する(datの要素数は十分にあるものとする)
配列dat[][]の要素はあらかじめ-1で初期化されている
ハッシュ関数 H(x) で格納先のIndexが決まっているとき
値Keyがどのインデックスに入っているか探索する

 ▲ dat[ H(x) ][0] ≠ -1
 |  ▲ dat[H(x)][0] == key
 |  | ・H(x)にデータは存在する
 |  +------------------
 |  | ・H(x)にデータは存在するが、先頭にはない
 |  | ・dat[H(x)][0] ~dat[H(x)][n]まで線形探索
 |  ▼
 +---------------------- 
 |  ・データなし
 ▼
int dat[8][20]
={{xx,-1,-1,…-1},//dat[0][] -1x20個
  {xx,-1,-1,…-1},//dat[1][] -1x20個
  {xx,-1,-1,…-1},//dat[2][] -1x20個
  {xx,-1,-1,…-1},//dat[3][] -1x20個
  {-1,-1,-1,…-1},//dat[4][] -1x20個
  {xx,-1,-1,…-1},//dat[5][] -1x20個
  {xx,-1,-1,…-1},//dat[6][] -1x20個
  {xx,xx,xx,…-1},//dat[6][] -1x20個
};

リスト構造(データ構造)
list<int> dat[8] //int型のlistを8個
list[0]:〇→〇
list[1]:〇→〇→〇
list[2]:〇
list[3]:〇→〇
list[4]:〇→〇→〇
list[5]:〇
list[6]:〇→〇
list[7]:〇

リスト構造について

C++のSTL::listを使って簡単にリスト構造を実現するよ

#include <iostream>
#include <list>
using std::cout;
using std::cin;
using std::endl;
using std::list;
 
//STLのlistを使うよっていうinclude
//テンプレート(いろんな方に対応できる便利な奴)
 
 
 
int main() {
	//intのリストを作りたいな
	list<int> ilist;
	//floatのリストを作りたいな
	list<float> flist;
 
	int indata;
 
	do{
		cout << "整数を入力:";
		cin >> indata;
		if(indata != -1)
			ilist.push_back(indata);
		else
			break;
                //リストのすべての要素を表示するfor文(ヘッドから勝手にシーケンシャルに最後までアクセス)
		for(auto& i: ilist)
		{
			cout << i << "→";		
		}
		//1個ずつきちんとアクセスしたい場合は、イテレータというポインタを使う(ちょっと難しい)
        //イテレータを使って手動で一個ずつ後ろにシーケンシャルアクセス
		//list<int>::iterator theI;
		//for(theI=ilist.begin(); theI !=ilist.end(); theI++)
		//{
		//		cout << *theI << "→";
		//}
		cout << endl;
	}while(true);
        //ランダムアクセスできないので、配列みたいなアクセスはできない
	//cout << ilist[2] << endl; 
}

基本情報の問題解いてみる

https://www.fe-siken.com/kakomon/16_haru/q13.html

16      2            10 (基数)  mod 8   int Data[10][5](ハッシュテーブル)
1A  0001 1010   16+8+2   =   26     2   〇Data[2]←26 [2][0]
35  0011 0101   32+16+5  =   53     5     Data[5]←53
3B  0011 1011   32+16+11 =   59     3   ★Data[3]←59 [3][0]
54  0101 0100   64+16+4  =   84     4     Data[4]←84
8E  1000 1110   128+14   =  142     6     Data[6]←142
A1  1010 0001   128+32+1   =161     1     Data[1]←161
AF  1010 1111   128+32+15  =175     7     Data[7]←175
B2  1011 0010   128+32+16+2=178     2   〇Data[2]←178 [2][1]
B3  1011 0011   128+32+16+3=179     3   ★Data[3]←179 [3][1]
                    〇★が衝突(シノニムの発生)

dat[127]                   出力
データあり →データ   dat[2]←94    インデックス 2
データない → -1      dat[2]←-1        データなし
データが複数→データ列  dat[2]←94, 8, 3 異なるデータ