Поиск самого длинного пробега в списке
Учитывая список данных, я пытаюсь создать новый список, в котором значение в позиции i
является длиной самого длинного запуска, начиная с позиции i
в исходном списке. Например, данный
x_list = [1, 1, 2, 3, 3, 3]
Должен вернуться:
run_list = [2, 1, 1, 3, 2, 1]
Мое решение:
freq_list = []
current = x_list[0]
count = 0
for num in x_list:
if num == current:
count += 1
else:
freq_list.append((current,count))
current = num
count = 1
freq_list.append((current,count))
run_list = []
for i in freq_list:
z = i[1]
while z > 0:
run_list.append(z)
z -= 1
Во-первых, я создаю список freq_list
кортежей, где каждый элемент первого кортежа является элементом из x_list
, а второй - номером общего прогона.
В этом случае:
freq_list = [(1, 2), (2, 1), (3, 3)]
Имея это, я создаю новый список и добавляю соответствующие значения.
Однако мне было интересно, есть ли более короткий путь/другой способ сделать это?
Ответы
Ответ 1
Здесь простое решение, которое выполняет итерацию по списку назад и увеличивает счетчик каждый раз, когда число повторяется:
last_num = None
result = []
for num in reversed(x_list):
if num != last_num:
# if the number changed, reset the counter to 1
counter = 1
last_num = num
else:
# if the number is the same, increment the counter
counter += 1
result.append(counter)
# reverse the result
result = list(reversed(result))
Результат:
[2, 1, 1, 3, 2, 1]
Ответ 2
Это возможно с помощью itertools
:
from itertools import groupby, chain
x_list = [1, 1, 2, 3, 3, 3]
gen = (range(len(list(j)), 0, -1) for _, j in groupby(x_list))
res = list(chain.from_iterable(gen))
Результат
[2, 1, 1, 3, 2, 1]
объяснение
- Сначала используйте
itertools.groupby
для группировки одинаковых элементов в вашем списке. - Для каждого элемента в вашей
groupby
создайте объект range
который отсчитывает назад от длины числа последовательных элементов до 1. - Превратите это все в генератор, чтобы избежать создания списка списков.
- Используйте
itertools.chain
для цепи диапазонов от генератора.
Замечание по эффективности
Производительность будет уступать решению Aran-Fey. Хотя itertools.groupby
- O (n), он сильно использует дорогие вызовы __next__
. Они не масштабируются так же, как итерация в простых for
петель. См. Документацию itertools для псевдокода groupby
.
Если производительность является вашей главной задачей, придерживайтесь цикла for
.
Ответ 3
Вы выполняете обратный кумулятивный счет для смежных групп. Мы можем создать функцию накопленного счета Numpy с помощью
import numpy as np
def cumcount(a):
a = np.asarray(a)
b = np.append(False, a[:-1] != a[1:])
c = b.cumsum()
r = np.arange(len(a))
return r - np.append(0, np.flatnonzero(b))[c] + 1
а затем сгенерировать наш результат с помощью
a = np.array(x_list)
cumcount(a[::-1])[::-1]
array([2, 1, 1, 3, 2, 1])
Ответ 4
Я бы использовал генератор для такого рода задач, потому что он избегает создания полученного списка поэтапно и может использоваться лениво, если нужно:
def gen(iterable): # you have to think about a better name :-)
iterable = iter(iterable)
# Get the first element, in case that fails
# we can stop right now.
try:
last_seen = next(iterable)
except StopIteration:
return
count = 1
# Go through the remaining items
for item in iterable:
if item == last_seen:
count += 1
else:
# The consecutive run finished, return the
# desired values for the run and then reset
# counter and the new item for the next run.
yield from range(count, 0, -1)
count = 1
last_seen = item
# Return the result for the last run
yield from range(count, 0, -1)
Это также будет работать, если вход не может быть reversed
(некоторые генераторы/итераторы не могут быть отменены):
>>> x_list = (i for i in range(10)) # it a generator despite the variable name :-)
>>> ... arans solution ...
TypeError: 'generator' object is not reversible
>>> list(gen((i for i in range(10))))
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
И он работает для вашего ввода:
>>> x_list = [1, 1, 2, 3, 3, 3]
>>> list(gen(x_list))
[2, 1, 1, 3, 2, 1]
Это можно упростить, используя itertools.groupby
:
import itertools
def gen(iterable):
for _, group in itertools.groupby(iterable):
length = sum(1 for _ in group) # or len(list(group))
yield from range(length, 0, -1)
>>> x_list = [1, 1, 2, 3, 3, 3]
>>> list(gen(x_list))
[2, 1, 1, 3, 2, 1]
Я также сделал некоторые тесты и, согласно этим решениям Aran-Feys, является самым быстрым, за исключением длинных списков, в которых выигрывает решение piRSquareds:
Это была моя настройка бенчмаркинга, если вы хотите подтвердить результаты:
from itertools import groupby, chain
import numpy as np
def gen1(iterable):
iterable = iter(iterable)
try:
last_seen = next(iterable)
except StopIteration:
return
count = 1
for item in iterable:
if item == last_seen:
count += 1
else:
yield from range(count, 0, -1)
count = 1
last_seen = item
yield from range(count, 0, -1)
def gen2(iterable):
for _, group in groupby(iterable):
length = sum(1 for _ in group)
yield from range(length, 0, -1)
def mseifert1(iterable):
return list(gen1(iterable))
def mseifert2(iterable):
return list(gen2(iterable))
def aran(x_list):
last_num = None
result = []
for num in reversed(x_list):
if num != last_num:
counter = 1
last_num = num
else:
counter += 1
result.append(counter)
return list(reversed(result))
def jpp(x_list):
gen = (range(len(list(j)), 0, -1) for _, j in groupby(x_list))
res = list(chain.from_iterable(gen))
return res
def cumcount(a):
a = np.asarray(a)
b = np.append(False, a[:-1] != a[1:])
c = b.cumsum()
r = np.arange(len(a))
return r - np.append(0, np.flatnonzero(b))[c] + 1
def pirsquared(x_list):
a = np.array(x_list)
return cumcount(a[::-1])[::-1]
from simple_benchmark import benchmark
import random
funcs = [mseifert1, mseifert2, aran, jpp, pirsquared]
args = {2**i: [random.randint(0, 5) for _ in range(2**i)] for i in range(1, 20)}
bench = benchmark(funcs, args, "list size")
%matplotlib notebook
bench.plot()
Python 3.6.5, NumPy 1.14
Ответ 5
Вот простой итеративный подход к его достижению с помощью collections.Counter
:
from collections import Counter
x_list = [1, 1, 2, 3, 3, 3]
x_counter, run_list = Counter(x_list), []
for x in x_list:
run_list.append(x_counter[x])
x_counter[x] -= 1
который вернет вам run_list
как:
[2, 1, 1, 3, 2, 1]
В качестве альтернативы, здесь один лайнер для достижения этой цели с использованием перечисления списков с enumerate
но он неэффективен из-за итеративного использования list.index(..)
:
>>> [x_list[i:].count(x) for i, x in enumerate(x_list)]
[2, 1, 1, 3, 2, 1]
Ответ 6
Вы можете подсчитать последовательные равные элементы, а затем добавить обратный отсчет от count-of-items к 1 к результату:
def runs(p):
old = p[0]
n = 0
q = []
for x in p:
if x == old:
n += 1
else:
q.extend(range(n, 0, -1))
n = 1
old = x
q.extend(range(n, 0, -1))
return q
(Через пару минут) О, это то же самое, что и код MSeifert, но без итеративного аспекта. Эта версия кажется почти такой же быстрой, как метод Аран-Фей.