116

I'm using Ruby 1.8.6 with Rails 1.2.3, and need to determine whether two arrays have the same elements, regardless of whether or not they're in the same order. One of the arrays is guaranteed not to contain duplicates (the other might, in which case the answer is no).

My first thought was

require 'set' a.to_set == b.to_set 

but I was wondering if there was a more efficient or idiomatic way of doing it.

4
  • possible duplicate of Ruby - Does array A contain all elements of array B Commented Jun 6, 2012 at 19:33
  • Try array.should =~ another_array check stackoverflow.com/questions/2978922/… Commented May 17, 2013 at 0:06
  • You could have saved a lot of confusion by: 1) stating whether the elements of the arrays are necessarily sortable; and 2) provide a simple example to clarify what you mean by, "whether two arrays have the same elements" (e.g., do [1,2] and [2,1,1] have the same elements?) Commented Feb 14, 2015 at 19:03
  • Ruby 2.6 has introduced difference which offers a solution both very fast and very readable. More info here. Commented Jun 24, 2019 at 15:19

9 Answers 9

170

This doesn't require conversion to set:

a.sort == b.sort 
Sign up to request clarification or add additional context in comments.

4 Comments

No conversion? What is .uniq.sort then? Besides uniq is similar to to_set internally plus additional .to_a.sort
Accepting this since it's closest to what I ended up using, though without the uniqs. Actually I ended up creating one of the arrays with Range#to_a, so I only had to sort the other one.
This won't work if the array contains elements that cannot be simply sorted (e.g. an array of hashes). sahil dhankhar's solution appears to be a more general solution.
Painfully simple, for small Arrays where performance of sorting them isn't too costly. Thanks.
44

for two arrays A and B: A and B have same contents if: (A-B).blank? and (B-A).blank?

or you can just check for: ((A-B) + (B-A)).blank?

Also as suggested by @cort3z this solution als0 works for polymorphic arrays i.e

 A = [1 , "string", [1,2,3]] B = [[1,2,3] , "string", 1] (A-B).blank? and (B-A).blank? => true # while A.uniq.sort == B.uniq.sort will throw error `ArgumentError: comparison of Fixnum with String failed` 

::::::::::: EDIT :::::::::::::

As suggested in the comments, above solution fails for duplicates.Although as per the question that is not even required since the asker is not interested in duplicates(he is converting his arrays to set before checking and that masks duplicates and even if you look at the accepeted answer he is using a .uniq operator before checking and that too masks duplicates.). But still if duplicates interests you ,Just adding a check of count will fix the same(as per the question only one array can contain duplicates). So the final solution will be: A.size == B.size and ((A-B) + (B-A)).blank?

6 Comments

This will fail if either array contains duplicates. E.g., if A=[1] and B=[1,1], both (A-B) and (B-A) will return blank. See Array Documentation.
@dafrazzman totally agree with you. I have modified my answer to incorporate your feedback.But if you have a close look at the question(or the accepted answer), asker is using: a.to_set == b.to_set and the accepted answer is using a.uniq.sort == b.uniq.sort and both give exact same result as ((A-B) + (B-A)).blank? for A=[1] and B=[1,1] agree ? Since he was just asking for an improvement over his original solution , my original solution still works :) . agree?
This solution is quite nice since it handles objects of multiple types. Say you have A = [123, "test", [], some_object, nil] and B = A#because I am lazy, then A.uniq.sort will throw error (comparison of string and Array failed).
Would this be O(n) then since it's dependent on the array size? (linear)
It wouldn't work if the arrays have same size but the repeated elements are not the same. For instance A = [1, 1, 2] and B = [1, 2, 2]
|
43

Ruby 2.6+

Ruby's introduced difference in 2.6.

This gives a very fast, very readable solution here, as follows:

a = [1, 2, 3, 4, 5, 6] b = [1, 2, 3, 4, 5, 6] a.difference(b).any? # => false a.difference(b.reverse).any? # => false a = [1, 2, 3, 4, 5, 6] b = [1, 2, 3] a.difference(b).any? # => true 

However, the reverse isn't true:

a = [1, 2, 3] b = [1, 2, 3, 4, 5, 6] a.difference(b).any? # => false 

This means to get the difference in both directions it is necessary to run:

a.difference(b).any? || b.difference(a).any? 

Running the benchmarks:

a = Array.new(1000) { rand(100) } b = Array.new(1000) { rand(100) } Benchmark.ips do |x| x.report('sort') { a.sort == b.sort } x.report('sort!') { a.sort! == b.sort! } x.report('to_set') { a.to_set == b.to_set } x.report('minus') { ((a - b) + (b - a)).empty? } x.report('difference') { a.difference(b).any? } x.report('difference two way') { a.difference(b).any? || b.difference(a).any? } end sort 10.175k (± 6.2%) i/s - 50.778k in 5.015112s sort! 10.513k (± 6.8%) i/s - 53.212k in 5.089106s to_set 4.953k (± 8.8%) i/s - 24.570k in 5.037770s minus 15.290k (± 6.6%) i/s - 77.520k in 5.096902s difference 25.481k (± 7.9%) i/s - 126.600k in 5.004916s difference two way 12.652k (± 8.3%) i/s - 63.232k in 5.038155s 

