固定范围数字的双向散列

Two-way hashing of fixed range numbers

我需要创建一个函数,它将 0-N 范围内的单个整数作为参数,returns 是同一范围内看似随机的数字。

每个输入数字应该始终只有一个输出,并且应该始终相同。

这样的函数会产生这样的结果:

f(1) = 4
f(2) = 1
f(3) = 5
f(4) = 2
f(5) = 3

我相信这可以通过某种哈希算法来完成?我不需要任何复杂的东西,只是不需要像 f(1) = 2f(2) = 3 等太简单的东西

最大的问题是我需要它是可逆的。例如。上面的 table 应该是从左到右和从右到左,使用不同的函数进行从右到左的转换是可以的。

我知道最简单的方法是创建一个数组,将其打乱,然后将关系存储在数据库或其他东西中,但由于我需要 N 非常大,所以我想避免这种情况,如果可能。

编辑: 对于我的特殊情况,N 是一个特定的数字,正好是 16777216 (64^4)。

我提出要描述我如何 "randomly" 在生成研究数据集时加扰 9 位 SSN。这不会替换或散列 SSN。它重新排列数字。如果您不知道数字被加扰的顺序,则很难将数字按正确的顺序放回原位。我有一种直觉,这不是提问者真正想要的。所以,如果这个答案被认为离题,我很乐意删除它。

我知道我有 9 位数字。因此,我从一个依次具有 9 个索引值的数组开始:

$a = array(0,1,2,3,4,5,6,7,8);

现在,我需要将一个我能记住的密钥变成一种随机排列数组的方法。洗牌每次都必须是相同密钥的相同顺序。我使用了一些技巧。我使用 crc32 将单词转换为数字。我使用 srand/rand 来获得随机值的可预测顺序。注意:mt_rand 不再使用相同的种子生成相同的随机数字序列,所以我必须使用 rand。

srand(crc32("My secret key"));
usort($a, function($a, $b) { return rand(-1,1); });

数组 $a 仍然有数字 0 到 8,但它们被打乱了顺序。如果我使用相同的关键字,我每次都会得到相同的随机顺序。这让我每个月都重复这个并得到相同的结果。然后,通过一个打乱的数组,我可以从 SSN 中挑选数字。首先,我确保它有 9 个字符(一些 SSN 以整数形式发送,并且省略了前导 0)。然后,我通过使用 $a.

选择数字来构建一个屏蔽的 SSN
$ssn = str_pad($ssn, 9, '0', STR_PAD_LEFT);
$masked_ssn = '';
foreach($a as $i) $masked_ssn.= $ssn{$i};

$masked_ssn 现在将包含 $ssn 中的所有数字,但顺序不同。从技术上讲,有一些关键字可以使 $a 洗牌后成为原始有序数组,但这种情况非常罕见。

希望这是有道理的。如果是这样,您可以更快地完成所有操作。如果将原始字符串转换为字符数组,则可以对字符数组进行打乱。你只需要每次重新播种兰特。

$ssn = "111223333"; // Assume I'm using a proper 9-digit SSN
$a = str_split($ssn);
srand(crc32("My secret key"));
usort($a, function($a, $b) { return rand(-1,1); });
$masked_ssn = implode('', $a);

这在 运行 时间方面并不是真的更快,因为 rand 是一个相当昂贵的函数,而你 运行 rand 在这里要多得多。如果您像我一样屏蔽了数千个值,您将希望使用只洗牌一次的索引数组,而不是对每个值都洗牌。

现在,我该如何撤消它?假设我对索引数组使用第一种方法。它类似于 $a = {5, 3, 6, 1, 0, 2, 7, 8, 4}。这些是屏蔽顺序中原始 SSN 的索引。所以,我可以很容易地建立原始SSN。

$ssn = '000000000'; // I like to define all 9 characters before I start
foreach($a as $i=>$j) $ssn[$j] = $masked_ssn{$i};

如您所见,$i 在屏蔽的 SSN 中从 0 到 8 计数。 $j 计数 5、3、6... 并将掩码 SSN 中的每个值放在原始 SSN 中的正确位置。

如果范围始终是 2 的幂——比如 [0,16777216)——那么你可以使用 exclusive-or 正如@MarkBaker 建议的那样。如果您的范围不是 2 的幂,它就不会那么容易工作。

可以用加减法取模N,虽然这些单独太明显了,还得结合点别的。

您也可以进行模 N 乘法运算,但将其反转很复杂。为了简单起见,我们可以分离出低八位并将它们相乘并以不干扰这些位的方式相加,这样我们就可以再次使用它们来反转操作。

我不知道 PHP 所以我要用 C 来举个例子。也许是一样的。

int enc(int x) {
  x = x + 4799 * 256 * (x % 256);
  x = x + 8896843;
  x = x ^ 4777277;
  return (x + 1073741824) % 16777216;
}

并解码,以相反的顺序回放操作:

int dec(int x) {
  x = x + 1073741824;
  x = x ^ 4777277;
  x = x - 8896843;
  x = x - 4799 * 256 * (x % 256);
  return x % 16777216;
}

那1073741824一定是N的倍数,256一定是N的倍数,如果N不是2的幂那么就不能(必然)用异或(^在 C 中是排他性的,我也在 PHP 中假设)。您可以 fiddle 随意添加和删除阶段的其他数字。

在两个函数中加入1073741824是为了保证x保持正数;这是为了使模运算永远不会给出负结果,即使我们从 x 中减去可能使其在此期间变为负值的值。

看来您的答案不错,但还有其他选择。线性同余生成器 (LCG) 可以提供 1 对 1 映射,并且已知使用 Euclid 算法是可逆的。对于 24 位

Xi = [(A * Xi-1) + C] Mod M
where M = 2^24 = 16,777,216

A = 16,598,013
C = 12,820,163

对于 LCG 的可逆性,请查看 Reversible pseudo-random sequence generator