How to Randomly Sort (Scramble) an Array in Ruby

How to randomly sort (scramble) an array in Ruby?

Built in now:

[1,2,3,4].shuffle => [2, 1, 3, 4]
[1,2,3,4].shuffle => [1, 3, 2, 4]

Randomizing array elements

Use the shuffle method ...

irb(main):001:0> [1,2,3,4,5].shuffle
=> [3, 4, 2, 5, 1]

How to shuffle an array the same way every time?

Array#shuffle can take a seeded Random instance.

a = [1,2,3,4]
seed = 1

a.shuffle(random: Random.new(seed))
# => [4, 1, 3, 2]

a.shuffle(random: Random.new(seed))
# => [4, 1, 3, 2]

Just replace seed = 1 with whatever random seed you want to use.

Ruby - return an array in random order

If you don't have [].shuffle, [].sort_by{rand} works as pointed out by sepp2k. .sort_by temporarily replaces each element by something for the purpose of sorting, in this case, a random number.

[].sort{rand-0.5} however, won't properly shuffle. Some languages (e.g. some Javascript implementations) don't properly shuffle arrays if you do a random sort on the array, with sometimes rather public consequences.

JS Analysis (with graphs!): http://www.robweir.com/blog/2010/02/microsoft-random-browser-ballot.html

Ruby is no different! It has the same problem. :)

#sort a bunch of small arrays by rand-0.5
a=[]
100000.times{a << [0,1,2,3,4].sort{rand-0.5}}

#count how many times each number occurs in each position
b=[]
a.each do |x|
x.each_index do |i|
b[i] ||=[]
b[i][x[i]] ||= 0
b[i][x[i]] += 1
end
end
p b

=>

[[22336, 18872, 14814, 21645, 22333],
[17827, 25005, 20418, 18932, 17818],
[19665, 15726, 29575, 15522, 19512],
[18075, 18785, 20283, 24931, 17926],
[22097, 21612, 14910, 18970, 22411]]

Each element should occur in each position about 20000 times. [].sort_by(rand) gives much better results.

#sort with elements first mapped to random numbers
a=[]
100000.times{a << [0,1,2,3,4].sort_by{rand}}

#count how many times each number occurs in each position
...

=>

[[19913, 20074, 20148, 19974, 19891],
[19975, 19918, 20024, 20030, 20053],
[20028, 20061, 19914, 20088, 19909],
[20099, 19882, 19871, 19965, 20183],
[19985, 20065, 20043, 19943, 19964]]

Similarly for [].shuffle (which is probably fastest)

[[20011, 19881, 20222, 19961, 19925],
[19966, 20199, 20015, 19880, 19940],
[20062, 19894, 20065, 19965, 20014],
[19970, 20064, 19851, 20043, 20072],
[19991, 19962, 19847, 20151, 20049]]

Writing a shuffle method in Ruby

Your idea is to pick elements at random until you have picked all elements.

Other than verbosity, another issue that you have is that you use #delete. Hence if there are two repeating elements in the array, you will get only one in the shuffled result.

Also you mutate the array passed, which is generally undesirable. Here is an implementation of the same algorithm:

def recursive_shuffle(to_shuffle, shuffled = [])
return shuffled if to_shuffle.empty?
to_shuffle = to_shuffle.dup if shuffled.empty?

element = to_shuffle.sample
to_shuffle.delete_at(to_shuffle.index(element))

recursive_shuffle(to_shuffle, shuffled + [element])
end

As for a simpler solution:

array.sort_by { rand }

Ruby, how to shuffle one array into another

(0..a1.length).to_a.sample(a2.length).sort
.zip(a2)
.reverse
.each{|i, e| a1.insert(i, e)}

Shuffling an array with a condition

Juksefantomet sent me this solution in discord, since he can't post here due to an account lock.

The below codeblock contains an alternative approach on how to tackle the problem at hand. This contains a fragmented solution to understand the steps on how you would normally approach a complex problem like the one presented above.

going through the various steps you can see how each condition has to be known in advance and specified to the point where your final array is not "illegal".

    @illegal_order = ['1','2','3','4','5','6','7','8','9']

puts @illegal_order.count

puts "#{@illegal_order}"

@foo = []

# traverse the original array and append a random value from that into foo array
# this can of course and should be done in a loop where you check for duplicates
# this is done below in this example, fragmented to see the individual action
(0..@illegal_order.count).each do |add_value|
@temp = @illegal_order[rand(@illegal_order.count)]
unless @foo.count == @illegal_order.count
@foo.push(@temp)
end
end

# making sure it contains everything in the original array
@foo.each do |bar|
unless @illegal_order.include?(bar)
@approve = false
puts "Errored generation!"
end
end

# defining patterns, this can of course be extracted by the original array and be done alot cleaner
# but printing it out to better break it down
@pattern1 = [@illegal_order[0],@illegal_order[1],@illegal_order[2]]
@pattern2 = [@illegal_order[3],@illegal_order[4],@illegal_order[5]]
@pattern3 = [@illegal_order[6],@illegal_order[7],@illegal_order[8]]

# Let us step through the new array and use case to check for various conditions that would flag the new array as invalid
@foo.each do |step|
# setting a temp pattern based on current state
@temp_pattern = [@foo[step.to_i-1],@illegal_order[step.to_i],@illegal_order[step.to_i+1]]
case step
when @temp_pattern == @pattern1
@illegalpatterns = true
when @temp_pattern == @pattern2
@illegalpatterns = true
when @temp_pattern == @pattern3
@illegalpatterns = true
end
# checking the foo array if it contains duplicates, if yes, set conditional to true
@illegal_order.each do |count|
if @foo.count(count) > 1
@duplicates = true
end
end
end

# printing out feedback based on duplicates or not, this is where you rescramble the array if duplicate found
(@duplicates == true) ? (puts "dupes found") : (puts "looks clear. no duplicates")
# printing out feedback based on illegal patterns or not, this is where you rescramble the array if duplicate found
(@illegalpatterns == true) ? (puts "illegal patterns found") : (puts "looks clear, no illegal patterns")

puts "#{@foo}"


Related Topics



Leave a reply



Submit