My takeaway would be that difference is a great choice for a one directional diff.

If you need to check in both directions, it's a balance between performance and readability. For me, the readability pips it, but that's a call to be made on a case by case basis.

Hope that helps someone!

1 Comment

You'll still need to check sizes, using .difference as suggested gives a false positive if one of the arrays is empty.
26

Speed comparsions

require 'benchmark/ips' require 'set' a = [1, 2, 3, 4, 5, 6] b = [1, 2, 3, 4, 5, 6] Benchmark.ips do |x| x.report('sort') { a.sort == b.sort } x.report('sort!') { a.sort! == b.sort! } x.report('to_set') { a.to_set == b.to_set } x.report('minus') { ((a - b) + (b - a)).empty? } end Warming up -------------------------------------- sort 88.338k i/100ms sort! 118.207k i/100ms to_set 19.339k i/100ms minus 67.971k i/100ms Calculating ------------------------------------- sort 1.062M (± 0.9%) i/s - 5.389M in 5.075109s sort! 1.542M (± 1.2%) i/s - 7.802M in 5.061364s to_set 200.302k (± 2.1%) i/s - 1.006M in 5.022793s minus 783.106k (± 1.5%) i/s - 3.942M in 5.035311s 

6 Comments

btw order of elemetns does not affect sort's speed
Surprised me... I expected by-set comparison to outperform all others due to sets lookup O(n) time complexity. So that any well implemented sort would require O(n logn). Whereas casting to sets and looking up values would overall make it in O(n) time.
I'd expect to_set to start outperforming with large enough arrays where O(n logn) would start mattering more than the effort required to convert array to set
This is helpful, but not really an answer in itself? Perhaps better adding this to an existing solution?
In minus, it's a pity to build the union. (a - b).empty? && (b - a).empty?.
|
18

When the elements of a and b are Comparable,

a.sort == b.sort 

Correction of @mori's answer based on @steenslag's comment

1 Comment

...when a and b can be sorted.
8

If you expect [:a, :b] != [:a, :a, :b] to_set doesn't work. You can use frequency instead:

class Array def frequency p = Hash.new(0) each{ |v| p[v] += 1 } p end end [:a, :b].frequency == [:a, :a, :b].frequency #=> false [:a, :b].frequency == [:b, :a].frequency #=> true 

3 Comments

why not just a.sort == b.sort if he cares about frequency?
@fl00r What if items are not comparable? ["", :b].frequency == [:b, ""].frequency #=> true
also you can do something functional as a.group_by{|i| i} == b.group_by{|i| i}
7

If you know the arrays are of equal length and neither array contains duplicates then this works too:

( array1 & array2 ) == array1 

Explanation: the & operator in this case returns a copy of a1 sans any items not found in a2, which is the same as the original a1 iff both arrays have the same contents with no duplicates.

Analyis: Given that the order is unchanged, I'm guessing this is implemented as a double iteration so consistently O(n*n), notably worse for large arrays than a1.sort == a2.sort which should perform with worst-case O(n*logn).

3 Comments

Doesn't work always: a1 = [1,2,3], a2 = [2, 1, 3] a1 && a2 returns [2,1,3] for me which is not equal to a1
@Kaylan, don't you mean it only works when a1==a2? It may work if array1 on the right side of the equality is replaced by array2, but I doubt that the order of the elements returned by & is guaranteed.
@KalyanRaghu & is a set intersection operator for arrays, && is logical AND - they are very different!
6

combining & and size may be fast too.

require 'benchmark/ips' require 'set' Benchmark.ips do |x| x.report('sort') { a.sort == b.sort } x.report('sort!') { a.sort! == b.sort! } x.report('to_set') { a.to_set == b.to_set } x.report('minus') { ((a - b) + (b - a)).empty? } x.report('&.size') { a.size == b.size && (a & b).size == a.size } end Calculating ------------------------------------- sort 896.094k (±11.4%) i/s - 4.458M in 5.056163s sort! 1.237M (± 4.5%) i/s - 6.261M in 5.071796s to_set 224.564k (± 6.3%) i/s - 1.132M in 5.064753s minus 2.230M (± 7.0%) i/s - 11.171M in 5.038655s &.size 2.829M (± 5.4%) i/s - 14.125M in 5.010414s 

1 Comment

& description from ruby official doc Set Intersection — Returns a new array containing unique elements common to the two arrays. The order is preserved from the original array.
1

One approach is to iterate over the array with no duplicates

# assume array a has no duplicates and you want to compare to b !a.map { |n| b.include?(n) }.include?(false) 

This returns an array of trues. If any false appears, then the outer include? will return true. Thus you have to invert the whole thing to determine if it's a match.

2 Comments

@Victor Moroz, you're correct, and a frequency count would simply be O(n).
This won't work when b includes all the elements of a plus some extra

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.