Nathan Clayton

Back to blog

LeetCode Contains Duplicate: From Brute Force to Peak Ruby

A simple LeetCode problem turned into a surprisingly good lesson in both algorithms and Ruby itself. From brute force nested loops to hash-based Sets to Ruby’s almost suspiciously concise uniq.size solution, this problem highlights how the same challenge can evolve as you learn both performance optimization and the philosophy of the language.

One thing I have been enjoying while working through LeetCode problems in Ruby is seeing how the same solution evolves as you learn both algorithms and the language itself.

Contains Duplicate is a perfect example of that progression.

The problem itself is simple:

Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct.

At first glance, this feels almost trivial.

Then you realize there are multiple ways to solve it, each teaching something slightly different about performance, readability, and Ruby itself.


Brute Force

The most direct solution is simply comparing every number against every other number.

def contains_duplicate_brute_force(nums)
    left = 0

    while left < nums.length
        right = left + 1

        while right < nums.length
            return true if nums[left] == nums[right]
            right += 1
        end

        left += 1
    end

    false
end

Conceptually, this makes perfect sense:

  • Pick a number
  • Compare it against the rest of the array
  • If you ever find a match, return true

This is a classic nested loop approach.

The problem is performance.

For an array of size n, this solution performs roughly N*N comparisons in the worst case.

That gives us:

  • Time Complexity: O(n²)
  • Space Complexity: O(1)

The nice thing about brute force solutions is they are often the easiest to reason about during interviews.

The downside is they scale terribly.


Using a Set

This is the solution that usually represents the "real" algorithmic improvement.

require 'set'

def contains_duplicate(nums)
    seen = Set.new

    nums.each do |num|
        return true if seen.include?(num)
        seen.add(num)
    end

    false
end

This works because Sets are optimized for fast lookups.

Instead of comparing each element against every other element:

  • We store numbers we've already seen
  • Before adding a new number, we check whether it already exists

That changes the performance dramatically.

  • Time Complexity: O(n)
  • Space Complexity: O(n)

This is usually considered the ideal interview solution.

It demonstrates:

  • understanding of hash-based lookup structures
  • ability to trade memory for speed
  • awareness of algorithmic complexity

It also reads cleanly.

One small Ruby detail worth mentioning:

require 'set'

is needed when running this outside platforms like LeetCode, since Set is part of Ruby’s standard library but is not loaded automatically.


The Extremely Ruby Solution

Then Ruby comes along and says:

“What if we just... removed duplicates first?”

def contains_duplicate_with_length(nums)
    nums.uniq.length != nums.length
end

def contains_duplicate_with_size(nums)
    nums.uniq.size != nums.size
end

This is aggressively Ruby.

The idea is simple:

nums.uniq

returns a new array with duplicates removed.

So:

  • if the lengths are different
  • duplicates must have existed

Honestly, this is probably what many experienced Ruby developers would write in real production code unless performance became an issue.

It is short.

It is readable.

It communicates intent immediately.

And under the hood, uniq is already using hashing internally anyway.


length vs size

Now for the funny part.

These two methods are effectively identical:

length
size

For Arrays in Ruby:

arr.length == arr.size

Both return the number of elements.

So why do both exist?

Mostly readability and convention.

length

Feels more semantic when talking about sequences or strings.

name.length

reads naturally.

size

Feels more generic and collection-oriented.

users.size

also reads naturally.

Under the hood for Arrays, they are aliases.

There is no meaningful performance difference here.

This means these two solutions are effectively the same:

nums.uniq.length != nums.length

and

nums.uniq.size != nums.size

The only real difference is stylistic preference.


Which Solution Should You Use?

Interview Setting

The Set solution is probably the strongest answer.

It demonstrates actual algorithmic optimization.

seen = Set.new

shows the interviewer you understand data structures and lookup efficiency.


Real Ruby Code

Honestly?

You will probably see this a lot:

nums.uniq.size != nums.size

Ruby developers tend to value expressiveness heavily.

If performance is acceptable, the cleanest code often wins.


The Fun Part About Learning Ruby

One thing I keep noticing while learning Ruby is how often the language encourages you to think at a higher level.

In Java or C++, I would instinctively reach for loops immediately.

Ruby constantly nudges you toward:

  • expressing intent
  • using collections effectively
  • leveraging built-in abstractions
  • writing code that reads almost like English

Sometimes that can feel magical.

Sometimes it feels suspiciously magical.

But it has been fun seeing how even a tiny LeetCode problem can evolve from:

compare every number manually

into:

nums.uniq.size != nums.size

with almost the entire implementation detail disappearing behind expressive language features.

Share this post

Spread the word on your favorite platform.

Are you sure?