比較を繰り返す検索処理を避けたい
数に規則性のない数列(テーブル)からインデックスを得るとき、高級言語(死語?)では検索機能(table.index(N)など)がありますが、できれば検索なしでインデックスを得たいときがあります。(ここでの「検索」は比較を繰り返す処理とします)
テーブル中の数値が重複して出てこないとき、剰余(と逆引き表)を使うと検索なしにインデックスを得られる場合があります。
>>> table = [57183, 26302, 12855, 61489, 21256, 14532, 24168, 37683, 18386, 35798, 7211]
>>> [n % 45 for n in table]
[33, 22, 30, 19, 16, 42, 3, 18, 26, 23, 11]
>>> rtab = [
... -1, -1, -1, 6, -1, -1, -1, -1, -1, -1,
... -1, 10, -1, -1, -1, -1, 4, -1, 7, 3,
... -1, -1, 1, 9, -1, -1, 8, -1, -1, -1,
... +2, -1, -1, 0, -1, -1, -1, -1, -1, -1,
... -1, -1, 5, -1, -1, -1
... ]
>>> [rtab[n % 45] for n in table]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
例では、table の値を除数 45 で剰余したリストも中の数が重複していません(単射)。
目的の数列の剰余が単射になれば、逆引き表を作って検索不要にできます。
この方法の利点と欠点は以下のとおりです。
利点
- 高速化が期待できる
- 比較を繰り返す検索ではなくなるため
- 単純な逆引き表より小さな逆引き表を期待できる
-
rtab[N]
よりrtab[N % d]
のrtab
は小さいかも
-
欠点
- テーブル中の数値は重複してはいけない (set に変換しても同じサイズ)
- 逆引き表の前準備が必要
- 逆引き表はテーブル(正引き)より大きい
- 単純逆引きに比べると剰余逆引きは遅い
-
rtab[N]
よりrtab[N % d]
は剰余の分だけ遅い
-
その他
- 逆引き表は辞書(dictなど)相当なので、インデックス以外に置換えも可
除数の求め方
求めたい除数は「逆引き表の大きさ」でもあるので
初期値: テーブルの大きさ (len(table))
増加値: 1
として、テーブルの剰余が単射になるまで除数を増やして検索します。
除数の最大値は、テーブル内の最大値 (max(table)) になるので、単純逆引き表を用いた方が良いです。
除数の検索と逆引き表の生成プログラム
コマンドライン引数に数列を渡すと、除数の検索と逆引き表の生成を行います。
オプションで以下のことができます。
逆引きでの無効を示す値の変更 (-d N)
除数の上限を設定 (-l N)
#!/usr/bin/env python3
def create_remainder_table(table, divisor_max=None, invalid_value=None):
"除数検索と逆引き表の生成"
tlen = len(table)
if not tlen:
return (0, tuple())
if type(table) in (tuple, list, set):
ltab = tuple(table)
table = {ltab[i]: i for i in range(tlen)}
tlen = len(table)
divisor = tlen
if divisor_max is None:
divisor_max = max(table)
keys = tuple(table.keys())
while True:
rtab = {(n % divisor) for n in keys}
injective = (len(rtab) == tlen)
if injective or divisor >= divisor_max:
break
divisor += 1
if not injective:
return (0, tuple())
vals = tuple(table.values())
rtab = [(tlen if invalid_value is None else invalid_value)] * divisor
for i in range(tlen):
rtab[keys[i] % divisor] = vals[i]
return (divisor, rtab)
# コマンドライン処理
if __name__ == '__main__':
import re
import sys
import random
import argparse
def format_list(data, *, width=16, prefix='',
datafmt='%s', msgfmt='%#s', separator=',', space=1, suffix='\n'):
fmtdata = [datafmt % d for d in data]
msgfmt = msgfmt.replace('#', ('%d' % max([len(s) for s in fmtdata])))
msglist = [(msgfmt % d) + separator for d in fmtdata]
return (suffix.join([prefix + (' ' * space).join(msglist[i:min(i+width, len(msglist))])
for i in range(0, len(msglist), width)]) + suffix)
parser = argparse.ArgumentParser(description='剰余による数列の逆引き表生成支援道具')
parser.add_argument('N', nargs='+', help='数列の数 (-1 を一つだけ指定すると乱数によるテスト)')
parser.add_argument('-d', '--defval', metavar='N', help='表の穴埋め値を指定します (既定:表の大きさ)')
parser.add_argument('-l', '--limit', metavar='N', help='除数の上限を指定します (既定:表の最大値)')
args = parser.parse_args()
if len(args.N) == 1 and args.N[0] == '-1':
table = {random.randint(0, 65535) for n in range(random.randint(10, 50))}
else:
RE_NUM = re.compile('(0b(?P<n2>[01]+)|0o(?P<n8>[0-7]+)|(?P<n10>[0-9]+)|0x(?P<n16>[0-9A-Fa-f]+)).*')
table = []
for arg in args.N:
for s in arg.split(','):
m = RE_NUM.match(s.strip())
if not m:
continue
for kv in filter(lambda kv: kv[1], m.groupdict().items()):
table.append(int(kv[1], int(kv[0][1:])))
stab = set(table)
if len(stab) != len(table):
print('重複があります:', ','.join([
'%d' % d for d in set(filter(lambda n: table.count(n) != 1, table))
]))
sys.exit(2)
divisor_max = None if args.limit is None else int(args.limit)
invalid_value = len(table) if args.defval is None else int(args.defval)
(divisor, index) = create_remainder_table(table, divisor_max, invalid_value)
if divisor <= 0:
print('除数を見つけられませんでした')
sys.exit(2)
nrem = tuple(n % divisor for n in table)
srem = tuple('%d:%d' % (n, n % divisor) for n in table)
print('除数:', divisor)
print('逆引き:\n' + format_list(index, width=8, prefix=' '*4)[:-2])
print(('元の表: %d 個\n' % len(table)) + format_list(table, width=8, prefix=' '*4)[:-2])
print('剰余:\n' + format_list(nrem, width=8, prefix=' '*4)[:-2])
print('被除数:剰余:\n' + format_list(srem, width=8, prefix=' '*4)[:-2])
$ python3 findivisor.py 2, 3, 5, 7, 11, 13, 17, 19
除数: 13
逆引き:
5, 8, 0, 1, 6, 2, 7, 3,
8, 8, 8, 4, 8
元の表: 8 個
2, 3, 5, 7, 11, 13, 17, 19
剰余:
2, 3, 5, 7, 11, 0, 4, 6
被除数:剰余:
2:2, 3:3, 5:5, 7:7, 11:11, 13:0, 17:4, 19:6
$ python3 findivisor.py 461, 353, 419, 523, 139, 13, 31, 53, 89
除数: 37
逆引き:
9, 9, 9, 9, 9, 3, 9, 9,
9, 9, 9, 9, 2, 5, 9, 8,
7, 0, 9, 9, 1, 9, 9, 9,
9, 9, 9, 9, 4, 9, 9, 6,
9, 9, 9, 9, 9
元の表: 9 個
461, 353, 419, 523, 139, 13, 31, 53,
89
剰余:
17, 20, 12, 5, 28, 13, 31, 16,
15
被除数:剰余:
461:17, 353:20, 419:12, 523:5, 139:28, 13:13, 31:31, 53:16,
89:15
テスト:数列として {-1} を与えると表は乱数生成になります。
$ python3 findivisor.py -1 -d -1
除数: 63
逆引き:
-1, -1, -1, -1, -1, -1, 5, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
4, -1, -1, -1, 11, -1, -1, 13,
-1, -1, -1, -1, 2, -1, 9, 3,
15, 12, 1, 14, -1, -1, 7, 6,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, 10, 8, -1, -1, -1,
-1, -1, -1, -1, -1, 0, -1
元の表: 16 個
23434, 31597, 33733, 60700, 40651, 8574, 25176, 33176,
22795, 47532, 31992, 55586, 1167, 54770, 32165, 56795
剰余:
61, 34, 28, 31, 16, 6, 39, 38,
52, 30, 51, 20, 33, 23, 35, 32
被除数:剰余:
23434:61, 31597:34, 33733:28, 60700:31, 40651:16, 8574:6, 25176:39, 33176:38,
22795:52, 47532:30, 31992:51, 55586:20, 1167:33, 54770:23, 32165:35, 56795:32
HTML+JavaScript による生成
以下、スクリーン ショット
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
<title>除数検索と逆引き表生成</title>
<style>
table {
border: solid 1px gray;
border-collaspe: collaspe;
border-spacing: 0;
}
th, td {
border: solid 1px gray;
padding: 6px;
}
th {
background-color: #efefef;
}
</style>
</head>
<body>
<h1>除数検索と逆引き表生成</h1>
<p>
<textarea id="inp_table" rows="8" cols="80" placeholder="数列を入力"></textarea><br />
配列の穴埋め値:<input id="inp_defval" type="text" size="8" placeholder="自動">
<input type="button" value="更新" onclick="onUpdate();">
</p>
<p>入力配列の幅:
<input id="inp_wtab" type="number" size="5" placeholder="10" style="width: 3em;">
<input type="button" value="更新" onclick="onUpdate();">
</p>
<p>逆引配列の幅:
<input id="inp_wrem" type="number" size="5" placeholder="10" style="width: 3em;">
<input type="button" value="更新" onclick="onUpdate();">
</p>
<p>処理結果 => <span id="status">なし</span></p>
<table id="result"></table>
<h3>入力と剰余の対応</h3>
<table id="out_table1">
<tr><th>配列</th><th> </th></tr>
<tr><th>入力</th><td> </td></tr>
<tr><th>剰余</th><td> </td></tr>
</table>
<h3>逆引きと入力の対応</h3>
<table id="out_table2">
<tr><th>配列</th><th> </th></tr>
<tr><th>逆引</th><td> </td></tr>
<tr><th>入力</th><td> </td></tr>
</table>
<p></p>
<details>
<summary>デバッグ用</summary>
<div>
<p>入力文字の分解:<span id="dbg_nums"></span></p>
<p>上記の数値化:<span id="dbg_table"></span></p>
<p>穴埋め値:<span id="dbg_defval"></span></p>
<p>除数:<span id="dbg_divisor"></span></p>
<p>逆引き:<span id="dbg_index"></span></p>
</div>
</details>
<script type="text/javascript">
<!--
function setStatus(msg) {
document.getElementById('status').innerText = msg;
}
function clearChildren(tag) {
while (tag.firstChild)
tag.removeChild(tag.firstChild);
}
function setStyle(tag, style) {
if (style) for (const s of style)
tag.style.setProperty(s[0], s[1]);
return tag;
}
function setTextRight(tag) {
return setStyle(tag, [['text-align', 'right']]);
}
function trAppendChild(tr, tag, msg, style) {
const tx = document.createElement(tag);
tx.innerText = msg;
tr.appendChild(tx);
return setStyle(tx, style);
}
function trAppendTH(tr, msg, style) { return trAppendChild(tr, 'th', msg, style); }
function trAppendTHRight(tr, msg, style) { return setTextRight(trAppendChild(tr, 'th', msg, style)); }
function trAppendTD(tr, msg, style) { return trAppendChild(tr, 'td', msg, style); }
function trAppendTDRight(tr, msg, style) { return setTextRight(trAppendChild(tr, 'td', msg, style)); }
function getInputNumber(id, defval, limit) {
const tag = document.getElementById(id);
let val = Number(tag.value);
if (val < 1 || !Number.isInteger(val))
val = defval;
return Math.max(Math.min(val, limit), 1);
}
function onUpdate() {
setStatus('処理中');
const inptable = document.getElementById('inp_table');
const inpdefval = document.getElementById('inp_defval');
const nums = [...inptable.value.matchAll(/[ox_0-9A-Fa-f]+/g)];
const table = nums.map(s => Number(s));
if (table.length == 0) {
setStatus('エラー:数列を入力してください');
return;
}
document.getElementById('dbg_nums').innerText = nums;
document.getElementById('dbg_table').innerText = table;
const tabchk = new Set(table);
if (tabchk.has(NaN)) {
setStatus('エラー:数列に扱えない数値があります');
return;
}
if (tabchk.size != table.length) {
setStatus('エラー:重複があります');
return;
}
// 除数の検索と逆引き表の生成
const divisor_max = Math.max.apply(null, table);
let divisor = table.length;
// if (divisor > divisor_max) divisor = divisor_max;
let remains = table;
for (; divisor <= divisor_max; divisor++) {
remains = table.map(n => n % divisor);
if (new Set(remains).size == table.length) break;
}
const sindex = [...Array(divisor)].map(() => undefined);
for (let i = 0; i < sindex.length; i++) sindex[remains[i]] = i;
const ndefval = Number(inpdefval.value != '' ? inpdefval.value : '--');
const defval = (Number.isInteger(ndefval) ? ndefval : table.length);
const index = sindex.map(n => (n != undefined) ? n : defval);
// 結果出力
document.getElementById('dbg_defval').innerText = defval;
document.getElementById('dbg_divisor').innerText = divisor;
document.getElementById('dbg_index').innerText = index;
{
const otag = document.getElementById('result');
clearChildren(otag);
{
const tr = document.createElement('tr');
trAppendTH(tr, '定数');
trAppendTD(tr, '除数 = ' + divisor + ', 穴埋め値 = ' + defval);
otag.appendChild(tr);
}
{
const tr = document.createElement('tr');
trAppendTH(tr, '逆引表');
trAppendTD(tr, ('' + index).replaceAll(',', ', '));
otag.appendChild(tr);
}
{
const tr = document.createElement('tr');
trAppendTH(tr, '正引表');
trAppendTD(tr, ('' + table).replaceAll(',', ', '));
otag.appendChild(tr);
}
}
{
const otag = document.getElementById('out_table1');
clearChildren(otag);
const width = getInputNumber('inp_wtab', 10, table.length);
for (let iy = 0; iy < table.length; iy += width) {
const tr_idx = document.createElement('tr');
const tr_fwd = document.createElement('tr');
const tr_rem = document.createElement('tr');
const style = [['background-color', '#f7ffff']];
trAppendTH(tr_idx, '配列');
trAppendTH(tr_fwd, '入力');
trAppendTH(tr_rem, '剰余');
for (let ix = 0; ix < width; ix++) {
const i = ix + iy;
if ((ix + iy) < table.length) {
trAppendTHRight(tr_idx, ix + iy);
trAppendTDRight(tr_fwd, table[ix + iy], style);
trAppendTDRight(tr_rem, table[ix + iy] % divisor, style);
} else {
trAppendTH(tr_idx, '');
trAppendTD(tr_fwd, '');
trAppendTD(tr_rem, '');
}
}
otag.appendChild(tr_idx);
otag.appendChild(tr_fwd);
otag.appendChild(tr_rem);
}
}
{
const otag = document.getElementById('out_table2');
clearChildren(otag);
const width = getInputNumber('inp_wrem', 10, index.length);
for (let iy = 0; iy < index.length; iy += width) {
const tr_idx = document.createElement('tr');
const tr_rev = document.createElement('tr');
const tr_fwd = document.createElement('tr');
trAppendTH(tr_idx, '配列');
trAppendTH(tr_rev, '逆引');
trAppendTH(tr_fwd, '入力');
for (let ix = 0; ix < width; ix++) {
const i = ix + iy;
if (i < index.length) {
const j = index[i];
const valid = (0 <= j && j < table.length);
let rev = j;
let fwd = '';
if (valid)
fwd = table[j];
else
rev = '(' + j + ')';
const style = [['background-color', valid ? '#f7ffff' : '#fff7f7']];
trAppendTHRight(tr_idx, i);
trAppendTDRight(tr_rev, rev, style);
trAppendTDRight(tr_fwd, fwd, style);
} else {
trAppendTH(tr_idx, '');
trAppendTD(tr_rev, '');
trAppendTD(tr_fwd, '');
}
}
otag.appendChild(tr_idx);
otag.appendChild(tr_rev);
otag.appendChild(tr_fwd);
}
}
setStatus('完了');
}
// !-->
</script>
</body>
</html>