Ruby中需要具有alphanumberic的独特随机字符串

时间:2015-08-06 06:53:21

标签: ruby random

我使用以下代码在Ruby中生成[A-Z a-z 0-9]的唯一1​​0个字符的随机字符串:

random_code = [*('a'..'z'),*('0'..'9'),*('A'..'Z')].shuffle[0, 10].join

但是,有时此随机字符串不包含数字或大写字符。你能帮我一个方法,生成一个独特的随机字符串,至少需要一个数字,一个大写和一个小写字符?

3 个答案:

答案 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 amountnerdy 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

<小时/> 现在让我们试着将没有重复的n个元素的k-置换问题转换成多次完全k-置换而不重复。

生成排列的一个很酷的技巧是使用数字和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在他们的二进制表示中!

不幸的是,我们随机性的瓶颈在于我们想要产生的质数越大,它就越难。这是我能够在洗牌顺序中生成范围内的所有数字时最好的,而不会同时将它们保存在内存中。

<小时/> 所以要把所有东西都用到:

  1. 我们生成 10 元素的完整排列种子。
  2. 我们生成一个随机素数。
  3. 我们随机选择是否要为序列中的下一个数字或我们已经开始的数字生成排列(最多为有限数量的已开始数字)。
  4. 我们使用生成的数字的位图来获得所述排列。
  5. 注意这只能解决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