如何有效地在bash中生成大的,均匀分布的随机整数?


30

我一直想知道什么是在bash中获得良好随机性的最佳方法,即,什么程序可以获取MIN和之间的随机正整数,MAX从而

  1. 该范围可以任意大(或至少,例如,高达2 32 -1);
  2. 值是均匀分布的(即无偏差);
  3. 这是有效的。

获得bash随机性的有效方法是使用$RANDOM变量。但是,这仅采样0到2 15 -1 之间的值,该值可能不足以用于所有目的。人们通常会使用模数使其达到他们想要的范围,例如,

MIN=0
MAX=12345
rnd=$(( $RANDOM % ($MAX + 1 - $MIN) + $MIN ))

另外,这会产生偏差,除非$MAX碰巧将2 15 -1 = 32767相除。例如,如果$MIN为0且$MAX为9,则值0到7比值8和9更有可能,因为$RANDOM永远不会是32768或32769。随着范围的增加,此偏差会变得更糟,例如,如果$MIN为0且$MAX为9999,然后通过2767数字0具有的概率4 / 32767,而数字2768到9999只的概率3 / 32767

因此,尽管上述方法满足条件3,但不满足条件1和2。

到目前为止,我想满足条件1和2的最佳方法是使用/dev/urandom以下方法:

MIN=0
MAX=1234567890
while
  rnd=$(cat /dev/urandom | tr -dc 0-9 | fold -w${#MAX} | head -1 | sed 's/^0*//;')
  [ -z $rnd ] && rnd=0
  (( $rnd < $MIN || $rnd > $MAX ))
do :
done

基本上,只是从中收集随机性/dev/urandom/dev/random如果需要加密强度高的伪随机数生成器,并且如果您有很多时间,或者可能是硬件随机数生成器,则可以考虑使用),删除每个不是十进制数字的字符,将其折叠输出到的长度$MAX并削减前导0。如果碰巧只得到0,$rnd则为空,因此在这种情况下设置rnd0。检查结果是否超出我们的范围,如果超过,请重复。我本着模仿do ... while循环的精神,将while循环的“ body”强制进入了后卫,以强制至少执行一次body ,因为从一rnd开始就没有定义。

我认为我满足了这里的条件1和2,但是现在我搞砸了条件3。这有点慢。大约需要一秒钟的时间(如果幸运的话,只需十分之一秒的时间)。实际上,甚至无法保证循环会终止(尽管随着时间的增加,终止的可能性会收敛到1)。

是否有一种有效的方法来获取bash中预先指定且可能很大范围内的无偏随机整数?(我会在时间允许的情况下继续进行调查,但与此同时,我认为这里的某个人可能有一个不错的主意!)

答案表

  1. 最基本的(也是可移植的)想法是生成足够长的随机位串。使用bash的内置$RANDOM变量或使用odand /dev/urandom(或/dev/random),可以通过多种方式生成随机位串。如果随机数大于$MAX,则重新开始。

  2. 另外,也可以使用外部工具。

    • Perl解决方案
      • 优点:轻巧,简单,灵活
      • 相反:不适用于2 32 -1 以上的非常大的数字
    • Python解决方案
      • 专业版:简单,灵活,甚至可以大量使用
      • 相反:便携式性较差
    • zsh解决方案
      • 优点:还是适合使用zsh的人
      • 相反:可能更不便携

为什么只选择整数,而不是对随机位进行base64编码,然后将一定数量的字符(取决于所需的范围)从编码形式转换为base64的base10?
muru 2014年

是否需要重击?会像rand=$(command)做,如果command回报率是满足您需求的iteger?
terdon

@muru实际上是个好主意。我曾对类似的想法进行过一些思考,使用dd if=/dev/urandom 2>/dev/null和管道化od -t d(避免通过base64绕行),但是我不清楚转换是如何发生的以及它是否确实是公正的。如果您可以将您的想法扩展为高效,可行的脚本并解释为什么没有偏见,那将是一个很好的答案。:)
Malte Skoruppa 2014年

