I want to merge two lists in python, with the lists being of different lengths, so that the elements of the shorter list are as equally spaced within the final list as possi
This is basically the same as Bresenham's line algorithm. You can calculate "pixel" positions and use them as the indices into the lists.
Where your task differs is that you only want each element to show up once. You'd need to either modify the algorithm or post-process the indices, appending the elements from the lists only the first time they appear. There is a slight ambiguity, though: when both pixel/list indices change at the same time, you'll need to pick which one to include first. This corresponds to the two different options for interleaving the lists that are mentioned in the question and a comment.
A variant of @Jon Clements answer using more_itertools.collate with explanation.
Given
import itertools as it
import more_itertools as mit
a, b = range(1, 5), ["a", "b"]
Code
first = enumerate(a)
second = zip(it.count(0, len(a) // len(b)), b)
[x for i, x in mit.collate(first, second, key=lambda x: x[0])]
# [1, 'a', 2, 3, 'b', 4]
Details
This answer is updated to use with Python 3.
first
and second
are iterables of tuples, each tuple comprising a position-element pair.
list(first)
# [(0, 1), (1, 2), (2, 3), (3, 4)]
list(second)
# [(0, 'a'), (2, 'b')]
more_itertools.collate()
wraps heapq.merge(), which merges the pre-sorted first
and second
iterables in order. In the final list comprehension, the key
is the sorting function while the last element in each tuple in returned.
Install this third-party package via > pip install more_itertools
or use heapq.merge() directly.
If we modify @Jon's answer like this
from itertools import count
import heapq
[x[1] for x in heapq.merge(izip(count(0, len(b)), a), izip(count(0, len(a)), b))]
It doesn't matter which of a
/b
is longest
If we want to do this without itertools:
def interleave(l1, l2, default=None):
max_l = max(len(l1), len(l2))
data = map(lambda x: x + [default] * (max_l - len(x)), [l1,l2])
return [data[i%2][i/2] for i in xrange(2*max_l)]
Ahh, missed the equally spaced part. This was for some reason marked as duplicate with a question that didn't require equally spaced in the presence of differing list lengths.
With the assumption that a
is the sequence to be inserted into:
from itertools import izip, count
from operator import itemgetter
import heapq
a = [1, 2, 3, 4]
b = ['a', 'b']
fst = enumerate(a)
snd = izip(count(0, len(a) // len(b)), b)
print map(itemgetter(1), heapq.merge(fst, snd))
# [1, 'a', 2, 3, 'b', 4]
I like unutbu's answer but not the nested style, so I rewrote it. While I was there I noticed the sorting wasn't stable, so I fixed it using operator.itemgetter.
I also replaced itertools.count
with enumerate
because it's more intuitive. As a bonus it should also be more accurate for large inputs, though I haven't tested it.
import itertools
import operator
def distribute(sequence):
"""
Enumerate the sequence evenly over the interval (0, 1).
>>> list(distribute('abc'))
[(0.25, 'a'), (0.5, 'b'), (0.75, 'c')]
"""
m = len(sequence) + 1
for i, x in enumerate(sequence, 1):
yield i/m, x
def intersperse(*sequences):
"""
Evenly intersperse the sequences.
Based on https://stackoverflow.com/a/19293603/4518341
>>> list(intersperse(range(10), 'abc'))
[0, 1, 'a', 2, 3, 4, 'b', 5, 6, 7, 'c', 8, 9]
>>> list(intersperse('XY', range(10), 'abc'))
[0, 1, 'a', 2, 'X', 3, 4, 'b', 5, 6, 'Y', 7, 'c', 8, 9]
>>> ''.join(intersperse('hlwl', 'eood', 'l r!'))
'hello world!'
"""
distributions = map(distribute, sequences)
get0 = operator.itemgetter(0)
for _, x in sorted(itertools.chain(*distributions), key=get0):
yield x
Note that there's one difference from your second example, where 'b'
and 'c'
get moved down:
>>> list(intersperse(range(1, 6), 'abc'))
[1, 'a', 2, 3, 'b', 4, 'c', 5]