Cảm ơn tất cả các câu trả lời tuyệt vời của bạn. Tôi đã kết thúc với giải pháp sau đây, mà tôi muốn chia sẻ.
Trước khi tôi đi sâu vào chi tiết hơn về các vấn đề cá nhân và địa ngục, đây là tl; dr : kịch bản mới sáng bóng của tôi :-)
#!/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 ))
Lưu nó vào ~/bin/rand
và bạn có sẵn một hàm ngẫu nhiên ngọt ngào trong bash có thể lấy mẫu một số nguyên trong một phạm vi tùy ý nhất định. Phạm vi có thể chứa các số nguyên âm và dương và có thể dài tới 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
Tất cả các ý tưởng của người trả lời khác là tuyệt vời. Các câu trả lời của terdon , JF Sebastian và jimmij đã sử dụng các công cụ bên ngoài để thực hiện nhiệm vụ một cách đơn giản và hiệu quả. Tuy nhiên, tôi thích một giải pháp bash thực sự cho tính di động tối đa, và có thể một chút, đơn giản là vì yêu thích bash;)
Câu trả lời của Ramesh và l0b0 được sử dụng /dev/urandom
hoặc /dev/random
kết hợp với od
. Tuy nhiên, điều đó tốt, cách tiếp cận của họ có nhược điểm là chỉ có thể lấy mẫu các số nguyên ngẫu nhiên trong phạm vi 0 đến 2 8n -1 cho một số n, vì phương pháp này lấy mẫu byte, tức là bitstrings có độ dài 8. Đây là những bước nhảy khá lớn với tăng n.
Cuối cùng, câu trả lời của Falco mô tả ý tưởng chung về cách điều này có thể được thực hiện cho các phạm vi tùy ý (không chỉ sức mạnh của hai). Về cơ bản, đối với một phạm vi nhất định {0..max}
, chúng ta có thể xác định sức mạnh tiếp theo của hai là gì, tức là chính xác có bao nhiêu bit được yêu cầu để biểu diễn max
dưới dạng một chuỗi bit. Sau đó, chúng ta có thể lấy mẫu nhiều bit đó và xem liệu giá trị này, như một số nguyên, có lớn hơn không max
. Nếu vậy, lặp lại. Vì chúng tôi lấy mẫu nhiều bit cần thiết để đại diện max
, mỗi lần lặp có xác suất lớn hơn hoặc bằng 50% thành công (50% trong trường hợp xấu nhất, 100% trong trường hợp tốt nhất). Vì vậy, điều này là rất hiệu quả.
Kịch bản của tôi về cơ bản là một triển khai cụ thể cho câu trả lời của Falco, được viết bằng bash thuần túy và hiệu quả cao vì nó sử dụng các hoạt động bitwise tích hợp của bash để lấy mẫu bitstrstr theo độ dài mong muốn. Nó cũng tôn vinh một ý tưởng của Eliah Kagan , đề nghị sử dụng $RANDOM
biến tích hợp bằng cách ghép các bitstrings do các lệnh lặp đi lặp lại của $RANDOM
. Tôi thực sự đã thực hiện cả hai khả năng sử dụng /dev/urandom
và $RANDOM
. Theo mặc định, kịch bản trên sử dụng $RANDOM
. (Và ok, nếu sử dụng /dev/urandom
chúng ta cần od và tr , nhưng những thứ này được hỗ trợ bởi POSIX.)
Vì vậy, làm thế nào nó hoạt động?
Trước khi tôi nhận được điều này, hai quan sát:
Hóa ra bash không thể xử lý các số nguyên lớn hơn 2 63 -1. Xem cho chính mình:
$ echo $((2**63-1))
9223372036854775807
$ echo $((2**63))
-9223372036854775808
Dường như bash sử dụng các số nguyên 64 bit đã ký để lưu các số nguyên. Vì vậy, ở 2 63, nó "kết thúc" và chúng ta nhận được một số nguyên âm. Vì vậy, chúng tôi không thể hy vọng có được bất kỳ phạm vi nào lớn hơn 2 63 -1 với bất kỳ chức năng ngẫu nhiên nào chúng tôi sử dụng. Bash đơn giản là không thể xử lý nó.
Bất cứ khi nào chúng tôi muốn lấy mẫu một giá trị trong một phạm vi tùy ý giữa min
và max
có thể min != 0
, chúng tôi chỉ có thể lấy mẫu một giá trị giữa 0
và max-min
thay vào đó và sau đó thêm min
vào kết quả cuối cùng. Điều này hoạt động ngay cả khi min
và cũng có thể max
là âm , nhưng chúng ta cần cẩn thận để lấy mẫu một giá trị giữa 0
và giá trị tuyệt đối max-min
. Vì vậy, sau đó, chúng ta có thể tập trung vào cách lấy mẫu một giá trị ngẫu nhiên giữa 0
và một số nguyên dương tùy ý max
. Phần còn lại là dễ dàng.
Bước 1: Xác định số lượng bit cần thiết để biểu diễn một số nguyên (logarit)
Vì vậy, đối với một giá trị nhất định max
, chúng tôi muốn biết chỉ cần bao nhiêu bit để biểu diễn nó dưới dạng một chuỗi bit. Điều này là để sau này chúng ta có thể lấy mẫu ngẫu nhiên chỉ cần bao nhiêu bit, điều này làm cho tập lệnh rất hiệu quả.
Hãy xem nào. Vì với n
các bit, chúng ta có thể biểu thị tới giá trị 2 n -1, sau đó số n
bit cần thiết để biểu thị một giá trị tùy ý x
là trần (log 2 (x + 1)). Vì vậy, chúng ta cần một hàm để tính trần của logarit đến cơ sở 2. Nó khá tự giải thích:
log2() {
local x=$1 n=1 l=0
while (( x>n && n>0 ))
do
let n*=2 l++
done
echo $l
}
Chúng ta cần điều kiện n>0
để nếu nó phát triển quá lớn, bao bọc xung quanh và trở nên tiêu cực, vòng lặp được đảm bảo chấm dứt.
Bước 2: Lấy mẫu ngẫu nhiên một chuỗi bit có độ dài n
Các ý tưởng di động nhất là sử dụng /dev/urandom
(hoặc thậm chí /dev/random
nếu có lý do chính đáng) hoặc $RANDOM
biến tích hợp của bash . Trước tiên hãy xem làm thế nào để làm điều đó $RANDOM
.
Tùy chọn A: Sử dụng $RANDOM
Điều này sử dụng ý tưởng được đề cập bởi Eliah Kagan. Về cơ bản, vì $RANDOM
lấy mẫu số nguyên 15 bit, chúng ta có thể sử dụng $((RANDOM<<15|RANDOM))
để lấy mẫu số nguyên 30 bit. Điều đó có nghĩa là, dịch chuyển một lệnh gọi đầu tiên $RANDOM
15 bit sang trái và áp dụng một bitwise hoặc với lần gọi thứ hai $RANDOM
, kết hợp hiệu quả hai bitstr được lấy mẫu độc lập (hoặc ít nhất là độc lập như tích hợp sẵn của bash $RANDOM
).
Chúng ta có thể lặp lại điều này để có được số nguyên 45 bit hoặc 60 bit. Sau đó, bash không thể xử lý được nữa, nhưng điều này có nghĩa là chúng ta có thể dễ dàng lấy mẫu một giá trị ngẫu nhiên trong khoảng từ 0 đến 2 60 -1. Vì vậy, để lấy mẫu một số nguyên n bit, chúng tôi lặp lại quy trình cho đến khi chuỗi bit ngẫu nhiên của chúng tôi, có độ dài tăng dần theo các bước 15 bit, có độ dài lớn hơn hoặc bằng n. Cuối cùng, chúng tôi cắt bỏ các bit quá nhiều bằng cách dịch chuyển bit sang bên phải một cách thích hợp và chúng tôi kết thúc với một số nguyên ngẫu nhiên n bit.
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) ))
}
Tùy chọn B: Sử dụng /dev/urandom
Ngoài ra, chúng ta có thể sử dụng od
và /dev/urandom
lấy mẫu một số nguyên n bit. od
sẽ đọc các byte, tức là các bit có độ dài 8. Tương tự như trong phương pháp trước, chúng ta lấy mẫu rất nhiều byte để số bit được lấy mẫu tương đương lớn hơn hoặc bằng n và cắt các bit quá nhiều.
Số byte thấp nhất cần thiết để có ít nhất n bit là bội số thấp nhất của 8 lớn hơn hoặc bằng n, tức là sàn ((n + 7) / 8).
Điều này chỉ hoạt động lên đến số nguyên 56 bit. Lấy mẫu thêm một byte sẽ giúp chúng ta có một số nguyên 64 bit, nghĩa là, giá trị lên tới 2 64 -1, mà bash không thể xử lý.
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) ))
}
Đặt các mảnh lại với nhau: Lấy số nguyên ngẫu nhiên trong các phạm vi tùy ý
n
Bây giờ chúng ta có thể lấy mẫu bitbit bit, nhưng chúng ta muốn lấy mẫu các số nguyên trong một phạm vi từ 0
đến max
, đồng nhất ngẫu nhiên , trong đó max
có thể là tùy ý, không nhất thiết phải là lũy thừa của hai. (Chúng ta không thể sử dụng modulo vì điều đó tạo ra sự thiên vị.)
Toàn bộ lý do tại sao chúng tôi đã cố gắng rất nhiều để lấy mẫu càng nhiều bit cần thiết để biểu thị giá trị max
, giờ đây chúng tôi có thể sử dụng một vòng lặp để lấy mẫu một chuỗi n
bit -bit một cách an toàn cho đến khi chúng tôi lấy mẫu một giá trị thấp hơn hoặc bằng max
. Trong trường hợp xấu nhất ( max
là lũy thừa của hai), mỗi lần lặp kết thúc với xác suất 50% và trong trường hợp tốt nhất ( max
là lũy thừa hai trừ một), lần lặp đầu tiên chấm dứt một cách chắc chắn.
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
}
Kết thúc mọi thứ
Cuối cùng, chúng tôi muốn lấy mẫu các số nguyên giữa min
và max
, ở đâu min
và max
có thể tùy ý, thậm chí âm. Như đã đề cập trước đây, điều này bây giờ là tầm thường.
Hãy đặt tất cả trong một tập lệnh bash. Thực hiện một số công cụ phân tích cú pháp đối số ... Chúng tôi muốn hai đối số min
và max
, hoặc chỉ một đối số max
, trong đó min
mặc định 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
... Và cuối cùng, để lấy mẫu thống nhất một cách ngẫu nhiên một giá trị giữa min
và max
, chúng tôi lấy mẫu một số nguyên ngẫu nhiên giữa 0
và giá trị tuyệt đối của max-min
, và thêm min
vào kết quả cuối cùng. :-)
diff=$((max-min)) && diff=${diff#-}
echo $(( $(rand $diff) + min ))
Lấy cảm hứng từ điều này , tôi có thể thử sử dụng dieharder để kiểm tra và đánh giá PRNG này, và đưa những phát hiện của tôi vào đây. :-)