Code
def reorder(arr)
groups = arr.group_by { |h| h[:letter] }
return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1
max_key = groups.max_by { |_,a| a.size }.first
letters = ([max_key] + (groups.keys - [max_key])).cycle
ordered = []
while ordered.size < arr.size
k = letters.next
ordered << groups[k].pop unless groups[k].empty?
end
ordered
end
nilis returned if it is not possible to rearrange the elements in such a way that g[:letter] != h[:letter] for all pairs of consecutive elements g and h.
Note that this method has near linear computational complexity, O(arr.size), "near" because hash lookups are not quite constant time.
If desired, one could call the method with arr randomized: reorder(arr.shuffle).
Example
arr = [
{ letter: "a" }, { letter: "e" }, { letter: "b" }, { letter: "b" },
{ letter: "e" }, { letter: "a" }, { letter: "a" }, { letter: "f" }
]
reorder(arr)
#=> [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
# {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}]
Proof
The assertion is that if the line
return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1
were removed from the method the array returned by the method would have the property that for all pairs of successive elements, g, h, g[:letter] != h[:letter] if and only if
2 * groups.map { |_,v| v.size }.max <= arr.size + 1
The proof has two parts.
The above inequality holds if the method produces a valid array
Compute
max_key = groups.max_by { |_,a| a.size }.first
max_key_freq = groups.map { |_,v| v.size }.max
and assume a valid array is returned. There must be at least one element other than max_key between each successive value of max_key in that array. The number of elements of arr other than max_key must therefore be at least max_key_freq - 1, so that
max_key_freq + max_key_freq - 1 <= arr.size
Hence,
2 * max_key_freq <= arr.size + 1
which is the same as:
2 * groups.map { |_,v| v.size }.max <= arr.size + 1
The above inequality does not hold if the method produces an invalid array
Suppose ordered is returned and it contains successive elements g and h for which both g[:letter] and h[:letter] equal the same letter l.
Because of the way ordered is constructed:
groups[k] must be empty for all keys k in groups for which k != l;
f[:letter] must equal l for all elements of ordered following g (if there are any); and
l must be the first key enumerated by keys, which is a letter that appears with a frequency that is not less than that of any other letter. l has frequency groups.map { |_,v| v.size }.max.
If n = groups.keys.size there must be a non-negative integer k (loosely, the number of rounds of allocations for all keys of groups) such that the number of elements h of arr for which h[:letter] != l equals k*n and the number of elements h of arr for which h[:letter] == l is k*n + 2 + m, where m >= 0. The size of arr is therefore 2*k*n + 2 + m.
In that case,
2 * groups.map { |_,v| v.size }.max > arr.size + 1
-> 2 * (k*n + 2 + m) > (k*n + 2 + m + k*n) + 1
-> 2*k*n + 4 + 2*m > 2*k*n + 3 + m
-> (4-3) + m > 0
-> true
Explanation
For the example,
groups = arr.group_by { |h| h[:letter] }
#=> {"a"=>[{:letter=>"a"}, {:letter=>"a"}, {:letter=>"a"}],
# "e"=>[{:letter=>"e"}, {:letter=>"e"}],
# "b"=>[{:letter=>"b"}, {:letter=>"b"}],
# "f"=>[{:letter=>"f"}]}
The following tells us that a solution exists.
2 * groups.map { |_,v| v.size }.max > arr.size + 1
#=> 2 * [3, 2, 2, 1].max > 8 + 1
#=> 2 * 3 > 9
#=> 6 > 9
#=> false
Next create an enumerator letters.
max_key = groups.max_by { |_,a| a.size }.first
#=> "a"
letters = ([max_key] + (groups.keys - [max_key])).cycle
#=> #<Enumerator: ["a", "e", "b", "f"]:cycle>
The elements of letters are generated as follows.
letters.next #=> "a"
letters.next #=> "e"
letters.next #=> "b"
letters.next #=> "f"
letters.next #=> "a"
letters.next #=> "e"
... ad infinititum
See Array#cycle.
I can best explain the remaining calculations by salting the method with puts statements before running the method. Note that arr.size #=> 8.
def reorder(arr)
groups = arr.group_by { |h| h[:letter] }
puts "groups = #{groups}"
return nil if 2 * groups.map { |_,v| v.size }.max > arr.size + 1
max_key = groups.max_by { |_,a| a.size }.first
letters = ([max_key] + (groups.keys - [max_key])).cycle
ordered = []
while ordered.size < arr.size
puts "\nordered.size = #{ordered.size} < #{arr.size} = #{ordered.size < arr.size}"
k = letters.next
puts "k = #{k}"
puts "groups[#{k}].empty? = #{groups[k].empty?}"
ordered << groups[k].pop unless groups[k].empty?
puts "ordered = #{ordered}"
puts "groups = #{groups}"
end
ordered
end
reorder(arr)
#=> [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
# {:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}]
The following is displayed.
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}, {:letter=>"e"}],
"b"=>[{:letter=>"b"}, {:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 0 < 8 = true
k = a
groups[a].empty? = false
ordered = [{:letter=>"a"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}, {:letter=>"e"}],
"b"=>[{:letter=>"b"}, {:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 1 < 8 = true
k = e
groups[e].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}],
"b"=>[{:letter=>"b"}, {:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 2 < 8 = true
k = b
groups[b].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}],
"b"=>[{:letter=>"b"}],
"f"=>[{:letter=>"f"}]}
ordered.size = 3 < 8 = true
k = f
groups[f].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"}]
groups = {"a"=>[{:letter=>"a"}, {:letter=>"a"}],
"e"=>[{:letter=>"e"}], "b"=>[{:letter=>"b"}],
"f"=>[]}
ordered.size = 4 < 8 = true
k = a
groups[a].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}]
groups = {"a"=>[{:letter=>"a"}],
"e"=>[{:letter=>"e"}],
"b"=>[{:letter=>"b"}],
"f"=>[]}
ordered.size = 5 < 8 = true
k = e
groups[e].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}]
groups = {"a"=>[{:letter=>"a"}],
"e"=>[],
"b"=>[{:letter=>"b"}],
"f"=>[]}
ordered.size = 6 < 8 = true
k = b
groups[b].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}]
groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[], "f"=>[]}
ordered.size = 7 < 8 = true
k = f
groups[f].empty? = true
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}]
groups = {"a"=>[{:letter=>"a"}], "e"=>[], "b"=>[], "f"=>[]}
ordered.size = 7 < 8 = true
k = a
groups[a].empty? = false
ordered = [{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"f"},
{:letter=>"a"}, {:letter=>"e"}, {:letter=>"b"}, {:letter=>"a"}]
groups = {"a"=>[], "e"=>[], "b"=>[], "f"=>[]}
Refering to the revised question, if
arr = ["a", "a", "b", "b", "c", "c", "d", "d", "e", "e"]
one could simply write:
arr.each_slice(arr.index { |s| s != arr.first }.to_a.transpose.flatten
#=> ["a", "b", "c", "d", "e", "a", "b", "c", "d", "e"]
or
arr.each_slice(arr.count(arr.first)).to_a.transpose.flatten