URL短縮システムを自作する場合など、限られた種類の文字を使って極力短いIDを作りたいことがあります。
例えば65536種類のIDを作りたい場合には、例えば0~65535の一意な数値を16進表記して0~FFFFの最大4桁のIDにできるわけですが、もっと短くできないか、という話です。
なお、IDの予測困難性や衝突耐性などは特に考えないことにします。そんなの考えたら長いID一択ですもんね。
そこで、n種類の短いIDを作るとき、0~n-1の数値と一対一に対応する極力短い文字列を作ることを考えます。
ちなみに、数値以外と対応付ける場合は、データ←→数値←→IDというふうに間接的に対応付けることができます。
要件
- 数値のなるべく短い文字列表現
- 文字列は可変長
- 使用可能な文字には一定の制限あり
3番目は、制御文字やマルチバイト文字を使えない等、実際にIDとして使うときに必要となる制限です。
ここではm種類の文字(例えば0~9、a~z、A~Zの62種類)を使うこととします。
方針
一番シンプルな発想はm進数表記ですが、これには最短にならないケースがあるという問題があります。
例えば、0…Zと続いた後には、10~ZZのIDが続きます。
これでは00~0Zが使われなくてもったいないですね。
量としては些細なものですが、ちょっと気持ち悪いです。
そこで、m進数表記をちょっと発展させます。
桁数が増えたときに、その桁数で改めて0から連番を振りなおすという考え方です。
0~Z(数値の0~61)まで使い切ったら、今度は10(数値の62)ではなく00(数値の0)から始めるということです。
そしてZZ(数値の3843、一桁のときからの通算で3905)まで使い切ったら、今度は000から始めるというわけです。
サンプルコード
PHPで実装したコードは以下のようになります。
<?php
$letters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$count = strlen($letters);
function GenerateID // ID生成
($num)
{
global $letters, $count;
$ret = '';
// 何桁で表現すべきかを計算
$L = 0;
$n = $count;
while (true) {
$L++;
if ($num<$n) break;
$num -= $n;
$n *= $count;
}
// 必要な桁数分文字を追加
for ($i=0; $i<$L; $i++) {
$ret = GetLetter($num%$count).$ret;
$num = (int)($num/$count);
}
return $ret;
}
function GetLetter
($index)
{
global $letters;
return substr($letters, $index, 1);
}
?>解説
サンプルにするにはあまりよくないコードですが、
- $Lが現在の桁数
- $numが現在の連番
- $nが$L桁で表現できる数
となっています。
動作例
<?php
// 結果:ZVau
echo GenerateID(15000000);
?>この例では8桁の数値が4桁で表せました。
ちなみに、普通の62進数では10Wbu(5桁)、普通の16進数ではe4e1c0(6桁)となります。
ただ、こんなふうに普通の62進数より短くなるケースは決して多くはありません。
何が何でも最短じゃなきゃ気持ち悪いっていう人だけどうぞ。