@terdon我更喜欢bash。我的意思是,您当然可以调用pythonperl您喜欢的语言,但这并不是到处都有。我想要更轻便的东西。好吧,awk我想,随机函数会很好。但是,便携性越好:)
Malte Skoruppa 2014年

2
是的,我一直在思考perl -e 'print int(rand(2**32-1))');。那是非常容易携带的,并且很快。Awk不会削减它,因为大多数实现都是从同一种子开始的。因此,您在随后的运行中将获得相同的随机数。它仅在同一运行中更改。
terdon

Answers:


17

我从这里看到了另一种有趣的方法。

rand=$(openssl rand 4 | od -DAn)

似乎也是一个不错的选择。它从随机设备读取4个字节,并将它们格式化为0和之间的无符号整数2^32-1

rand=$(od -N 4 -t uL -An /dev/urandom | tr -d " ")


为什么od命令不同。两者都只输出4字节的无符号整数:1st-from openssl,2nd-from /dev/random
jfs

1
我编辑使用@Ramesh来/dev/urandom代替/dev/random-我看不出要使用/dev/random,因此它确实很昂贵/很慢,或者会使系统的其他部分变慢。(请随意编辑并解释一下是否确实需要。)
Volker Siegel 2014年

1
不用担心,这种简单的差异产生如此复杂的效果真的令人惊讶。这就是为什么我坚持将示例更改为正确的示例的原因-人们从示例中学习。
Volker Siegel 2014年

1
@MalteSkoruppa:I代表sizeof(int)那可能比4原则上少。顺便说一句,od -DAn失败了,(2**32-1)od -N4 -tu4 -An继续工作。
jfs 2014年

8

谢谢大家的出色回答。最后,我想分享以下解决方案。

在我进一步介绍原因和方式之前,这是tl; dr:我闪亮的新脚本:-)

#!/usr/bin/env bash
#
# Generates a random integer in a given range

# computes the ceiling of log2
# i.e., for parameter x returns the lowest integer l such that 2**l >= x
log2() {
  local x=$1 n=1 l=0
  while (( x>n && n>0 ))
  do
    let n*=2 l++
  done
  echo $l
}

# uses $RANDOM to generate an n-bit random bitstring uniformly at random
#  (if we assume $RANDOM is uniformly distributed)
# takes the length n of the bitstring as parameter, n can be up to 60 bits
get_n_rand_bits() {
  local n=$1 rnd=$RANDOM rnd_bitlen=15
  while (( rnd_bitlen < n ))
  do
    rnd=$(( rnd<<15|$RANDOM ))
    let rnd_bitlen+=15
  done
  echo $(( rnd>>(rnd_bitlen-n) ))
}

# alternative implementation of get_n_rand_bits:
# uses /dev/urandom to generate an n-bit random bitstring uniformly at random
#  (if we assume /dev/urandom is uniformly distributed)
# takes the length n of the bitstring as parameter, n can be up to 56 bits
get_n_rand_bits_alt() {
  local n=$1
  local nb_bytes=$(( (n+7)/8 ))
  local rnd=$(od --read-bytes=$nb_bytes --address-radix=n --format=uL /dev/urandom | tr --delete " ")
  echo $(( rnd>>(nb_bytes*8-n) ))
}

# for parameter max, generates an integer in the range {0..max} uniformly at random
# max can be an arbitrary integer, needs not be a power of 2
rand() {
  local rnd max=$1
  # get number of bits needed to represent $max
  local bitlen=$(log2 $((max+1)))
  while
    # could use get_n_rand_bits_alt instead if /dev/urandom is preferred over $RANDOM
    rnd=$(get_n_rand_bits $bitlen)
    (( rnd > max ))
  do :
  done
  echo $rnd
}

# MAIN SCRIPT

