How to compare integer in a list and return a list of integer and its frequencies

Hi all, I’m trying to do some Leetcode exercises in elixir.
I have a function that takes an integer and it needs to return a list of lists that includes the digit frequency and the digit itself.
Something list this for example number = 11223411 => return [ [2,1], [2,2], [1,3] [1,4], [2,1] ]
The first number is the frequency and the second one is the digit itself. I’m stuck on counting the digit.

So here’s where I am

    list_of_integer = Integer.digits(number)
    list_length = len(list_of_integer)

    for n <- 0..(list_length-2) do
         if (Enum.at(list_of_integer, n) == (Enum.at(list_of_integer, n + 1))) do 
               # here am not really sure what to do next.. is it the right approach tho? :(     
         end
    end

This Leetcode is number 38 ‘count and say’. If anyone has done it, I would appreciate it if you could share your approach to solving this.

With elixir you don’t (and usually can’t) solve issues by using loops like you’d do in non-functional languages. You’d want to look into reducing over your data to get to the expected result. for might look like a loop, but it’s not.

1 Like

You might be interested in Enum.frequencies/1. It returns a map, but you can transform that to a nested list. Implementing it manually with Enum.reduce/3 is a good learning exercise though.

Enum.frequencies/1 wont return what i need. because it’ll return the frequency of each unique element… so for example if I have 112211, it’ll return 1 => 4, 2 => 2. what I need is the frequency in the exact order
[2,1], [2,2], [2,1]
How do you mean with using Enum.reduce?

A general note: 99.9% of the time, if you find yourself writing Enum.at inside of a loop - especially with an index value that can go all the way to the end - you should consider a different approach. The motivation is that every call to Enum.at takes time proportional to the index so accessing elements at the end gets increasingly expensive.

For this specific problem, it’s worth calling out how this isn’t solved by Integer.digits + Enum.frequencies: the digits are only counted when they are together.

Skimming through the list of functions in Enum (you should do this, A LOT) we find a promising function: chunk_by:

https://hexdocs.pm/elixir/Enum.html#chunk_by/2

An example of using this looks like:

number
|> Integer.digits()
|> Enum.chunk_by(& &1)

which would result in [[1, 1], [2, 2], [3], [4], [1, 1]] where each element of the list is a list of the same number over and over.

Then you can transform that result into the counts you’re expecting:

number
|> Integer.digits()
|> Enum.chunk_by(& &1)
|> Enum.map(fn many_digits -> [length(many_digits), hd(many_digits)] end)
4 Likes

You should try not to think how You would solve this in your previous language. It will only slow You down.

A good start, as mentionned by @al2o3cr is to learn the Enum module.

A solution with Enum.reduce might look like this. What You need to understand, value don’t change, but can be transformed.

{list, previous, count} = Enum.reduce(list_of_integer, {[], nil, 0}, fn integer, {list, previous, count} -> 
  if integer == previous do
    ... return an acc with incremented count
  else
   ... return an acc with count = 1, previous = integer, update list with new element
  end
end)
1 Like

Could use Enum.reduce/3!

reducer = fn (digit, acc) ->
  case acc[digit] do
    nil -> Map.put(acc, digit, 1)
    current_count -> Map.put(acc, digit, current_count + 1)
  end
end

11223411
|> Integer.to_string
|> String.graphemes
|> Enum.reduce(%{}, reducer)
|> Map.to_list

Result: [{"1", 4}, {"2", 2}, {"3", 1}, {"4", 1}]
1 Like

thank you very much!

A map as accumulator won‘t retain the correct order for a longer input. For short inputs it only works due to an implementation detail of small maps one shouldn‘t depend on.