Заменить список списка "сокращенным" списком списка при сохранении порядка
У меня есть список списка, как в приведенном мной коде. Я хочу связать каждый дополнительный список, если есть общие значения. Затем я хочу заменить список списка сокращенным списком списка. Примеры:, если у меня есть список [[1,2,3],[3,4]]
Я хочу [1,2,3,4]
. Если у меня есть [[4,3],[1,2,3]]
, я хочу [4,3,1,2]
. Если у меня есть [[1,2,3],[a,b],[3,4],[b,c]]
, я хочу [[1,2,3,4],[a,b,c]]
или [[a,b,c],[1,2,3,4]]
Мне все равно, какой из них.
Я почти там...
Моя проблема в том, что у меня есть случай вроде [[1,2,3],[10,5],[3,8,5]]
Я хочу [1,2,3,10,5,8]
, но с моим текущим кодом я получаю [1,2,3,8,10,5]
Вот мой код:
import itertools
a = [1,2,3]
b = [3,4]
i = [21,22]
c = [88,7,8]
e = [5,4]
d = [3, 50]
f = [8,9]
g= [9,10]
h = [20,21]
lst = [a,b,c,i,e,d,f,g,h,a,c,i]*1000
#I have a lot of list but not very many different lists
def any_overlap(a, b):
sb = set(b)
return any(itertools.imap(sb.__contains__, a))
def find_uniq(lst):
''' return the uniq parts of lst'''
seen = set()
seen_add = seen.add
return [ x for x in lst if x not in seen and not seen_add(x)]
def overlap_inlist(o_lst, lstoflst):
'''
Search for overlap, using "any_overlap", of a list( o_lst) in a list of lists (lstoflst).
If there is overlap add the uniq part of the found list to the search list, and keep
track of where that list was found
'''
used_lst =[ ]
n_lst =[ ]
for lst_num, each_lst in enumerate(lstoflst):
if any_overlap(o_lst,each_lst):
n_lst.extend(each_lst)
used_lst.append(lst_num)
n_lst= find_uniq(n_lst)
return n_lst, used_lst
def comb_list(lst):
'''
For each list in a list of list find all the overlaps using 'ovelap_inlist'.
Update the list each time to delete the found lists. Return the final combined list.
'''
for updated_lst in lst:
n_lst, used_lst = overlap_inlist(updated_lst,lst)
lst[:] = [x for i,x in enumerate(lst) if i not in used_lst]
lst.insert(0,n_lst)
return lst
comb_lst = comb_list(lst)
print comb_lst
Вывод из этого script:
[[88, 7, 8, 9, 10], [1, 2, 3, 4, 50, 5], [21, 22, 20]]
Я хочу, чтобы ключ находился в исходном порядке, например:
[[88, 7, 8, 9, 10], [1, 2, 3, 4, 5, 50,], [21, 22, 20]]
5 и 50 переключаются в новый lst [2]
Я немного новичок в python. Я был бы признателен за любые решения проблемы или комментарии по моему текущему коду. Я не компьютерные ученые, я полагаю, что может быть какой-то алгоритм, который делает это быстро (может быть, из теории множеств?). Если есть такой алгоритм, укажите на правильное направление.
Я могу сделать этот способ более сложным, чем это...
Спасибо!!
Ответы
Ответ 1
Здесь применяется грубая сила (это может быть проще понять):
from itertools import chain
def condense(*lists):
# remember original positions
positions = {}
for pos, item in enumerate(chain(*lists)):
if item not in positions:
positions[item] = pos
# condense disregarding order
sets = condense_sets(map(set, lists))
# restore order
result = [sorted(s, key=positions.get) for s in sets]
return result if len(result) != 1 else result[0]
def condense_sets(sets):
result = []
for candidate in sets:
for current in result:
if candidate & current: # found overlap
current |= candidate # combine (merge sets)
# new items from candidate may create an overlap
# between current set and the remaining result sets
result = condense_sets(result) # merge such sets
break
else: # no common elements found (or result is empty)
result.append(candidate)
return result
Пример
>>> condense([1,2,3], [10,5], [3,8,5])
[1, 2, 3, 10, 5, 8]
>>> a = [1,2,3]
>>> b = [3,4]
>>> i = [21,22]
>>> c = [88,7,8]
>>> e = [5,4]
>>> d = [3, 50]
>>> f = [8,9]
>>> g= [9,10]
>>> h = [20,21]
>>> condense(*[a,b,c,i,e,d,f,g,h,a,c,i]*1000)
[[1, 2, 3, 4, 5, 50], [88, 7, 8, 9, 10], [21, 22, 20]]
>>> condense([1], [2, 3, 2])
[[1], [2, 3]]
Сравнение производительности
condense_*()
функции взяты из ответов на этот вопрос. lst_OP
список ввода из вопроса (разного размера), lst_BK
- тестовый список из @Blckknght answer (разного размера). См. источник.
Измерения показывают, что решения, основанные на понятиях "непересекающихся множеств" и "связанных компонентов неориентированного графа", выполняются аналогично на обоих типах ввода.
name time ratio comment
condense_hynekcer 5.79 msec 1.00 lst_OP
condense_hynekcer2 7.4 msec 1.28 lst_OP
condense_pillmuncher2 11.5 msec 1.99 lst_OP
condense_blckknght 16.8 msec 2.91 lst_OP
condense_jfs 26 msec 4.49 lst_OP
condense_BK 30.5 msec 5.28 lst_OP
condense_blckknght2 30.9 msec 5.34 lst_OP
condense_blckknght3 32.5 msec 5.62 lst_OP
name time ratio comment
condense_blckknght 964 usec 1.00 lst_BK
condense_blckknght2 1.41 msec 1.47 lst_BK
condense_blckknght3 1.46 msec 1.51 lst_BK
condense_hynekcer2 1.95 msec 2.03 lst_BK
condense_pillmuncher2 2.1 msec 2.18 lst_BK
condense_hynekcer 2.12 msec 2.20 lst_BK
condense_BK 3.39 msec 3.52 lst_BK
condense_jfs 544 msec 563.66 lst_BK
name time ratio comment
condense_hynekcer 6.86 msec 1.00 lst_rnd
condense_jfs 16.8 msec 2.44 lst_rnd
condense_blckknght 28.6 msec 4.16 lst_rnd
condense_blckknght2 56.1 msec 8.18 lst_rnd
condense_blckknght3 56.3 msec 8.21 lst_rnd
condense_BK 70.2 msec 10.23 lst_rnd
condense_pillmuncher2 324 msec 47.22 lst_rnd
condense_hynekcer2 334 msec 48.61 lst_rnd
Чтобы воспроизвести результаты clone gist и запустить measure-performance-condense-lists.py
Ответ 2
Вот мой подход. Он использует концепцию "непересекающегося множества", чтобы сначала определить, какие подсловы накладываются друг на друга, затем соединяет их вместе, устраняя дубликаты.
from collections import OrderedDict
def disjoint_set_find(djs, node): # disjoint set find, with path compression
if node not in djs: # base case, node is a root of a set
return node
djs[node] = disjoint_set_find(djs, djs[node]) # recurse, caching results
return djs[node]
def disjoint_set_union(djs, first, second):
first = disjoint_set_find(djs, first) # find root of first set
second = disjoint_set_find(djs, second) # and of second set
if first < second: # make smaller root the root of the new combined set
djs[second] = first
elif second < first:
djs[first] = second
# deliberately ignore the case where first == second (same set)
def condenseBK(*master_list):
values = OrderedDict() # maps values to the first sublist containing them
overlaps = {} # maps sublist indexes to each other to form a disjoint set
for i, sublist in enumerate(master_list):
for v in sublist:
if v not in values: # no overlap, so just store value
values[v] = i
else: # overlap detected, do a disjoint set union
disjoint_set_union(overlaps, values[v], i)
output = [] # results
output_map = {} # map from original indexes to output indexes
for v, i, in values.items(): # iterate over values in order
root = disjoint_set_find(overlaps, i)
if root not in output_map:
output_map[i] = len(output)
output.append([]) # create new output sublists as necessary
output[output_map[root]].append(v)
return output
Пример вывода:
>>> a = [1,2,3]
>>> b = [3,4]
>>> c = [88,7,8]
>>> d = [3, 50]
>>> e = [5,4]
>>> f = [8,9]
>>> g = [9,10]
>>> h = [20,21]
>>> i = [21,22]
>>> lst = [a,b,c,i,e,d,f,g,h,a,c,i]*1000
>>> condenseBK(*lst)
[[1, 2, 3, 4, 5, 50], [88, 7, 8, 9, 10], [21, 22, 20]]
Объяснение алгоритма:
По запросу здесь объясняется, как работает мой код.
Первые две функции реализуют операции find
и union
набора disjoint set. Структура данных реализована со сдвигом словаря узлов к их родительским узлам. Любой node, который не является ключом словаря, представляет собой root
набора. Операция find
возвращает корень node набора, содержащего заданный node
. Чтобы немного помочь производительности, я реализовал "сжатие пути", что уменьшает количество рекурсивных шагов, необходимых с течением времени. Операция union
объединяет множества, содержащие ее аргументы first
и second
.
Основная функция condense
состоит из двух частей. Во-первых, он создает пару структур данных, а затем использует их для построения вывода.
values
- это OrderedDictionary, который сопоставляется с каждым значением с индексом первого подсписка, в котором он содержится. Порядок добавления каждого значения используется для вывода результата в правильном порядке.
overlaps
- словарь, используемый как для непересекающегося множества. Его узлы представляют собой целые индексы перекрывающихся подписок.
Первые петли заполняют эти две структуры данных. Они перебирают подсписки и их содержимое. Если значение ранее не было замечено, оно добавляется в словарь values
. Если он был замечен, текущий подсписок перекрывает предыдущий подписок, содержащий это значение.
Чтобы устранить перекрытие, код выполняет объединение непересекающихся множеств, содержащих два подписок.
Выход построен в списке output
. Поскольку, вероятно, будет меньше выходных подписок, чем во входных данных, нам нужен дополнительный словарь для сопоставления между старыми индексами (от ввода) и новыми индексами, которые применяются к выходному списку.
Чтобы заполнить список результатов, мы перебираем значения, которые происходят в том порядке, в котором они были добавлены благодаря использованию класса OrderedDict. Используя непересекающееся множество, он может комбинировать перекрывающиеся списки правильно.
Этот алгоритм имеет очень хорошую производительность, когда есть много списков для обработки, которые не перекрываются сразу. Например, этот набор из 200 трехэлементных списков в конечном счете все перекрывает, но вы только начинаете видеть, что перекрытия появляются после обработки первых 100:
lst2 = [list(range(4*i, 4*(i+1)-1)) for i in range(100)] + \
[list(range(4*i+2, 4*(i+1)+1)) for i in range(100)]
Ответ 3
Я уверен, , что есть более чистый способ сделать это, но я начал по определенному пути и сделал то, что мне пришлось сделать, чтобы он работал без рефакторинга.
lookup = {}
out = []
index = 0
for grp in lst:
keys = [lookup.get(num, None) for num in grp]
keys = [key for key in keys if key is not None]
if len(keys):
if len(set(keys)) != 1:
for num in grp:
out[keys[0]].append(num)
seen = set()
keys = [key for key in keys if key not in seen and not seen.add(key)]
for key in keys[1:]:
out[keys[0]].extend(out[key])
del out[key]
seen = set()
out[keys[0]] = [item for item in out[keys[0]] if item not in seen and not seen.add(item)]
else:
for num in grp:
lookup[num] = keys[0]
out[keys[0]].extend(grp)
seen = set()
out[keys[0]] = [item for item in out[keys[0]] if item not in seen and not seen.add(item)]
else:
out.append(grp)
for num in grp:
lookup[num] = index
index += 1
print out
Благодаря @Steven для метода сокращения списка с помощью набора.
OUTPUT
[[1, 2, 3, 4, 5, 50], [88, 7, 8, 9, 10], [21, 22, 20]]
Ответ 4
Ваша проблема, по сути, является теоретико-графической, проблема связанных компонентов, с дополнительным требованием относительно порядка элементов каждой компоненты.
В вашей программе набор всех списков формирует неориентированный граф, где каждый список является node на графике. Мы говорим, что два списка связаны напрямую, если они имеют общие элементы и связаны косвенно, если существует третий список, к которому оба подключены, прямо или косвенно. С учетом, например, три списка [a, b], [b, c] и [c, d], тогда [a, b] и [b, c] связаны непосредственно, а также [b, c] и [c, d], но [a, b] и [c, d] связаны косвенно, поскольку, хотя они не имеют общих элементов, оба они совместно используют элементы с одним и тем же списком [b, c].
Группа узлов является связной компонентой, если все узлы в группе связаны (прямо или косвенно), и никакая другая node в графе не связана с любым node в группе.
Существует довольно простой алгоритм линейного времени, который генерирует все связанные компоненты в неориентированном графе. Используя это, мы можем определить функцию, которая генерирует все списки сжатых непересекающихся списков, сохраняя порядок их элементов:
from itertools import imap, combinations_with_replacement
from collections import defaultdict
def connected_components(edges):
neighbors = defaultdict(set)
for a, b in edges:
neighbors[a].add(b)
neighbors[b].add(a)
seen = set()
def component(node, neighbors=neighbors, seen=seen, see=seen.add):
unseen = set([node])
next_unseen = unseen.pop
while unseen:
node = next_unseen()
see(node)
unseen |= neighbors[node] - seen
yield node
for node in neighbors:
if node not in seen:
yield component(node)
def condense(lists):
tuples = combinations_with_replacement(enumerate(imap(tuple, lists)), 2)
overlapping = ((a, b) for a, b in tuples
if not set(a[1]).isdisjoint(b[1]))
seen = set()
see = seen.add
for component in connected_components(overlapping):
yield [item for each in sorted(component)
for item in each[1]
if not (item in seen or see(item))]
print list(condense([[1, 2, 3], [10, 5], [3, 8, 5], [9]]))
print list(condense([[1, 2, 3], [5, 6], [3, 4], [6, 7]]))
Результат:
[[1, 2, 3, 10, 5, 8], [9]]
[[5, 6, 7], [1, 2, 3, 4]]
Сложность времени condense()
является квадратичной, так как каждый список должен быть протестирован против каждого другого списка, чтобы узнать, имеют ли они общие элементы. Поэтому производительность ужасная. Можем ли мы как-нибудь улучшить его? Да.
Два списка связаны напрямую, если они имеют общие элементы. Мы можем изменить это соотношение: два элемента напрямую связаны, если они принадлежат к одному и тому же списку, и косвенно связаны, если существует третий элемент, который связан (прямо или косвенно) с обоими из них. С учетом, например, два списка [a, b] и [b, c], то a и b связаны напрямую, а также b и c, и поэтому a и c связаны косвенно. Если мы также адаптируем идею J.F.Sebastians о сохранении положения каждого первого элемента, мы можем реализовать его так:
def condense(lists):
neighbors = defaultdict(set)
positions = {}
position = 0
for each in lists:
for item in each:
neighbors[item].update(each)
if item not in positions:
positions[item] = position
position += 1
seen = set()
def component(node, neighbors=neighbors, seen=seen, see=seen.add):
unseen = set([node])
next_unseen = unseen.pop
while unseen:
node = next_unseen()
see(node)
unseen |= neighbors[node] - seen
yield node
for node in neighbors:
if node not in seen:
yield sorted(component(node), key=positions.get)
Он по-прежнему использует алгоритм связанных компонентов, но на этот раз мы рассматриваем элементы как связанные, а не списки. Результаты те же, что и раньше, но поскольку временная сложность теперь линейна, она работает намного быстрее.
Ответ 5
Я попытался написать быстрое и читаемое решение . Он никогда не намного медленнее других решений, если я знаю, но иногда может быть намного быстрее, потому что он дополнительно оптимизирован для более длительного подсписок или для многих подписок, которые являются подмножествами любой из существующих групп. (Это мотивировано текстом вопроса "У меня много списков, но не очень много разных списков".) В коде используется меньше памяти только для сжатых данных, которые могут быть намного меньше исходных данных. Он может работать, например. с генератором, собирающим данные из процесса реального времени. Оценка сложности O (n log n). Я думаю, что ни один алгоритм, использующий сортировку, не может быть линейной сложности.
def condense(lists):
groups = {} # items divided into groups {id(the_set): the_set,...}
members = {} # mapping from item to group
positions = {} # mapping from item to sequential ordering
iposition = 0 # counter for positions
for sublist in lists:
if not sublist or members.get(sublist[0], set()).issuperset(sublist):
continue # speed-up condition if all is from one group
common = set([x for x in sublist if x in members])
if common: # any common group can be a destination for merge
dst_group = members[common.pop()]
common = common - dst_group # are more groups common for sublist?
while common:
grp = members[common.pop()]
if len(grp) > len(dst_group): # merge shorter into longer grp
grp, dst_group = dst_group, grp
dst_group.update(grp)
for item in grp:
members[item] = dst_group
del groups[id(grp)]
common = common - dst_group
else: # or create a new group if nothing common
dst_group = set()
groups[id(dst_group)] = dst_group
newitems = []
for item in sublist: # merge also new items
if item not in positions:
positions[item] = iposition
iposition += 1
newitems.append(item)
members[item] = dst_group
dst_group.update(newitems)
return [sorted(x, key=positions.get) for x in groups.values()]
Это быстрее, чем pillmuncher2 для подселистов дольше, чем приблизительно 8 элементов, потому что он может работать с большим количеством предметов вместе. Это также очень быстро для списков со многими подобными подсписками или множеством подписок, которые являются подмножеством любой из существующих групп. Это быстрее на 25% по сравнению с pillmuncher2 для lst_OP, однако медленнее на 15% для lst_BK.
Пример тестовых данных с длинными подсписками - [list(range(30)) + [-i] for i in range(100)]
.
Я намеренно написал "common = common - dst_group" вместо использования оператора set -=
или "set.difference_update", потому что исправление на месте неэффективно, если набор с правой стороны намного больше, чем на с левой стороны.
Модифицированное решение pillmuncher для удобства чтения. Модификация немного медленнее оригинала из-за замены генератора на list.append. Вероятно, это наиболее читаемое быстрое решение.
# Modified pillmuncher solution
from collections import defaultdict
def condense(lists):
neighbors = defaultdict(set) # mapping from items to sublists
positions = {} # mapping from items to sequential ordering
position = 0
for each in lists:
for item in each:
neighbors[item].update(each)
if item not in positions:
positions[item] = position
position += 1
seen = set()
see = seen.add
for node in neighbors:
if node not in seen:
unseen = set([node]) # this is a "todo" set
next_unseen = unseen.pop # method alias, not called now
group = [] # collects the output
while unseen:
node = next_unseen()
see(node)
unseen |= neighbors[node] - seen
group.append(node)
yield sorted(group, key=positions.get)
Ответ 6
class List(list): pass
rv = dict()
def condense_step():
"""find and merge one overlapping pair in rv"""
for i, iv in rv.items():
for j, jv in rv.items():
if i != j and i.intersection(j):
m = i.union(j)
del rv[i]
del rv[j]
rv.setdefault(m, [])
rv[m] += iv
rv[m] += jv
return True
def unique(l):
"""flatten list-of-lists excluding duplicates"""
seen = set()
for i in sum(l, []):
if i not in seen:
seen.add(i)
yield i
def condense(inp):
rv.clear()
inp = map(List, inp)
for i in range(len(inp)):
inp[i].order = i
rv.setdefault(frozenset(inp[i]), [])
rv[frozenset(inp[i])].append(inp[i])
while condense_step():
pass
for v in rv.values():
v.sort(key=lambda x: x.order)
return [list(unique(i)) for i in rv.values()]
Ответ 7
В этом решении используется только упорядоченный словарь.
deepcopy() необходимо, если требуется, чтобы исходная копия оставалась неизменной.
from collections import OrderedDict
from copy import deepcopy
def treat(passed_list):
L = deepcopy(passed_list)
dic = OrderedDict()
for subl in L:
for x in subl:
if x not in dic:
dic[x] = subl
print 'dic at start'
print '\n'.join('%-3s : %s' % (a,dic[a])
for a in dic) + '\n'
for sublist in L:
short = []
short.extend(el for el in sublist
if el not in short)
seen = []
for k,val in dic.iteritems():
if val is sublist:
break
if k in short:
if val not in seen:
seen.append(val)
sumseen = []
for elseen in seen:
for y in elseen:
sumseen.append(y)
dic[y] = sumseen
if seen:
for el in sublist:
if el not in sumseen:
sumseen.append(el)
dic[el] = sumseen
sublist[:] = short
cumul = []
cumul.extend(lu for lu in dic.itervalues()
if lu not in cumul)
return cumul
plus = [[1,2,3,2,1],[10,5,5,5,10],
[8,5,3,3,5],[45,50,12,45,40,12]]
lst = [[1,2,3], [10,5], [3,8,5]]
for one_list in (plus,lst):
print 'one_list before == %r\n' % one_list
print 'treat(one_list) == %r\n' % treat(one_list)
print 'one_list after == %r\n' % one_list
print '===================================='
результат
one_list before == [[1, 2, 3, 2, 1], [10, 5, 5, 5, 10], [8, 5, 3, 3, 5], [45, 50, 12, 45, 40, 12]]
dic at start
1 : [1, 2, 3, 2, 1]
2 : [1, 2, 3, 2, 1]
3 : [1, 2, 3, 2, 1]
10 : [10, 5, 5, 5, 10]
5 : [10, 5, 5, 5, 10]
8 : [8, 5, 3, 3, 5]
45 : [45, 50, 12, 45, 40, 12]
50 : [45, 50, 12, 45, 40, 12]
12 : [45, 50, 12, 45, 40, 12]
40 : [45, 50, 12, 45, 40, 12]
treat(one_list) == [[1, 2, 3, 10, 5, 8], [45, 50, 12, 40]]
one_list after == [[1, 2, 3, 2, 1], [10, 5, 5, 5, 10], [8, 5, 3, 3, 5], [45, 50, 12, 45, 40, 12]]
====================================
one_list before == [[1, 2, 3], [10, 5], [3, 8, 5]]
dic at start
1 : [1, 2, 3]
2 : [1, 2, 3]
3 : [1, 2, 3]
10 : [10, 5]
5 : [10, 5]
8 : [3, 8, 5]
treat(one_list) == [[1, 2, 3, 10, 5, 8]]
one_list after == [[1, 2, 3], [10, 5], [3, 8, 5]]
====================================
Это решение имеет неудобство: оно в 2-3 раза медленнее, чем решение Дж. Ф. Себастьяна.