谢谢大家的出色回答。最后,我想分享以下解决方案。
在我进一步介绍原因和方式之前,这是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
其他回答者的所有想法都很棒。通过这些问题的答案terdon,JF塞巴斯蒂安和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
我们需要od和tr,但是它们由POSIX支持。)
那么它是怎样工作的?
在开始之前,有两个观察:
事实证明,bash无法处理大于2 63 -1的整数。你自己看:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
看来bash在内部使用带符号的64位整数来存储整数。因此,在2 63处它“环绕”,我们得到一个负整数。因此,无论我们使用任何随机函数,我们都不希望获得大于2 63 -1的范围。Bash根本无法应付。
每当我们要样品之间的任意范围内的值min
,并max
有可能min != 0
,我们可以简单地品尝值之间0
和max-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
现位位串,但我们要样品整数从一个范围0
到max
,均匀随机,其中max
可以是任意的,不一定是两个电源。(我们不能使用模数,因为这会产生偏差。)
我们之所以如此努力地采样尽可能多的位来表示该值的全部要点max
是,我们现在可以安全地(有效地)使用循环来重复采样一个n
-bit位串,直到我们采样一个较低的值为止。或等于max
。在最坏的情况下(max
是2的幂),每次迭代以50%的概率终止,在最坏的情况下(是max
2减去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
,其中min
和max
可以是任意的,甚至是负数。如前所述,这现在是微不足道的。
让我们将其全部放入bash脚本中。做一些参数解析的事情...我们需要两个参数min
和max
,或者只有一个参数max
,min
默认为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,并把我的发现这里。:-)