Создайте дополнение списка, сохраняющее повторяющиеся значения

Данный список a = [1, 2, 2, 3] и его подписок b = [1, 2] найдите список, дополняющий b таким образом, что sorted(a) == sorted(b + complement). В приведенном выше примере complement будет списком [2, 3].

Заманчиво использовать понимание списка:

complement = [x for x in a if x not in b]

или устанавливает:

complement = list(set(a) - set(b))

Однако оба этих пути вернут complement = [3].

Очевидным способом сделать это будет:

complement = a[:]
for element in b:
    complement.remove(element)

Но это чувствует себя глубоко неудовлетворительным и не очень Pythonic. Я пропустил очевидную идиому или это так?

Как указано ниже, что касается производительности, это O(n^2) Есть ли более эффективный способ?

Ответ 1

Единственный более декларативный и, следовательно, Pythonic способ, который появляется у меня в голове и что повышает производительность для больших ba) - это использовать какой-то счетчик с декрементом:

from collections import Counter

class DecrementCounter(Counter):

    def decrement(self,x):
        if self[x]:
            self[x] -= 1
            return True
        return False

Теперь мы можем использовать понимание списка:

b_count = DecrementCounter(b)
complement = [x for x in a if not b_count.decrement(x)]

Здесь мы следим за подсчетами в b, для каждого элемента в a мы смотрим, является ли это частью b_count. Если это действительно так, мы уменьшаем счетчик и игнорируем элемент. В противном случае мы добавим его в complement. Обратите внимание, что это работает только, если мы уверены, что такой complement существует.

После вы построили complement, вы можете проверить, существует ли дополнение:

not bool(+b_count)

Если это False, то такое дополнение не может быть построено (например, a=[1] и b=[1,3]). Таким образом, полная реализация может быть:

b_count = DecrementCounter(b)
complement = [x for x in a if not b_count.decrement(x)]
if +b_count:
    raise ValueError('complement cannot be constructed')

Если поиск словаря выполняется в O (1) (что обычно происходит, только в редких случаях это O (n)), то этот алгоритм работает в O (| a | + | b |) (так что сумма размеры списков). В то время как подход remove обычно выполняется в O (| a | & times; | b |).

Ответ 2

Чтобы уменьшить сложность вашего уже действующего подхода, вы можете использовать collections.Counter (который является специализированным словарем с быстрым поиском) для подсчета элементов в обоих списках.

Затем обновите счет путем вычитания значений, а в конце фильтруйте список, сохраняя только те элементы, чей счетчик > 0, и перестройте его/цепочку, используя itertools.chain

from collections import Counter
import itertools

a  = [1, 2, 2, 2, 3]
b = [1, 2]

print(list(itertools.chain.from_iterable(x*[k] for k,x in (Counter(a)-Counter(b)).items() if x > 0)))

результат:

[2, 2, 3]

Ответ 3

O (n log n)

a = [1, 2, 2, 3]
b = [1, 2]
a.sort()
b.sort()

L = []
i = j = 0
while i < len(a) and j < len(b):
    if a[i] < b[j]:
        L.append(a[i])
        i += 1
    elif a[i] > b[j]:
        L.append(b[j])
        j += 1
    else:
        i += 1
        j += 1

while i < len(a):
    L.append(a[i])
    i += 1

while j < len(b):
    L.append(b[j])
    j += 1

print(L)

Ответ 4

Основная идея: если значения не уникальны, сделайте их уникальными

def add_duplicate_position(items):
    element_counter = {}
    for item in items:
        element_counter[item] = element_counter.setdefault(item,-1) + 1
        yield element_counter[item], item

assert list(add_duplicate_position([1, 2, 2, 3])) == [(0, 1), (0, 2), (1, 2), (0, 3)]

def create_complementary_list_with_duplicates(a,b):
    a = list(add_duplicate_position(a))
    b = set(add_duplicate_position(b))
    return [item for _,item in [x for x in a if x not in b]]

a = [1, 2, 2, 3]
b = [1, 2]
assert create_complementary_list_with_duplicates(a,b) == [2, 3]

Ответ 5

Если порядок элементов в дополнении не имеет значения, то коллекции .Counter - это все, что необходимо:

from collections import Counter

a = [1, 2, 3, 2]
b = [1, 2]

complement = list((Counter(a) - Counter(b)).elements())  # complement = [2, 3]

Если порядок элементов в дополнении должен быть того же порядка, что и в исходном списке, используйте следующее:

from collections import Counter, defaultdict
from itertools import count

a = [1,2,3,2]
b = [2,1]

c = Counter(b)
d = defaultdict(count)

complement = [x for x in a if next(d[x]) >= c[x]]  # complement = [3, 2]