# check number of parameters
if (( $# != 1 && $# != 2 ))
then
  cat <<EOF 1>&2
Usage: $(basename $0) [min] max

Returns an integer distributed uniformly at random in the range {min..max}
min defaults to 0
(max - min) can be up to 2**60-1  
EOF
  exit 1
fi

# If we have one parameter, set min to 0 and max to $1
# If we have two parameters, set min to $1 and max to $2
max=0
while (( $# > 0 ))
do
  min=$max
  max=$1
  shift
done

# ensure that min <= max
if (( min > max ))
then
  echo "$(basename $0): error: min is greater than max" 1>&2
  exit 1
fi

# need absolute value of diff since min (and also max) may be negative
diff=$((max-min)) && diff=${diff#-}

echo $(( $(rand $diff) + min ))

将其保存到后~/bin/rand,您将在bash中拥有一个甜美的随机函数,该函数可以对给定范围内的整数进行采样。该范围可以包含负整数和正整数,并且长度最多可以为2 60 -1:

$ rand 
Usage: rand [min] max

Returns an integer distributed uniformly at random in the range {min..max}
min defaults to 0
(max - min) can be up to 2**60-1  
$ rand 1 10
9
$ rand -43543 -124
-15757
$ rand -3 3
1
$ for i in {0..9}; do rand $((2**60-1)); done
777148045699177620
456074454250332606
95080022501817128
993412753202315192
527158971491831964
336543936737015986
1034537273675883580
127413814010621078
758532158881427336
924637728863691573

其他回答者的所有想法都很棒。通过这些问题的答案terdonJF塞巴斯蒂安jimmij使用外部工具做一个简单而有效的方式工作。但是,出于对bash的热爱,我更喜欢一个真正的bash解决方案,以实现最大的可移植性,也许还有一点点,只是出于对bash的热爱;)

拉梅什的和l0b0 '使用的回答/dev/urandom/dev/random与组合od。很好,但是,他们的方法的缺点是只能对0到2 8n -1的n 范围内的随机整数进行采样,因为该方法对字节(即长度为8的位串)进行采样。增加

最后,法尔科(Falco)的答案描述了如何对任意范围(不仅是2的幂)进行此操作的一般想法。基本上,对于给定范围{0..max},我们可以确定2的下一个幂是多少,即,确切地需要多少才能表示max为位串。然后,我们可以采样那么多的位,并查看此双串(作为整数)是否大于max。如果是这样,请重复。由于我们采样的位数与表示所需的位数相同max,因此每次迭代的概率都大于或等于成功的50%(最坏情况下为50%,最好情况下为100%)。因此,这非常有效。

我的脚本基本上是Falco答案的具体实现,使用纯bash编写,效率很高,因为它使用bash的内置按位运算来采样所需长度的位串。此外,它还兑现了Eliah Kagan的一个想法,该想法建议$RANDOM通过将反复调用所导致的位串连接起来来使用内置变量$RANDOM。我实际上实现了使用/dev/urandom和的可能性$RANDOM。默认情况下,以上脚本使用$RANDOM。(好吧,如果使用,/dev/urandom我们需要odtr,但是它们由POSIX支持。)

那么它是怎样工作的?

在开始之前,有两个观察:

  1. 事实证明,bash无法处理大于2 63 -1的整数。你自己看:

    $ echo $((2**63-1))
    9223372036854775807
    $ echo $((2**63))
    -9223372036854775808

    看来bash在内部使用带符号的64位整数来存储整数。因此,在2 63处它“环绕”,我们得到一个负整数。因此,无论我们使用任何随机函数,我们都不希望获得大于2 63 -1的范围。Bash根本无法应付。

  2. 每当我们要样品之间的任意范围内的值min,并max有可能min != 0,我们可以简单地品尝值之间0max-min替代,然后添加min到最终结果。即使min并且可能max负数都可以起作用,但是我们需要注意采样一个介于0和之间的值 max-min。因此,我们可以集中精力研究如何对介于0和之间的随机值进行采样max。其余的很容易。

步骤1:确定表示整数需要多少位(对数)

因此,对于给定的值max,我们想知道将其表示为位串需要多少位。这样一来,以后我们就可以根据需要随机地采样任意数量的位,这使得脚本非常有效。

让我们来看看。因为有了n位,我们最多可以表示2 n -1 值,所以n表示任意值所需的位数x是上限(log 2(x + 1))。因此,我们需要一个函数来计算以2为底的对数的上限。这是不言而喻的:

log2() {
  local x=$1 n=1 l=0
  while (( x>n && n>0 ))
  do
    let n*=2 l++
  done
  echo $l
}

我们需要条件,n>0以便如果条件变得太大,回绕并变为负值,则保证循环终止。

第2步:随机取样一个长度为 n

最可移植的想法是使用/dev/urandom(或即使/dev/random有充分的理由)或bash的内置$RANDOM变量。让我们先来看看如何做$RANDOM

选项A:使用 $RANDOM

这使用了Eliah Kagan提到的想法。基本上,由于$RANDOM对15位整数$((RANDOM<<15|RANDOM))进行采样,因此我们可以对30位整数进行采样。这意味着,将第一次调用$RANDOM向左移动15位,并按位或第二次调用$RANDOM,有效地连接两个独立采样的位串(或至少与bash内置函数一样独立$RANDOM)。

我们可以重复此操作以获得45位或60位整数。此后bash无法处理它,但这意味着我们可以轻松采样0到2 60 -1 之间的随机值。因此,要对n位整数进行采样,请重复此过程,直到长度以15位为步长增长的随机位串的长度大于或等于n为止。最后,我们通过向右适当的按位移位来切除过多的位,最后得到一个n位的随机整数。

get_n_rand_bits() {
  local n=$1 rnd=$RANDOM rnd_bitlen=15
  while (( rnd_bitlen < n ))
  do
    rnd=$(( rnd<<15|$RANDOM ))
    let rnd_bitlen+=15
  done
  echo $(( rnd>>(rnd_bitlen-n) ))
}

选项B:使用 /dev/urandom

另外,我们可以使用od/dev/urandom采样一个n位整数。od它将读取字节,即长度为8的位串。与以前的方法类似,我们对这么多的字节进行采样,以至于等效的采样位数大于或等于n,并切掉了太多的位。

获得至少n位所需的最低字节数是大于或等于n的8的最低倍数,即floor((n + 7)/ 8)。

最多只能使用56位整数。再采样一个字节将为我们提供一个64位整数,即bash无法处理的最大2 64 -1 值。

get_n_rand_bits_alt() {
  local n=$1
  local nb_bytes=$(( (n+7)/8 ))
  local rnd=$(od --read-bytes=$nb_bytes --address-radix=n --format=uL /dev/urandom | tr --delete " ")
  echo $(( rnd>>(nb_bytes*8-n) ))
}

组合在一起:获得任意范围内的随机整数

我们可以品尝到n现位位串,但我们要样品整数从一个范围0max均匀随机,其中max可以是任意的,不一定是两个电源。(我们不能使用模数,因为这会产生偏差。)

我们之所以如此努力地采样尽可能多的位来表示该值的全部要点max是,我们现在可以安全地(有效地)使用循环来重复采样一个n-bit位串,直到我们采样一个较低的值为止。或等于max。在最坏的情况下(max是2的幂),每次迭代以50%的概率终止,在最坏的情况下(是max2减去1的幂),第一次迭代必定终止。

rand() {
  local rnd max=$1
  # get number of bits needed to represent $max
  local bitlen=$(log2 $((max+1)))
  while
    # could use get_n_rand_bits_alt instead if /dev/urandom is preferred over $RANDOM
    rnd=$(get_n_rand_bits $bitlen)
    (( rnd > max ))
  do :
  done
  echo $rnd
}

整理东西

最后,我们要对min和之间的整数进行采样max,其中minmax可以是任意的,甚至是负数。如前所述,这现在是微不足道的。

让我们将其全部放入bash脚本中。做一些参数解析的事情...我们需要两个参数minmax,或者只有一个参数maxmin默认为0

# check number of parameters
if (( $# != 1 && $# != 2 ))
then
  cat <<EOF 1>&2
Usage: $(basename $0) [min] max

Returns an integer distributed uniformly at random in the range {min..max}
min defaults to 0
(max - min) can be up to 2**60-1  
EOF
  exit 1
fi

# If we have one parameter, set min to 0 and max to $1
# If we have two parameters, set min to $1 and max to $2
max=0
while (( $# > 0 ))
do
  min=$max
  max=$1
  shift
done

# ensure that min <= max
if (( min > max ))
then
  echo "$(basename $0): error: min is greater than max" 1>&2
  exit 1
fi

...最后,要对min和之间的一个值进行随机抽样max,我们对0和的绝对值之间的一个随机整数进行抽样max-min,然后将其min加到最终结果中。:-)

diff=$((max-min)) && diff=${diff#-}

echo $(( $(rand $diff) + min ))

灵感来自这个,我可能会尝试使用dieharder测试和基准这个PRNG,并把我的发现这里。:-)


您的解决方案假设sizeof(int) == 8(64bit)由于--format=u
jfs

1
您的解决方案使我想起了random.py的编写方式。random.Random类使用53bit?生成器返回任意大的随机数(多次调用),random.SystemRandom使用os.urandom()可以实现的功能相同/dev/urandom
jfs 2014年

uL表示该范围的sizeof(long)> = 8。不保证。您可以使用u8断言平台具有这样的整数。
jfs 2014年

@JFSebastian我一直在想,到目前为止,我的脚本还没有硬编码关于long int大小的任何假设。即使longsigned int的大小大于(或小于)64位(例如128位),它也可能会起作用。但是,如果我使用--format=u8了该代码,则会对该假设进行硬编码sizeof(int)==8。另一方面,如果使用,则--format=uL没有问题:我认为没有平台具有 64位整数,但仍将long int定义为较低的东西。所以基本上我认为--format=uL允许更大的灵活性。你怎么看?
Malte Skoruppa 2014年

还有long long,可以是64位,而INT = =长在某些平台上的32位。如果不能在所有平台上都保证0,.2 ** 60的范围,则不可以。另一方面,bash在此类平台上本身可能不支持此范围(我不知道,也许它使用maxint_t,然后u8更正确(如果您要声明固定范围)(od如果您的范围是如果bash范围取决于long的大小,则uL可能更合适)。您是否需要bash在所有操作系统上支持的完整范围或固定范围?
jfs

6

可以是zsh吗?

max=1000
integer rnd=$(( $(( rand48() )) * $max ))

您可能还希望将种子与一起使用rand48(seed)。如果有兴趣,请参阅man zshmodulesman 3 erand48获取详细说明。


我个人不使用zsh,但这是一个很好的补充:)
Malte Skoruppa 2014年


5

如果您想要一个从0(2 ^ n)-1的数字,其中n mod 8 = 0,则可以简单地从中获得n / 8个字节/dev/random。例如,要获取随机数的十进制表示,int您可以:

od --read-bytes=4 --address-radix=n --format=u4 /dev/random | awk '{print $1}'

如果只想取n ,则可以先取上限(n / 8)个字节,然后移至所需的数量。例如,如果要15位:

echo $(($(od --read-bytes=2 --address-radix=n --format=u4 /dev/random | awk '{print $1}') >> 1))

如果您完全确定自己不在乎随机性的质量,并且想保证运行时间最短,可以使用/dev/urandom代替/dev/random。使用前,请确保您知道自己在做什么/dev/urandom


谢谢。因此,请n从获取随机字节/dev/urandom并使用格式化od。与这个答案的精神相似。两者都同样好:)尽管两者都具有0到2 ^(n * 8)-1位固定范围的缺点,其中n是字节数。我更喜欢一种用于任意范围的方法,最大范围 2 ^ 32-1,但也可以更低。这产生了偏向困难。
Malte Skoruppa 2014年

编辑为使用,/dev/urandom而不是/dev/random-我认为没有理由使用/dev/random,它可能确实很贵/很慢,或者减慢了系统的其他部分。(请随意编辑并解释一下是否确实需要。)
Volker Siegel 2014年

恰好相反:除非您知道需要/ dev / random,否则使用/ dev / urandom。认为/dev/urandom结果比大多数情况下/dev/random无法使用urandom 更糟糕。一旦被初始化(在系统启动时);其结果与Linux上几乎所有应用程序的结果一样好。在某些系统上,随机数和urandom相同。/dev/urandom/dev/random
jfs

1
--format=u应该用代替,--format=u4因为sizeof(int)可能比4理论上少。
jfs 2014年

@JFSebastian 本文围绕该主题进行了非常有趣的讨论。他们的结论似乎是既/dev/random/dev/urandom不满意,并认为“Linux应该添加一个安全的RNG阻止,直到它已收集到足够的种子熵,此后的行为就像urandom”。
l0b0

3

假设您不反对使用外部工具,那么这应该满足您的要求:

rand=$(perl -e 'print int(rand(2**32-1))'); 

它使用perl rand函数,该函数以上限为参数。您可以将其设置为任何您喜欢的。在抽象数学定义中,这种方法与真正的随机性有多接近不在本站点的讨论范围之内,但是应该很好,除非您需要使用它来进行极度敏感的加密等。也许即使在那里,但我也不会提出任何意见。


这会中断很多,例如5 ** 1234
jfs

1
@JFSebastian是的。我自从指定OP以来就发布了此信息,1^32-1但您需要对其进行调整以获取更大的数字。
terdon

2

您应该获得等于或大于期望的最大值的最接近的(2 ^ X)-1,并获得位数。然后只需多次调用/ dev / random并将所有位附加在一起,直到您有足够的余量,然后截断所有过多的位。如果结果数大于最大重复数。在最坏的情况下,您有超过50%的机会使随机数低于“最大值”,因此(在此最坏的情况下)您平均会接听两次电话。


为了提高效率,这实际上是一个不错的主意。Ramesh的答案l0b0的答案基本上都从中获得随机位/dev/urandom,但是在两个答案中,它始终都是8位的倍数。od正如您很好地解释的那样,在将其格式化为十进制之前,将较低范围的过多位截短是提高效率的一个好主意,因为循环仅具有2次迭代的预期次数。结合上面提到的任何一个答案,这可能是解决之道。
Malte Skoruppa 2014年

0

您的答案很有趣,但时间很长。

如果您想要任意大的数字,则可以在助手中加入多个随机数:

# $1 - number of 'digits' of size base
function random_helper()
{
  base=32768
  random=0
  for((i=0; i<$1; ++i)); do
    let "random+=$RANDOM*($base**$i)"
  done
  echo $random
}

如果问题是偏见,则只需将其消除即可。

# $1 - min value wanted
# $2 - max value wanted
function random()
{
  MAX=32767
  min=$1
  max=$(($2+1))
  size=$((max-min))
  bias_range=$((MAX/size))
  while
    random=$RANDOM
  [ $((random/size)) -eq $bias_range ]; do :; done
  echo $((random%size+min))
}

将这些功能结合在一起

# $1 - min value wanted
# $2 - max value wanted
# $3 - number of 'digits' of size base
function random()
{
  base=32768
  MAX=$((base**$3-1))
  min=$1
  max=$(($2+1))
  size=$((max-min))
  bias_range=$((MAX/size))
  while
    random=$(random_helper)
  [ $((random/size)) -eq $bias_range ]; do :; done
  echo $((random%size+min))
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.