我正在使用以下代码在Ruby中生成一个由[A-Z a-z 0-9]
组成的唯一10位随机字符串:
random_code = [*('a'..'z'),*('0'..'9'),*('A'..'Z')].shuffle[0, 10].join
然而,有时候随机字符串中并不包含数字或大写字母。您能帮忙编写一个方法来生成一个独特的随机字符串,该字符串至少包含一个数字、一个大写字母和一个小写字母吗?
我正在使用以下代码在Ruby中生成一个由[A-Z a-z 0-9]
组成的唯一10位随机字符串:
random_code = [*('a'..'z'),*('0'..'9'),*('A'..'Z')].shuffle[0, 10].join
然而,有时候随机字符串中并不包含数字或大写字母。您能帮忙编写一个方法来生成一个独特的随机字符串,该字符串至少包含一个数字、一个大写字母和一个小写字母吗?
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
更为简洁,但是效率问题难以承受。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
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
n_down + n_up + n_digits - 2*C(26,10) - 3
#=> 16_317_774_459
为了得出从一个有62个元素的总体中无放回地抽取10个样本,这些样本既没有小写字母、大写字母也没有数字的概率,我们将这个数除以从62个元素中无放回地抽取10个字符的总方式数:
(16_317_774_459.0/c(62,10)).round(2)
#=> 0.15
are
应该是 arr
。但是这并不能保证所有生成的字符串都是唯一的。关于我对第二个解决方案的反对意见 - 试着运行 11.times { [extract1(down), extract1(up), extract1(digits)].concat(((down+up+digits).sample(7))).shuffle.join }
。 - ndnenkov 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
)。
即使您拥有一个无限快的处理器,您仍然无法保存这么多数据量,无论是复杂对象的计数还是简单位。
相反的方法是生成随机排列,在集合中保留已生成的排列,并在返回下一个元素之前进行包含检查。这只是推迟了不可避免的 - 不仅您最终仍然必须保存相同数量的信息,而且随着生成的排列数量增加,生成新排列所需的尝试次数会趋近于无穷大。
正如您可能已经想到的那样,问题的根源在于随机性和独特性很难同时存在。
对于随机程序的一个直观定义将是每次执行时不以相同的顺序生成标记。很好,现在我们只需要取前n个排列(其中n = rand(100)
),将它们放在最后并按顺序枚举所有内容?您可以感到这会导致什么。为了使随机生成被认为是好的,连续运行的生成输出应该均匀分布。简单地说,获得任何可能的输出的概率应该等于1 / #__all_possible_outputs__。
没有重复的n个元素的k排列的可能数量是:
n!/(n-k)! = 327_234_915_316_108_800 ((61 - 10 + 1).upto(61).reduce(:*)
)
仍然无法达到。同样适用于
没有重复的n个元素的全排列的可能数量:
n! = 507_580_213_877_224_798_800_856_812_176_625_227_226_004_528_988_036_003_099_405_939_480_985_600_000_000_000_000 (1.upto(61).reduce(:*)
)
不重复情况下,n个元素的可能k组合数:
n!/k!(n-k)! = 90_177_170_226 ((61 - 10 + 1).upto(61).reduce(:*)/1.upto(10).reduce(:*)
)
最后,我们可能会在没有重复的情况下完全排列k个元素:
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
。
生成排列的一个很酷的技巧是使用数字和位图。想法是生成所有从 0 到 2^61 - 1 的数字并查看位。如果位于位置 i
上有一个 1
,我们将使用 all[i]
元素,否则我们将跳过它。我们仍然没有摆脱问题,因为 2^61 = 2305843009213693952 (2**61
),我们无法将其保存在内存中。
幸运的是,另一个很酷的技巧来自数论。
任意m个连续数对一个质数求模的幂次方的结果会给出从0到m-1的数字。
换句话说:
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]
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
花费太多时间来测试和跳过数字。我会尝试想出一个更好的生成器,但其他所有内容仍然有效。
numbers = ('0'..'9').to_a
downcase_letters = ('a'..'z').to_a
upcase_letters = downcase_letters.map(&:upcase)
all = [numbers, downcase_letters, upcase_letters]
one_of_each_set = all.map(&:sample)
random_code = (one_of_each_set + (all.flatten - one_of_each_set).sample(7)).shuffle.join
sample(7)
是不重复抽样。例如 7.times.to_a.sample(7) #=> => [0, 1, 4, 6, 2, 5, 3]
。 - Cary Swovelandone_of_each_set
变量来代替all.map(&:sample)
,否则您将无法从大列表中删除相同的项目,并可能出现重复。 - Neil Slater使用'SafeRandom'宝石 Github链接
它将提供最简单的方法为Rails 2、Rails 3、Rails 4、Rails 5生成随机值。
在这里,您可以使用strong_string方法生成强字符串组合(即字母(大写、小写)、数字和符号的组合)
# => Strong string: Minumum number should be greater than 5 otherwise by default 8 character string.
require 'safe_random'
puts SafeRandom.strong_string # => 4skgSy93zaCUZZCoF9WiJF4z3IDCGk%Y
puts SafeRandom.strong_string(3) # => P4eUbcK%
puts SafeRandom.strong_string(5) # => 5$Rkdo