Insert frequency of elements in list without using temporary list

Viewed 87

Input : ['a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']

Output: ['a', 'a', 2, 'b', 'b', 'b', 'b', 4, 'c', 'c', 2, 'd', 1, 'a',1]

What can be the best way to get the output as above without using temporary list in python? I am trying using while loop but the frequency of last element is missed out as the loop gets terminated.

l = ['a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']
    
    i = 0
    idx = 0
    length = len(l)
    while i < length:
        if l[i - 1] != l[i]:
            l.insert(i, (i - idx))
            length += 1
            idx = i + 1
            i += 2
        else:
            i += 1
    
    print(l)
5 Answers

The original code has a crucial bug: since i=0 in the first iteration, the condition is checking if the first and last items of the list match, which I don't think is what's intended.

Here's the corrected code.

l = ['a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']
    
i = 0  # dynamic index
n = 1  # length of sequence
length = len(l)
while i < length:
    if i == length-1 or l[i] != l[i+1]:
        l.insert(i+1, n)
        length += 1
        n = 1
        i += 2
    else:
        n += 1
        i += 1

print(l)

Instead, I'm comparing to the subsequent item. This of course would be a problem for the last item, but this condition is caught first by the check i == length-1.

Just add the last frequency at the end, so after the loop:

l.append(i - idx)

But make sure your input list isn't empty!

itertools is your friend:

import itertools

l = ['a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']
list(itertools.chain(*[(k, len(list(g))) for k,g in itertools.groupby(l)]))

['a', 2, 'b', 4, 'c', 2, 'd', 1, 'a', 1]

.. but fixing your own code is more satisfying ;)

Edit: So the problem definition changed again? Python 3.9+ needed:

list(itertools.chain(*[(*(gg:=list(g)), len(gg)) for k,g in itertools.groupby(l)]))

['a', 'a', 2, 'b', 'b', 'b', 'b', 4, 'c', 'c', 2, 'd', 1, 'a', 1]

Another approach could be:

from functools import reduce

breaks = [0] + [i for i in range(1, len(l)) if l[i] != l[i-1]] + [len(l)]

reduce(lambda x, y: x + y, [l[breaks[i-1]:breaks[i]] + [breaks[i] - breaks[i-1]] for i in range(1, len(breaks))])

OUTPUT

['a', 'a', 2, 'b', 'b', 'b', 'b', 4, 'c', 'c', 2, 'd', 1, 'a', 1]

Alternatively, if you would like to do everything inside the loop, this could be another approach

l = ['a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']
    

l = list(reversed(l))
e = l.pop()
res = []
res.append(e)
i = 1
while len(l) > 0:
    new_e = l.pop()
    if new_e == e:
        i += 1
    else:
        res.append(i)
        i = 1
        e = new_e
    res.append(new_e)
res.append(i)
    
print(res)

OUTPUT

['a', 'a', 2, 'b', 'b', 'b', 'b', 4, 'c', 'c', 2, 'd', 1, 'a', 1]

You can insert the repetition counts as you go if you are careful with resetting the counter so that the insertions in the list don't skew your end of streak detection:

L = ['a', 'a','b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']
count = 0                     # current number of reperitions
for i,c in enumerate(L):      # go through list's indexes and items
    if count and L[i-1] != c: # end of non-empty streak
        L.insert(i,count)     # insert count
        count = 0             # and reset it 
    else:
        count += 1            # count repetitions
L.append(count)               # count for last streak
print(L)
['a', 'a', 2, 'b', 'b', 'b', 'b', 4, 'c', 'c', 2, 'd', 1, 'a', 1]

When the count is inserted, the next iteration will be on the same item (c) that caused the end of streak. Because we reset the count to zero after insertion, that item will go to the else: part and be counted as 1 (0+1) thus properly starting a new streak.

Another way to approach this is to track the index of the first item of a streak and insert the difference between indexes when detecting a change. Here too, you need to be careful with the effect of inserting into the list that you are iterating over. The index of items that cause a break in the streak will be 1 more than their original index after inserting the count:

L = ['a', 'a','b', 'b', 'b', 'b', 'c', 'c', 'd', 'a']
s = 0                         # index of start of streak
for i,c in enumerate(L):      # go through list's indexes and items
    if L[s] != c:             # end of streak
        L.insert(i,i-s)       # insert count
        s = i+1               # track start of next streak 
L.append(len(L)-s)            # count for last streak
print(L)
['a', 'a', 2, 'b', 'b', 'b', 'b', 4, 'c', 'c', 2, 'd', 1, 'a', 1]
Related