我一直想知道什么是最好的方法好的MIN
bash 中的随机性,即,获得和MAX
之间的随机正整数的过程是什么
- 该范围可以是任意大(或者至少,比如说,高达 2 32 -1);
- 值均匀分布(即无偏差);
- 它是有效的。
在 bash 中获得随机性的一个有效方法是使用$RANDOM
变量。然而,这仅对 0 到 2 15 -1之间的值进行采样,该值可能不足以满足所有目的。人们通常使用模来使其进入他们想要的范围,例如,
MIN=0
MAX=12345
rnd=$(( $RANDOM % ($MAX + 1 - $MIN) + $MIN ))
$MAX
此外,除非恰好能整除 2 15 -1=32767,否则这会产生偏差。例如,如果$MIN
is 0 和$MAX
is 9,则值 0 到 7 的可能性比值 8 和 9 稍大,因为$RANDOM
永远不会是 32768 或 32769。随着范围的增加,这种偏差会变得更糟,例如,如果$MIN
is 0 和$MAX
is 9999,那么数字 0 到 2767 的概率为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
空,因此在本例中设置rnd
为0
。检查结果是否超出我们的范围,如果超出,则重复。我将 while 循环的“主体”强制放入守卫中,以便本着模拟循环的精神,强制主体至少执行一次,do ... while
因为rnd
一开始是未定义的。
我想我在这里满足了条件 1 和 2,但现在我搞砸了条件 3。这有点慢。最多需要一秒左右(幸运的话只需十分之一秒)。实际上,循环甚至不能保证终止(尽管随着时间的增加终止概率收敛到 1)。
在 bash 中,是否有一种有效的方法可以在预先指定的且可能很大的范围内获取无偏随机整数? (如果时间允许,我会继续调查,但与此同时,我认为这里有人可能有一个很酷的想法!)
答案表
最基本的(因此可移植的)想法是生成一个足够长的随机位串。生成随机位串的方法有多种,可以使用 bash 的内置
$RANDOM
变量,也可以使用od
and/dev/urandom
(或/dev/random
)。如果随机数大于$MAX
,则重新开始。或者,可以使用外部工具。
- Perl 解决方案
- 优点:非常便携、简单、灵活
- Contra:不适用于 2 32 -1以上的非常大的数字
- Python 解决方案
- 优点:简单、灵活,甚至适用于大量数据
- 缺点:便携性较差
- zsh 解决方案
- 优点:对于使用 zsh 的人来说还是有好处的
- 魂斗罗:可能更不便携
- Perl 解决方案
答案1
答案2
感谢大家的精彩回答。我最终得到了以下解决方案,我想与大家分享。
在我详细介绍原因和方法之前,先介绍一下太长了;博士:我闪亮的新脚本:-)
#!/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
其他回答者的所有想法都很棒。答案由特登,JF塞巴斯蒂安, 和吉米使用外部工具以简单有效的方式完成任务。然而,我更喜欢真正的 bash 解决方案,以实现最大的可移植性,也许有一点,只是出于对 bash 的热爱;)
拉梅什'沙l0b0的答案使用/dev/urandom
或/dev/random
与 结合使用od
。这很好,但是,他们的方法的缺点是只能对某些 n 的 0 到 2 8n -1 范围内的随机整数进行采样,因为该方法采样字节,即长度为 8 的位串。这些是相当大的跳跃增加 n.
最后,法尔科的答案描述了如何做到这一点的总体思路随意的范围(不仅仅是二的幂)。基本上,对于给定范围{0..max}
,我们可以确定 2 的下一个幂是什么,即到底有多少位需要表示max
为位串。然后我们可以采样那么多位,看看这个双串作为整数是否大于max
。如果是这样,请重复。由于我们采样表示 所需的位数max
,因此每次迭代成功的概率大于或等于 50%(最坏情况下为 50%,最好情况下为 100%)。所以这是非常有效的。
我的脚本基本上是 Falco 答案的具体实现,用纯 bash 编写,效率很高,因为它使用 bash 的内置按位运算来采样所需长度的位串。它还尊重一个想法埃利亚·卡根$RANDOM
这建议通过连接重复调用产生的位串来使用内置变量$RANDOM
。我实际上实现了使用/dev/urandom
和 的可能性$RANDOM
。默认情况下,上面的脚本使用$RANDOM
. (好吧,如果使用/dev/urandom
我们需要OD和t,但这些都是由 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
这使用了主意伊利亚·卡根提到过。基本上,由于$RANDOM
采样的是 15 位整数,因此我们可以用来$((RANDOM<<15|RANDOM))
采样 30 位整数。这意味着,将第一次调用$RANDOM
向左移动 15 位,并在第二次调用 时应用按位 or $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 位整数,即最大为 2 64 -1 的值,这是 bash 无法处理的。
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
现在可以对 -bit 位串进行采样,但我们想要采样范围从 到0
的整数max
,均匀随机,其中max
可以是任意的,不一定是二的幂。 (我们不能使用模,因为这会产生偏差。)
我们如此努力地采样表示该值所需的位数的全部原因max
是,我们现在可以安全(高效)地使用循环来重复采样n
-bit 位串,直到采样到较低的值。或等于max
.在最坏的情况下(max
是 2 的幂),每次迭代以 50% 的概率终止,而在最好的情况下(max
是 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
}
把事情包起来
最后,我们想要对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 ))
答案3
可以是zsh吗?
zmodload zsh/mathfunc
max=1000
integer rnd='rand48() * max'
(对于 0 到 999 之间的随机数)
您可能还想将种子与 一起使用rand48(seed)
。如果有兴趣,请参阅man zshmodules
和man 3 erand48
获取详细描述。
答案4
如果您想要一个号码0通过(2^n)-1在哪里n 模 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
!