Заменить список списка на "сокращенный" список списка при сохранении порядка
У меня есть список списка, как в коде, который я приложил. Я хочу связать каждый подсписок, если есть какие-либо общие значения. Затем я хочу заменить список списка на сокращенный список списка. примеры: если у меня есть список [[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
Выход из этого сценария:
[[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]
Я немного новичок в питоне. Я был бы признателен за любые решения проблемы или комментарии к моему текущему коду. Я не компьютерщик, я думаю, что может быть какой-то алгоритм, который делает это быстро( может быть, из теории множеств? ). Если есть такой алгоритм, пожалуйста, укажет мне правильное направление.Возможно, я делаю этот путь более сложным, чем он есть... Спасибо!!
7 ответов:
Вот подход грубой силы (это может быть проще понять):
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 (разного размера). СмотритеИсточник .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
Чтобы воспроизвести результаты клонировать gist и запустить
measure-performance-condense-lists.py
Вот мой подход. Он использует понятие "непересекающегося множества", чтобы сначала определить, какие подсписки перекрываются друг с другом, а затем соединяет их вместе, устраняя дубликаты.
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
непересекающегося множества . Структура данных реализована с помощью словаря сопоставление узлов с их родительскими узлами. Любой узел, который не является ключом словаря, являетсяroot
множества. Операцияfind
возвращает корневой узел множества, содержащего заданныйnode
. Чтобы немного повысить производительность, я реализовал "сжатие пути", которое уменьшает количество рекурсивных шагов, необходимых с течением времени. Операцияunion
объединяет множества, содержащие ее аргументыfirst
иsecond
. Основная функцияСостоит из двух частей. Сначала он устанавливает несколько структур данных, а затем использует их для построения выходных данных.
values
является упорядоченным указателем, который сопоставляет каждое значение с индексом первого подсписка, в котором оно содержится. Порядок добавления каждого значения используется для получения выходных данных в правильном порядке.Первые циклы заполняют эти две структуры данных. Они перебирают подсписки и их содержимое. Если значение не имеет как было замечено ранее, он добавлен в словарь
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)]
Я уверен, что есть более чистый способ сделать это, но я начал с определенного пути и сделал то, что должен был, чтобы заставить его работать без какого-либо рефакторинга.
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 за технику сокращения списка с набором.
Вывод
[[1, 2, 3, 4, 5, 50], [88, 7, 8, 9, 10], [21, 22, 20]]
Ваша задача, по существу, является теоретико-Графовой, проблемой связанных компонентов, с дополнительным требованием относительно порядка элементов каждого компонента.
В вашей программе набор всех списков образует неориентированный граф, где каждый список является узлом в графе. Мы говорим, что два списка связаны непосредственно, если они имеют общие элементы, и связаны косвенно, если существует третий список, к которому оба связаны прямо или косвенно. Учитывая, например, три списки [a, b], [b, c] и [c, d], то [a, b] и [b, c] связаны непосредственно, а также [b, c] и [c, d], но [a, b] и [c, d] связаны косвенно, так как, хотя они не имеют общих элементов, они оба имеют элементы с одним и тем же списком [b, c]. Группа узлов является связным компонентом, если все узлы в группе связаны (прямо или косвенно) и ни один другой узел в графе не связан ни с одним узлом в группе.Существует довольно простое линейное время алгоритм, который генерирует все связанные компоненты в неориентированном графе. Используя это, мы можем определить функцию, которая генерирует все списки сжатых непересекающихся списков, сохраняя при этом порядок их элементов:
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 связаны косвенно. Если мы также адаптируем идею Дж. Ф. Себастьяна о хранении положения каждого элемента первым возникновение, мы можем реализовать его следующим образом:
Он по-прежнему использует алгоритм связанных компонентов, но на этот раз мы рассматриваем элементы как связанные, а не списки. Результаты те же, что и раньше, но поскольку временная сложность теперь линейна, она работает намного быстрее.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)
Я попытался написать быстрое и читаемое решение. Он никогда не бывает намного медленнее, чем другие решения, если я знаю, но иногда может быть намного быстрее, потому что он дополнительно оптимизирован для более длинного подсписка или для многих подсписков, которые являются подмножеством любой Еще существующей группы. (Это мотивировано текстом вопроса "у меня есть много списков, но не очень много разных списков.") Код использует меньше памяти только для сжатых данных, которые могут быть намного меньше исходных данных. Он может работать, например, с a генератор, собирающий данные из процесса реального времени. Оценка сложности равна 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", потому что обновление на месте не эффективно, если набор на правой стороне намного больше, чем на левой стороне.
Модифицированное решение пиллмюнчера для облегчения чтения. Модификация немного медленнее оригинала из-за замены генератора на список.добавлять. Вероятно, самое читаемое быстрое решение.
# Modified pillmuncher's 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)
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()]
Это решение использует только упорядоченный словарь.
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 '===================================='
Результат
Это решение имеет неудобство: оно в 2-3 раза медленнее, чем решение Дж.Ф. Себастьяна.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]] ====================================