我使用以下代码在Ruby中生成[A-Z a-z 0-9]
的唯一10个字符的随机字符串:
random_code = [*('a'..'z'),*('0'..'9'),*('A'..'Z')].shuffle[0, 10].join
但是,有时此随机字符串不包含数字或大写字符。你能帮我一个方法,生成一个独特的随机字符串,至少需要一个数字,一个大写和一个小写字符?
答案 0 :(得分:4)
如果你想要一个脚本只生成一些少量的令牌(比如 2,5,10,100,1000,10 000 等),那么最好的方法就是简单地保持已经生成的令牌在内存中重试,直到生成新的令牌(从统计上讲,这不会花费很长时间)。如果不是这种情况 - 请继续阅读。
<小时/> 在考虑之后,这个问题实际上是非常有用的。对于brievety,我不会提到要求至少有一个数字,大小写和小写字母,但它将包含在最终解决方案中。也请all = [*'1'..'9', *'a'..'z', *'A'..'Z']
。
总结一下,我们希望生成n个元素的k-置换,其中随机重复唯一性约束。
k = 10,n = 61(all.size
)
Ruby碰巧有这样的方法,它是Array#repeated_permutation
。所以一切都很棒,我们可以使用:
all.repeated_permutation(10).to_a.map(&join).shuffle
然后逐个弹出结果字符串,对吧?错误!问题是可能性的数量恰好是:
k^n = 10000000000000000000000000000000000000000000000000000000000000(10**61
)。
即使你有一个无限制的快速处理器,你仍然无法保存这么多的数据,无论这是复杂对象还是简单的位数。
相反的是生成随机排列,将已经生成的集合保留在集合中并在返回下一个元素之前检查包含。这只是推迟了不可避免的事情 - 不仅你仍然需要在某个时刻保持相同数量的信息,但随着生成的排列数量的增加,生成新排列所需的尝试次数会分化为无穷大。
正如您可能已经想到的那样,问题的根源在于随机性和唯一性几乎无法实现。
<小时/> 首先,我们必须定义我们认为是随机的。根据{{3}} the amount的nerdy comics来判断,您可以推断出这不是黑白色。
随机程序的直观定义是在每次执行时不以相同顺序生成令牌的定义。太好了,所以现在我们可以采用前n个排列(n = rand(100)
),将它们放在最后并按顺序枚举所有内容?你可以感觉到它的发展方向。为了使随机生成被认为是好的,连续运行的生成输出应该是the subject。简单来说,获得任何可能输出的概率应该等于 1 / #__ all_possible_outputs __ 。
没有重复的n个元素的可能k-排列的数量是:
equaly distributed = 327_234_915_316_108_800((61 - 10 + 1).upto(61).reduce(:*)
)
仍然遥不可及。同样适用于
n个元素可能完全排列的数量,不重复:
n!/(n-k)! = 507_580_213_877_224_798_800_856_812_176_625_227_006_004_528_988_036_003_099_405_939_480_985_600_000_000_000_000(1.upto(61).reduce(:*)
)
n个元素的可能k组合的数量,不重复:
n! = 90_177_170_226((61 - 10 + 1).upto(61).reduce(:*)/1.upto(10).reduce(:*)
)
最后,我们可以突破k元素的完全排列而不重复:
n!/k!(n-k)! = 3_628_800(1.upto(10).reduce(:*)
)
~3.5m 并不是什么,但至少它是可以合理计算的。在我的个人笔记本电脑上k_permutations = 0.upto(9).to_a.permutation.to_a
平均花了 2.008337 秒。通常,随着计算时间的推移,这是很多。但是,假设您将在实际服务器上运行此操作,并且每个应用程序启动只运行一次,这不是什么。事实上,创造一些种子甚至是合理的。单个k_permutations.shuffle
获得 0.154134 秒,因此在大约一分钟内我们可以获得 61 随机排列:k_seeds = 61.times.map { k_permutations.shuffle }.to_a
。
生成排列的一个很酷的技巧是使用数字和k!。我们的想法是生成从 0 到 2 ^ 61 - 1 的所有数字并查看这些位。如果位置1
上有i
,我们将使用all[i]
元素,否则我们会跳过它。我们仍然没有逃避这个问题,因为 2 ^ 61 = 2305843009213693952(2**61
)我们无法记忆。
Fortunatelly,bitmaps来自救援,这次是从数论开始。
任何 m 连续数字以 m 的模数提升到素数的幂,给出从0到 m 的数字 - 1 < / p>
换句话说:
5.upto(65).map { |number| number**17 % 61 }.sort # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
5.upto(65).map { |number| number**17 % 61 } # => [36, 31, 51, 28, 20, 59, 11, 22, 47, 48, 42, 12, 54, 26, 5, 34, 29, 57, 24, 53, 15, 55, 3, 38, 21, 18, 43, 40, 23, 58, 6, 46, 8, 37, 4, 32, 27, 56, 35, 7, 49, 19, 13, 14, 39, 50, 2, 41, 33, 10, 30, 25, 16, 9, 17, 60, 0, 1, 44, 52, 45]
实际上,这是多么随机?事实证明 - m 共享的更常见的除数和所选的 m 数字,序列的分布越不均匀。但我们在这里很运气 - 61 ^ 2 - 1 是素数(也称为another cool trick)。因此,它可以共享的唯一除数是 1 和 61 ^ 2 - 1 。这意味着无论我们选择何种功率,数字 0 和 1 的位置都将是固定的。这并不完美,但其他 61 ^ 2 - 3 数字可以在任何位置找到。猜猜是什么 - 我们不关心 0 和 1 ,因为他们没有 10 {{1 s在他们的二进制表示中!
不幸的是,我们随机性的瓶颈在于我们想要产生的质数越大,它就越难。这是我能够在洗牌顺序中生成范围内的所有数字时最好的,而不会同时将它们保存在内存中。
<小时/> 所以要把所有东西都用到:
注意这只能解决n个元素的k-排列问题而不重复。我仍然没有想到添加重复的方法。
<小时/> 免责声明:以下代码不提供任何明示或暗示的保证。它的目的是进一步表达作者的想法,而不是一个生产就绪的解决方案:
1
编辑:结果证明我们的约束消除了太多的可能性。这会导致require 'prime'
class TokenGenerator
NUMBERS_UPPER_BOUND = 2**61 - 1
HAS_NUMBER_MASK = ('1' * 9 + '0' * (61 - 9)).reverse.to_i(2)
HAS_LOWER_CASE_MASK = ('0' * 9 + '1' * 26 + '0' * 26).reverse.to_i(2)
HAS_UPPER_CASE_MASK = ('0' * (9 + 26) + '1' * 26).reverse.to_i(2)
ALL_CHARACTERS = [*'1'..'9', *'a'..'z', *'A'..'Z']
K_PERMUTATIONS = 0.upto(9).to_a.permutation.to_a # give it a couple of seconds
def initialize
random_prime = Prime.take(10_000).drop(100).sample
@all_numbers_generator = 1.upto(NUMBERS_UPPER_BOUND).lazy.map do |number|
number**random_prime % NUMBERS_UPPER_BOUND
end.select do |number|
!(number & HAS_NUMBER_MASK).zero? and
!(number & HAS_LOWER_CASE_MASK).zero? and
!(number & HAS_UPPER_CASE_MASK).zero? and
number.to_s(2).chars.count('1') == 10
end
@k_permutation_seeds = 61.times.map { K_PERMUTATIONS.shuffle }.to_a # this will take a minute
@numbers_in_iteration = {go_fish: nil}
end
def next
raise StopIteration if @numbers_in_iteration.empty?
number_generator = @numbers_in_iteration.keys.sample
if number_generator == :go_fish
add_next_number if @numbers_in_iteration.size < 1_000_000
self.next
else
next_permutation(number_generator)
end
end
private
def add_next_number
@numbers_in_iteration[@all_numbers_generator.next] = @k_permutation_seeds.sample.to_enum
rescue StopIteration # lol, you actually managed to traverse all 2^61 numbers!
@numbers_in_iteration.delete(:go_fish)
end
def next_permutation(number)
fetch_permutation(number, @numbers_in_iteration[number].next)
rescue StopIteration # all k permutations for this number were already generated
@numbers_in_iteration.delete(number)
self.next
end
def fetch_permutation(number_mask, k_permutation)
k_from_n_indices = number_mask.to_s(2).chars.reverse.map.with_index { |bit, index| index if bit == '1' }.compact
k_permutation.each_with_object([]) { |order_index, k_from_n_values| k_from_n_values << ALL_CHARACTERS[k_from_n_indices[order_index]] }
end
end
花费太多时间测试和跳过数字。我将尝试考虑更好的发电机,但其他一切仍然有效。
@all_numbers_generator
答案 1 :(得分:4)
down = ('a'..'z').to_a
up = ('A'..'Z').to_a
digits = ('0'..'9').to_a
all = down + up + digits
[down.sample, up.sample, digits.sample].
concat(7.times.map { all.sample }).
shuffle.
join
#=> "TioS8TYw0F"
[编辑:以上反映了对这个问题的误解。不过,我会离开它。没有字符出现多次:
def rnd_str
down = ('a'..'z').to_a
up = ('A'..'Z').to_a
digits = ('0'..'9').to_a
[extract1(down), extract1(up), extract1(digits)].
concat(((down+up+digits).sample(7))).shuffle.join
end
def extract1(arr)
i = arr.size.times.to_a.sample
c = arr[i]
arr.delete_at(i)
c
end
rnd_str #=> "YTLe0WGoa1"
rnd_str #=> "NrBmAnE9bT"
down.sample.shift
(等)会比extract1
更紧凑,但效率低下太多了。
如果您不想重复随机字符串,只需记下您生成的字符串列表。如果您生成列表中的另一个,则丢弃它并生成另一个。但是,你不太可能必须生成任何额外的。例如,如果您生成100个随机字符串(满足至少一个小写字母,大写字母和数字的要求),那么将存在一个或多个重复字符串的可能性大约是700,000中的一个:
t = 107_518_933_731
n = t+1
t = t.to_f
(1.0 - 100.times.reduce(1.0) { |prod,_| prod * (n -= 1)/t }).round(10)
#=> 1.39e-07
t = C(62,10)
和C(62,10)
定义如下。
替代
有一种非常简单的方法可以做到这一点,结果非常有效:只需要在没有替换的情况下进行采样,直到找到满足至少小写字母,一个大写字母和一个数字的要求的样本。我们可以这样做:
DOWN = ('a'..'z').to_a
UP = ('A'..'Z').to_a
DIGITS = ('0'..'9').to_a
ALL = DOWN + UP + DIGITS
def rnd_str
loop do
arr = ALL.sample(10)
break arr.shuffle.join unless (DOWN&&arr).empty? || (UP&&arr).empty? ||
(DIGITS&&arr).empty?
end
end
rnd_str #=> "3jRkHcP7Ge"
rnd_str #=> "B0s81x4Jto
在找到“好”的样品之前,我们必须平均拒绝多少样品?事实证明(如果你真的,真的感兴趣的话,见下文)获得“坏”字符串的概率(即从all
的62个元素中随机选择10个字符,没有替换,没有小写字母,没有大写字母或没有数字,只有大约0.15。(15%)。这意味着85%的时间没有坏的样本将被拒绝之前找到一个好的。
事实证明,在对一个好的字符串进行采样之前,将要采样的错误字符串的预期数量为:
0.15/0.85 =~ 0.17
如果有人感兴趣,以下显示了上述概率的推导方式。
让n_down
为可以绘制10个样本的方式,没有小写字母:
n_down = C(36,10) = 36!/(10!*(36-10)!)
其中(二项式系数)C(36,10)
等于36个“事物”的组合数,一次可以“取”10个,等于:
C(36,10) = 36!/(10!*(36-10)!) #=> 254_186_856
类似地,
n_up = n_down #=> 254_186_856
和
n_digits = C(52,10) #=> 15_820_024_220
我们可以将这三个数字加在一起以获得:
n_down + n_up + n_digits #=> 16_328_397_932
这几乎是,但不完全是,绘制10个字符的方式的数量,没有替换,不包含小写字母字符,大写字母或数字。 “不完全”,因为有一些重复计算正在进行。必要的调整如下:
n_down + n_up + n_digits - 2*C(26,10) - 3
#=> 16_317_774_459
为了获得从62的人口中抽取10个样本的概率,没有替换,没有小写字母,没有大写字母或没有数字,我们将这个数字除以可以绘制10个字符的总数从62没有替换:
(16_317_774_459.0/c(62,10)).round(2)
#=> 0.15
答案 2 :(得分:0)
使用'SafeRandom'宝石GithubLink
它将提供最简单的方法来生成与Rails 2,Rails 3,Rails 4,Rails 5兼容的随机值。
这里您可以使用strong_string方法生成字符串的强组合(即字母(大写,小写),数字和符号的组合
nm <- names(lst1)
result <- lapply(unique(nm), function(n) unname(unlist(lst1[nm %in% n])))
names(result) <- unique(nm)
result
# $a
# [1] 1 2 3 4 5 6
#
# $b
# [1] 7 8 9 10 11 12